Skip to content

feat(cli): build apps with the agent from the terminal — base44 app + base44 code - #630

Open
ayal wants to merge 14 commits into
mainfrom
terminal-t1
Open

ayal wants to merge 14 commits into
mainfrom
terminal-t1

Conversation

@ayal

@ayal ayal commented Sep 17, 2026

Copy link
Copy Markdown

What

Build Base44 apps from the terminal. This adds five non-interactive base44 builder commands (create, send a turn, status, stop, pick the model), base44 sandbox preview, and an interactive base44 code session, all built on one shared resource layer. Everything hits existing backend endpoints — no apper changes.

What it looks like

base44 code in an empty directory, before the first prompt — the rendered mark, the account, the directory, the kind of app:

base44 code — idle

The same session a few minutes into the first build — tool results as they land, the agent's current thought, the tool in flight, and the footer chips:

base44 code — building

Layout

packages/cli/src/
├── core/resources/apps/          # shared by both surfaces
│   ├── api.ts                    #   createApp · createImportedApp · sendTurn · getAppState
│   │                             #   getPreviewUrl · stopTurn · resolveActiveBranchId · GitHub re-auth
│   └── stream.ts                 #   poll full-conversation, diff → events
├── core/model.ts                 # model catalog · read/save the account's pick
└── cli/commands/
    ├── builder/                  # non-interactive atoms
    │   ├── index.ts              #   `builder` group
    │   ├── new.ts · send.ts · status.ts · stop.ts · model.ts
    │   └── shared.ts             #   createAndLinkApp · assertBuilderApp · appTypeChip · resolveBranchId · githubReauthLines
    ├── sandbox/preview.ts        # `sandbox preview` — the app's preview URL (joins the existing sandbox group)
    └── code/                     # interactive session; builds on builder/shared.ts
        ├── index.ts              #   `code` command (create on first prompt | reopen | --app-id)
        ├── session.tsx · session-engine.ts · render.ts · paste.ts

program.ts registers builder and code; the existing sandbox group gains preview. base44-client.ts adds one request header, X-Builder-Model-Selection: user-v1, so the backend honors the saved model pick (the web editor's contract; with no pick it falls back to the app default).

Command reference

Every command and flag this PR adds. Globals from the existing CLI apply where marked: --json (machine output on stdout), --app-id <id> (also BASE44_APP_ID; otherwise the linked directory), --branch <name> where a command is branch-aware.

Command Arguments / flags Globals
base44 builder new [prompt] --import <repo> build over an existing GitHub repository · --mode <direct|fork|copy> how to import (default direct) · --repo-name <name> name for the new repo when forking/copying · --from-branch <name> import a specific branch · --name <name> directory and app name (invented when omitted) · --path <dir> directory to link (default: the current directory when empty, else ./<name>) --json
base44 builder send <message> one agent turn; blocks until it finishes; {"queued":true} when the agent is busy --json · --app-id · --branch
base44 builder status building, ready, or errored, with the backend's message --json · --app-id
base44 builder stop stop the running turn, server-side --app-id · --branch
base44 builder model [model] list the catalog with the current pick, or set it by name or id (Automatic/default/auto clears the pick) --json
base44 sandbox preview the live preview URL (cold-starts the sandbox) --json · --app-id
base44 code interactive session · --import <repo> · --path <dir> · --app-id <id> opens that app from anywhere

Inside base44 code: Enter sends · Esc stops the running turn, or snaps back to live when scrolled · ↑/↓ · PgUp/PgDn scroll the transcript · Ctrl+C clears the input, then exits · Ctrl+D exits · /model opens the picker, /model <name> switches directly.

builder send and code refuse a code-first project (base44 create) and a Superagent; --mode, --repo-name and --from-branch are refused without --import; --import and --path are refused inside a linked directory.

Non-interactive — base44 builder

Where the app lands (same rule as base44 create): an empty current directory is the project itself and names the app; otherwise ./<name>/ is created. --path <dir> picks the directory explicitly. builder new and a genesis base44 code session share this, and both end by printing the directory and the cd … && base44 code line to pick the app up again. The block also says the app's files live remotely (base44 sandbox ls to look, base44 eject for a copy) — a linked directory is a handle, like base44 link, not a checkout.

Each command is a Base44Command, so it gets the existing plumbing for free: --app-id <id> / linked-dir resolution, --branch, and the global --json.

Only builder-managed apps. builder send (and code) first read the app and refuse two kinds with a one-line message: a code-first project (base44 create, is_managed_source_code: false, where you own the source and a turn would edit a copy nobody sees) and a Superagent (user_agent, which has no builder conversation).

builder new — create an app and start its first build. A prompt makes a template app; --import <repo> builds over an existing GitHub repo.

$ base44 builder new "invoice tracker for freelancers" --json
{"id":"a1b2c3","repo_url":null,"editor_url":"https://app.base44.com/apps/a1b2c3/editor/preview",
 "preview_url":"https://preview-a1b2c3.base44.app","status":"ready"}

$ base44 builder new --import https://github.com/me/my-store --mode direct --json
{"id":"d4e5f6","repo_url":"https://github.com/me/my-store", … ,"status":"created"}

Without --json it shows a live spinner while the sandbox provisions, then streams the agent's tool calls, prints repo / editor / linked ./<dir>, then the preview URL. Flags: --import <repo>, --mode direct|fork|copy, --name, --repo-name, --from-branch. Inputs are validated before any API call.

builder send — one agent turn; blocks until it finishes and streams it.

$ base44 builder send "add a dark-mode toggle to the header"
  ↳ read_file   src/components/Header.jsx
  ↳ write_file  src/components/Header.jsx
✓ Turn finished.

$ base44 builder send "" --json
{"status":"ready","error_source":null,"reply":"Added a dark-mode toggle…"}
# a busy agent → {"queued":true}

builder status · sandbox preview · builder stop

$ base44 builder status --json     {"id":"a1b2c3","state":"ready","message":null}
$ base44 sandbox preview --json    {"preview_url":"https://preview-a1b2c3.base44.app"}   # boots the sandbox if cold
$ base44 builder stop --json       {"stopped":true}                                       # server-side stop

builder model — pick the builder model for your turns, account-wide. No argument lists the catalog and the current pick; default clears it.

$ base44 builder model --json
{"current":"claude_opus_5","models":[{"name":"default","id":null},{"name":"Opus 5","id":"claude_opus_5"}, …]}
$ base44 builder model sonnet
Builder model set to Sonnet 5 for every new turn.

Branch-aware by design

A request with no branch targets main — right for a template app. An app built over a repo works on its setup branch, so send/status/stop resolve it and stamp it, or they'd silently hit the wrong line:

sequenceDiagram
    participant CLI as base44 builder send
    participant BE as backend (existing endpoints)
    CLI->>BE: GET /api/apps/{id}/branches
    BE-->>CLI: [setup branch]  — or []  for a template app
    CLI->>BE: POST chat/message?branch_id=…   (none → main)
    BE-->>CLI: turn outcome
Loading

Interactive — base44 code

A full-screen Ink session, composed from the same helper the atoms use — there is one create path, not two. The header mark is rendered, not hand-drawn: a supersampled, aspect-corrected disc in sub-cell block glyphs with a thin slot, octant glyphs on terminals that draw them and quadrant blocks elsewhere (code/logo.ts, pinned by golden tests).

base44 code   (empty dir → first prompt creates the app)
  createAndLinkApp          ← the exact helper `builder new` calls
    createApp | createImportedApp
    writeAppConfig · setAppContext
  resolveActiveBranchId
  runGenesisSession → engine → sendTurn / getFullConversation (apps/api)
$ base44 code
  ● Web app — Base44 template + builder agent
  › a habit tracker with weekly streaks
  ✻ building…                       (live: entities, pages, functions stream in)
  ✓ ready · editor ↗ · preview ↗      (input stays open for the next turn)

$ base44 code --import https://github.com/me/my-store
  ● Repository — github.com/me/my-store

In a linked directory it reopens that app's session; base44 code --app-id <id> opens any app from anywhere, and base44 link attaches a directory to an existing app (the help text and the idle hint name both). The footer chip names the kind of app from its state — web app, game, mobile app, slides — or shows the repository for an app built over one; "import" is never used as a label. In-session: Enter sends, Esc stops the running turn (server-side), arrows/PgUp/PgDn scroll, Ctrl+C/D exit. The mode shows in the header line and a footer chip. /model opens an arrow-navigable model picker (/model <name> switches directly); the current pick shows in the footer.

Dependencies added — flagged for review

For the Ink session: ink, ink-text-input, react (+ @types/react); "jsx": "react-jsx" in tsconfig.json; a small bun plugin in infra/build.ts stubbing Ink's dev-only react-devtools import so the bundle builds; knip.json scans .tsx. bun.lock updated.

Lockfile — flagged for review. bun.lock gains only the entries for these four packages and their tree, recorded in registry-default form ("" URL) like every existing entry; bun also re-nests the older string-width/wrap-ansi family under cliui, yargs, @inquirer/core and msw. Versions and sha512 integrity are unchanged.

Build. infra/bundle.ts owns the runtime externals list and a bundler plugin that stubs Ink's dev-only react-devtools-core import. Standalone binaries now compile through Bun.build({ compile }) instead of spawning bun build --compile, so the same stub applies to them (marking the package external is not enough: the binary hoists the import and exits at startup).

Tests

  • tests/cli/builder.spec.ts (16): template create → settled turn → preview; create over a repository; empty-directory and --path linking; input validation; send branch-scoping + queued; send refuses a code-first project and a Superagent; status, sandbox preview, stop; model list, set, and default; code refuses without a TTY.
  • tests/core/stream.spec.ts (18): the poll-and-diff stream.
  • Full suite green: 83 files / 875 tests. Typecheck + biome clean; bun build OK.

Caveat — the agent's preview-verification tools need the editor open

Not specific to either flow. The agent's preview_screenshot / preview_execute_code are tab-bound: the backend sends a command into the app's socket room and waits for the preview iframe's builder-bridge, and the relay connecting them runs in the web editor tab. Driving the agent from the terminal with no editor open → those tools return iframe_unavailable, for both template and repo apps.

Template app App over a repo
Lens tools need the editor open yes yes
Bridge injection reliable (platform-served preview) fragile (external preview-proxy)
Headless fallback legacy server-side screenshot exists (flag-gated) none

The agent still builds fine from the terminal; only its "look at the running app" step depends on the editor. Building over a repo additionally needs the IMPORTED_APPS feature flag on the user, and model picking needs PER_USER_BUILDER_MODEL_SELECTION — both existing gates.


Next steps — not part of this PR

Listed so the line is explicit; none of this ships here.

T2 — CLI

  • --clone for apps built over a repository: clone into the linked directory on the setup branch, so git is the pull/push pair. Then builder commit / builder pr (refused on template apps).
  • Blank mode: a repository app from the starter with no repository of your own (needs the apper items below).
  • agents chat <name> "<msg>" and agents conversations under the existing agents group — the runtime endpoint answers synchronously and accepts the platform token. A /agent slash in code decided after.
  • superagent chat | conversations — same shape, separate product; needs its owner and a confirmation story for side effects.
  • Attach and resume: builder open <id> or a picker in code reusing link's app list; show recent history on attach; handle several active branches instead of falling back to main.
  • Agent-friendly output, claude -p style: when stdout is not a TTY, the reply goes to stdout and progress to stderr (today settled tool lines go to stdout and the reply is not printed); --stream-json for NDJSON events with a final envelope; exit codes reflecting the final status and --timeout on builder new and builder send.
  • Rollout gate: check a server feature flag from /api/auth/me (feature_flags) at the start of builder new, builder send and code, failing with one line that names it — the same list the web client uses; one new FeatureFlag on apper.
  • Session input editing: an in-house line editor for Alt+←/→, Alt+Backspace/Ctrl+W, Ctrl+A/E, Ctrl+U/K, replacing ink-text-input (TODO in session.tsx).
  • Creation flags for games and mobile apps (the atoms already work on every builder-driven type); model-list parity (Base 1 behind its flag, NEW badges).

T3 — CLI

  • Live source sync for template apps over the sandbox file commands, with checkpoints — the prerequisite for any create --prompt that brings files onto disk.

Apper

  • Chat refuses user-managed apps (is_managed_source_code: false), or the editor hides it for them — the CLI guards its side only.
  • A typed github_reauth_required error carrying the reconnect URL (replacing the CLI's string match).
  • Preview-proxy: inject the bridge from each app's owning backend instead of one hardcoded BACKEND_URL.
  • Preview verification without the editor open.
  • A canonical starter repo + durable GitHub auth on preview envs — unblocks blank mode.

🤖 Generated with Claude Code

Build Base44 apps from the terminal — the minimum shippable slice.

Non-interactive atoms under `base44 app`: `new` (a prompt → template app, or
--import <repo> over an existing GitHub repo), `send`, `status`, `preview`,
`stop`. Each is a Base44Command reusing prod plumbing (--app/linked-dir
resolution, --branch, global --json) and hits existing backend endpoints — no
apper changes. Atoms are branch-aware: an imported app works on its setup
branch and a request with no branch targets main, so the active branch is
resolved and stamped on send/status/stop. `app new` shows a live spinner
while the sandbox provisions (silent under --json).

Interactive `base44 code`: a full-screen Ink session, isolated under
commands/code and composed from the same createAndLinkApp helper the atoms
use. In an empty dir the first prompt creates the app (--import <repo> for a
repo); in a linked dir it reopens the app. Enter sends, Esc stops the turn
server-side, arrows scroll. No slash commands.

Shared layer core/resources/apps: create (builder | import), send, status,
preview, stop, branch resolution, GitHub re-auth link, and the
poll-and-diff conversation stream.

Deps for the Ink session: ink, ink-text-input, react (+ @types/react),
jsx react-jsx, and a bun plugin stubbing Ink's dev-only react-devtools
import. bun.lock updated.

Tests: tests/cli/app.spec.ts covers the atoms; tests/core/stream.spec.ts the
stream. Full suite green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ayal and others added 2 commits September 17, 2026 13:27
Account-wide model selection on the existing backend contract: `GET /api/auth/me`
reads the saved pick, `POST /api/auth/{id}/update-user` writes it, and the
`X-Builder-Model-Selection: user-v1` request header makes the backend honor it
(the web editor's contract — safe with no pick, it falls back to the app
default). Gated server-side by PER_USER_BUILDER_MODEL_SELECTION; a 400 on save
surfaces as a clear "this account can't pick a model" error.

`base44 app model` lists the catalog with the current pick (--json returns
{current, models}); `app model <name>` resolves a name/id loosely and saves it;
`default` clears it. In the interactive session, `/model` opens an
arrow-navigable picker (Enter selects, Esc cancels), `/model <name>` switches
directly, and the current pick shows in the footer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A local install on the Wix VPN rewrote every bun.lock tarball URL to an
explicit https://npm.dev.wixpress.com/... address. CI resolves only the
default registry (registry.npmjs.org, pinned to the embargo gateway), so
the newly added Ink/React entries failed with DNSResolveFailed; the
pre-existing ones only installed from the restored bun cache.

Restore main's shape: "" (registry default) on every entry, main's exact
@deno/loader line, and only the additions bun made for ink, ink-text-input,
react and @types/react. Versions and sha512 integrity are unchanged; a
frozen-lockfile install from a clean checkout succeeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.15-pr.630.30a3a78

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.1.15-pr.630.30a3a78"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.15-pr.630.30a3a78"
  }
}

Preview published to npm registry — try new features instantly!

ayal and others added 2 commits September 17, 2026 13:48
…pplies

Ink imports react-devtools-core, an optional peer we do not ship, from a
module that only loads under DEV=true. The dist build already replaces it
with an inert stub via a bundler plugin, but the standalone binaries were
compiled by spawning the bun CLI, which has no plugin hook, and failed to
resolve the package. Marking it external is not an option: the compiled
binary hoists the import and exits at startup with "Cannot find package".

Move the externals list and the stub into infra/bundle.ts and compile the
binaries with Bun.build({ compile }) using both, keeping the Windows-host
icon rule. Verified: the darwin-arm64 binary builds and runs --version and
app --help.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
knip flagged ten symbols exported from the new app/code modules that are
only used inside their own files. Keep them module-private.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ayal ayal changed the title feat(cli): base44 app — non-interactive agent build atoms + isolated base44 code session (T1) feat(cli): base44 app — non-interactive agent build atoms + base44 code interactive session (T1) Sep 17, 2026
@ayal ayal changed the title feat(cli): base44 app — non-interactive agent build atoms + base44 code interactive session (T1) feat(cli): build apps with the agent from the terminal — base44 app + base44 code Sep 17, 2026
Both commands create ./<app-name>/ under the current directory, and the
only hint was a "Next: cd" suffix on the outro (app new) or nothing at all
(code). Print one shared block at the end of both: the directory, and the
cd + base44 code / base44 app send lines to pick the app up again. The
genesis session also keeps "dir ./<name>" in its footer for the whole run,
a linked-directory session prints its project root on exit, and app new
--json gains dir and path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ayal and others added 5 commits September 17, 2026 14:14
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first entry is "Automatic — matched with the best model", as the web
picker names it, with `default` and `auto` accepted as typed aliases;
Gemini 3.8 Flash reads "fast responses for everyday tasks" instead of an
invented "fast". Fable 5 already matched ("uses more credits").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Align with `base44 create`: when the current directory is empty it is the
project — link it and name the app after the folder; otherwise create
./<name> as before. `--path <dir>` on both commands picks the directory
explicitly (and names the app after it unless --name is given). Every
label, the next-steps block, the session footer and the --json `dir`
("." when linked here) follow the same rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ayal
ayal requested a review from netanelgilad September 17, 2026 11:42
ayal and others added 2 commits September 17, 2026 16:50
- `base44 app` becomes `base44 builder` (new, send, status, stop, model):
  the group names the counterpart you talk to, next to `sandbox` and the
  prod `agents` group.
- `preview` moves to `sandbox preview`; it reads the sandbox preview URL.
- `code` and `builder send` refuse a code-first project (base44 create,
  is_managed_source_code=false) and a Superagent, naming the app kind and
  what to run instead.
- `code --app-id <id>` opens that app from anywhere; help and the idle hint
  name --app-id and `base44 link` as the ways to attach to an existing app.
- Session chips name the app kind from its state (web app, game, mobile
  app, slides) or show the repository for apps built over one; "Import" is
  no longer used as a label.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Port of `circle.py -d 6 --gap 3.5r --gap-height=0.8r`: a supersampled disc
in sub-cell block glyphs, aspect-corrected, with a thin slot centred on row
4. Octant-capable terminals (ghostty, kitty, wezterm, foot, contour) get the
2x4 tier with circle.py's fitted table; everything else gets quadrant blocks.
Fully covered cells are painted as background so fonts whose blocks stop
short of the line height don't stripe the fill. Golden tests pin both tiers
to the script's exact output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The input box and model picker were capped at 100 columns while the
transcript already wrapped to the full width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant