Skip to content

feat(cli): install and serve a site through the CLI - #635

Open
ronnyrin wants to merge 16 commits into
mainfrom
feat/site-install-and-serve
Open

ronnyrin wants to merge 16 commits into
mainfrom
feat/site-install-and-serve

Conversation

@ronnyrin

@ronnyrin ronnyrin commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Note

Description

Adds the two commands a hosted build sandbox needs to bring a project up without a developer at the keyboard: base44 install (run the site's installCommand and nothing else) and base44 site dev (run the site's dev server with no local backend, the frontend reaching its backend same-origin). base44 build is also relaxed to requireAuth: false, since it resolves the app id locally and runs a local build. All three are local-only paths, so a machine that has never logged in can install, build and serve a checkout.

site dev takes no arguments at all: the bind address and the dev server's spelling of its bind-address flag come from new site.devHost / site.devPort / site.devHostFlag config keys, which default to 0.0.0.0, 5173 and --host. A serveCommand that cannot receive forwarded arguments is refused rather than silently served on a different address.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • New base44 install (src/cli/commands/project/install.ts): runs the site block's installCommand via runTask, registered in program.ts. requireAuth: false and requireAppContext: false — no app to resolve, no API call. Fails with an actionable hint when the project has no site block.
  • New base44 site dev (src/cli/commands/site/dev.ts): reads the project config, falls back to npm run dev when the site block names no serveCommand, appends the configured bind address, and runs it through the existing ServeCommandRunner. requireAuth: false; exits with the child's code and tears down on SIGINT/SIGTERM.
  • New withServeAddress() core helper (src/core/site/serve-command.ts): appends <hostFlag> <host> --port <port> to npm run <script> (through --) and to pnpm/yarn/bun run scripts (directly), plus npm --prefix <path> run <script>. Script and prefix tokens are matched as narrow charsets rather than \S+, so shapes like npm run dev;evil or npm run $(id) are not treated as forwarding shapes. Returns droppedAddress alongside the command so "no address to append" cannot be confused with "the address went nowhere"; site dev turns a dropped address into a hard failure instead of a warning plus exit 0.
  • Config schema (src/core/project/schema.ts): site.devHostFlag (default --host; --hostname for Next, which exits on --host), site.devHost (default 0.0.0.0) and site.devPort (default 5173, validated 1–65535). serveCommand and outputDirectory still deliberately do not default — their absence is the signal base44 dev and deploy read.
  • base44 build no longer requires auth (src/cli/commands/project/build.ts): requireAuth: false, since the app id comes from --app-id, BASE44_APP_ID or .app.jsonc and the build is local.
  • appBaseUrl is now optional on ServeCommandRunnerOptions: when omitted, VITE_BASE44_APP_BASE_URL is not injected, so a same-origin frontend (the Base44 vite plugin proxies /api) is not pointed across origins. base44 dev still passes it.
  • Extracted stopRunnerOnProcessSignals() out of commands/dev.ts into src/cli/dev/stop-runner-on-signals.ts so both dev and site dev share it.
  • CHANGELOG updated with entries for all of the above.

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

New/updated test coverage:

  • tests/core/serve-command.spec.ts — cases over withServeAddress: npm via --, pnpm/yarn/bun direct, custom host flag, --prefix scripts, trimming, the fallback command, plus table-driven cases for non-forwarding shapes (vite, next dev, chained commands, already-pinned ports) and for injection-shaped strings.
  • tests/cli/site_dev.spec.ts — default sandbox address, config-supplied address, rejection of --port, refusal of a non-forwarding serveCommand, serving without a login, the --hostname spelling, same-origin env (URL=undefined), and the no-site-block failure. Driven by runLive against serve.js fixtures that echo their argv and env.
  • tests/cli/install.spec.ts — configured installCommand runs, works without a login, surfaces a failing command, fails without a site block.
  • tests/cli/build.spec.ts — builds without a logged-in user.
  • tests/core/project.spec.ts — the three new schema defaults.
  • New fixtures: with-dev-address, with-hostname-serve-command, with-npm-serve-command, with-installable-site, with-failing-install.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

Division of labour between the three dev paths: base44 dev is the developer machine (local backend), base44 dev --remote is the developer machine against the published backend, and base44 site dev is the hosted-sandbox path (frontend only, same-origin backend). base44 dev is unaffected by the new config keys — it binds nothing.

The commit history walks through several designs for where the bind address lives (command flags, then --backend-url, then config); the final shape is config-only, so base44 site dev needs no arguments and no caller has to know the values.

docs/ topic guides were not touched — the new commands follow the existing factory / Base44Command pattern rather than changing it — but the new site.dev* config keys may be worth a mention in the project-structure docs.


🤖 Generated by Claude | 2026-09-23 13:04 UTC | c3a4ce4

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.18-pr.635.c3a4ce4

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.18-pr.635.c3a4ce4"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.18-pr.635.c3a4ce4"
  }
}

Preview published to npm registry — try new features instantly!

A `site` block had to spell out every command, and the platform that runs
these projects kept its own copy of the same values to fill the gaps.
Defaults live here now, matching what `base44 create` scaffolds: install
`npm install`, build `npm run build`, output `./dist`. A block only names
what its project does differently.

`serveCommand` is deliberately left undefaulted: `base44 dev` reads its
absence as "no frontend to run here" and runs the backend alone, and a
default would spawn a dev server for every site block — one that fails
immediately takes the backend down with it. Commands that exist only to
serve a frontend default it themselves.

`site` itself stays optional, so "is there a site?" still keys on the
block: a backend-only project omits it and has no site. Inside a block the
build fields always resolve, which moves two refusals from "this field is
missing" to "there is no site block" — `base44 build` and `site deploy`
reword their hints, and `deploy` reads the block rather than one field.

Projects that declared a partial block change behaviour: they now build
and deploy on the defaults instead of being refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
@ronnyrin
ronnyrin force-pushed the feat/site-config-defaults branch from 6ab4533 to 93d2ceb Compare September 22, 2026 09:34
@ronnyrin
ronnyrin force-pushed the feat/site-install-and-serve branch 2 times, most recently from 623cd2c to 075f1f7 Compare September 22, 2026 09:41
`installCommand` and `serveCommand` were fields no command would run on
their own: install only happened inside `create`, `scaffold` and `eject`,
and serve only inside `base44 dev`, which also boots a local backend on
4400 and points the frontend at it. A host that already has a backend and
just needs this project installed and served had to compose npm itself.

`base44 install` runs `installCommand` and nothing else — local only, so a
machine that has never logged in can use it. `base44 site dev` runs
`serveCommand` against a backend the caller names, and since serving is
its whole job it falls back to `npm run dev` where the schema deliberately
does not. `--host`/`--port` are appended through the project's new
`devHostFlag` (`--host`, or Next's `--hostname`). Only an `npm run`
invocation can take them — `--` is what forwards arguments, and a bare
`vite` would read it as its own — so anything else passes through
untouched and says so rather than silently serving an unreachable address.

`base44 dev` and `dev --remote` are unchanged; the signal handling they
had is now shared rather than copied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
@ronnyrin
ronnyrin force-pushed the feat/site-install-and-serve branch from 075f1f7 to de2f19d Compare September 22, 2026 09:43
ronnyrin and others added 3 commits September 22, 2026 13:10
Review found that defaulting `outputDirectory` turns a field whose absence
means "nothing built to upload" into one that always resolves, and two callers
act on that:

`deployAll` still keyed the upload on the field, so `base44 deploy` on a
`"site": {"serveCommand": ...}` project (the shape of this repo's own
with-serve-command fixture) would resolve `./dist` and either fail a
previously-green deploy after pushing every other resource, or publish whatever
happened to sit in `./dist` — a backend bundle, typically — as the app's site.

`eject`'s guard `installCommand && buildCommand` became dead, so `eject --yes`
would run a network install, a build and a deploy for any ejected app with a
site block, unprompted.

So `outputDirectory` joins `serveCommand` as deliberately undefaulted, which is
the same rule applied consistently: only the two commands whose absence means
nothing default. `hasResourcesToDeploy` needs no change and drops out of the
diff; eject now keys on `outputDirectory` like every other caller. Added the
deploy test that would have caught the first one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
…feat/site-install-and-serve

# Conflicts:
#	packages/cli/src/core/project/schema.ts
Review found that defaulting `outputDirectory` turns a field whose absence
means "nothing built to upload" into one that always resolves, and two callers
act on that:

`deployAll` still keyed the upload on the field, so `base44 deploy` on a
`"site": {"serveCommand": ...}` project (the shape of this repo's own
with-serve-command fixture) would resolve `./dist` and either fail a
previously-green deploy after pushing every other resource, or publish whatever
happened to sit in `./dist` — a backend bundle, typically — as the app's site.

`eject`'s guard `installCommand && buildCommand` became dead, so `eject --yes`
would run a network install, a build and a deploy for any ejected app with a
site block, unprompted.

So `outputDirectory` joins `serveCommand` as deliberately undefaulted, which is
the same rule applied consistently: only the two commands whose absence means
nothing default. `hasResourcesToDeploy` needs no change and drops out of the
diff; eject now keys on `outputDirectory` like every other caller. Added the
deploy test that would have caught the first one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
@ronnyrin
ronnyrin force-pushed the feat/site-config-defaults branch from 8d9279d to 9c57bb8 Compare September 22, 2026 10:13
ronnyrin and others added 4 commits September 22, 2026 13:27
Review findings on this PR, plus the merge of #634's outputDirectory change.

`--host`/`--port` were composed only onto an `npm run` invocation and otherwise
warned and continued with exit 0. For this command's audience — a hosted sandbox
— a warning in a stream nobody reads is not a signal: the dev server comes up on
whatever address it likes and the CLI reports success. An address that cannot be
delivered is now a refusal. `pnpm`, `yarn` and `bun run` join the forwarding
shapes, since all three pass trailing arguments to a script and dropping the
address for them was the common case.

`withServeAddress` returns whether it dropped the address, so the caller cannot
mistake "no address to append" for "the address went nowhere" — the difference
being a preview that never loads. That removes the second evaluation of the same
regex at the call site, and the predicate it needed.

The script token was `\S+`, which matches `dev;evil` and `$(id)`: the predicate
answered "this forwards arguments" for a line whose second command is what
received them. It is a charset now. The composition also used the untrimmed
string while the test used the trimmed one.

`--port` went through `Number()`, so an empty string became port 0 — a random
port, silently — and `0x10`, `1e3` and `5173.0` were all accepted. It is parsed
in a Commander argParser now, digits only and in range.

`devHostFlag` does not default in the schema, for the same reason `serveCommand`
and `outputDirectory` do not: `site dev` is the only reader, and a default
permanently erases the difference between a project that wrote `--host` and one
that wrote nothing. `site dev` supplies the fallback.

Also: knip was failing on an unused exported type, invisible because none of the
five code-checking workflows run on a PR that targets another branch. Tests
added for the unauthenticated path on both new commands, `--port` rejection, a
failing install command and a custom `devHostFlag`; one assertion that could
never fail is gone, and the fixture no longer claims the schema defaults
`serveCommand`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
…feat/site-install-and-serve

# Conflicts:
#	packages/cli/src/cli/commands/project/eject.ts
#	packages/cli/src/core/project/schema.ts
#	packages/cli/tests/core/project.spec.ts
Both commands default inside a `site` block, so install and build always work
there now — the earlier `outputDirectory` check was guarding a config neither
writer produces. `ensure_cli_configs` and `eject_service` both emit the full
block including `outputDirectory: "./dist"`, and the eject ZIP comes from the
latter, so the check could never be the condition that differed.

Having a site block is the whole question: a backend-only project has nothing
to build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
Base automatically changed from feat/site-config-defaults to main September 22, 2026 13:50
ronnyrin and others added 7 commits September 22, 2026 16:53
…-serve

# Conflicts:
#	packages/cli/src/core/project/schema.ts
#	packages/cli/tests/cli/build.spec.ts
#	packages/cli/tests/core/project.spec.ts
`site dev` needed `--host 0.0.0.0 --port 5173` from every caller, which meant
the platform retyped its own constants into a command line — the duplication
this command exists to remove. Both now default in the CLI, so a hosted sandbox
runs `base44 site dev` with no arguments and no knowledge of the values.

`0.0.0.0` is the default because the command exists for a sandbox, whose preview
is reached from outside the container; a loopback default would make it
unreachable. On a developer's own machine that also exposes the dev server to
the LAN, which is what `site.devHost` is for. `base44 dev` is untouched — it
binds nothing.

`site.devHost` and `site.devPort` are optional, and resolution is flag, then
config, then default, so a project can narrow the bind without losing the
ability to override it per run. Neither defaults in the schema, for the same
reason `serveCommand` and `outputDirectory` do not: the single command that
reads them supplies the fallback, so a project that said nothing stays
distinguishable from one that chose these values. `devPort` is range-checked at
parse time, which the flag's own parser could not do for a config value.

One behaviour change: a `serveCommand` that takes no forwarded arguments, a bare
`vite`, is now refused on every `site dev` rather than only when an address was
asked for. There is always an address now, and a sandbox that silently serves on
a different port is the failure this refusal exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
`devHost`, `devPort` and `devHostFlag` were defaulted in `site dev`, which split
the site block across two files: two fields defaulting in the schema and three
in a command, with nothing telling a reader which was which.

They belong in the schema, because the rule that keeps `serveCommand` and
`outputDirectory` out of it does not apply to them. That rule is about absence
carrying meaning — `base44 dev` reads a missing `serveCommand` as "no frontend
to run here", and `deploy` reads a missing `outputDirectory` as "nothing built
to upload", so defaulting either erases a question a reader asks. Nothing asks
that of these three. `site dev` is their only reader and it wants a value.

So the block is now self-describing: every convention with a default has it in
one place, and the two fields without one are the two that mean something by
being absent. `withServeAddress` loses its internal host-flag fallback, since
the caller can no longer arrive without one, and the test for that case goes
with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
… config

Two of the three fields should not have existed. Nothing outside `site dev` and
the fixture added alongside them ever read `devHost` or `devPort`; they were
added to answer an objection about run-scoped values landing in a committed
file, and that objection was withdrawn once it turned out the sandbox uses one
fixed address. The fields outlived the argument for them.

An address is not a fact about a project. Nobody writing a repo has a view on
which port one particular run should listen on, so the convention belongs to the
command that has to pick one, with `--host`/`--port` for a caller that needs
something else.

`devHostFlag` stays, and stays in the schema, because it is the opposite case:
only the project knows whether its dev server wants `--host` or `--hostname`,
and the caller that needs it passes no arguments at all, so config is the only
channel. That leaves the site block with one field per question it can actually
answer.

Net 41 lines removed, including a fixture that only existed to exercise the
fields it shipped with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
`--host` and `--port` are gone. The address now comes from `site.devHost` and
`site.devPort`, which default in the schema to `0.0.0.0` and `5173`, so
`base44 site dev` takes no arguments for it and a caller needs to know none of
the values. Changing where it binds means editing base44/config.jsonc.

That removes the last thing the platform was composing. It also removes the
flag's own port parser: Zod range-checks `devPort` at parse time, which the
parser could not do for a config value, so validation now covers both the file
and the one place the value is read.

`withServeAddress` takes host and port as required, since a caller can no longer
arrive without them, which deletes its no-address branch and the test for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
Left over from the last edit to this spec; caught by lint, not by the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
The option pointed the frontend at a named backend via
VITE_BASE44_APP_BASE_URL. Nothing ever passed it: the sandbox runs
`base44 site dev` bare and relies on the frontend reaching its backend
same-origin through the vite plugin's /api proxy, and `base44 dev` and
`dev --remote` set the same variable internally without going through it. It
was the last option on a command meant to take none.

The serve runner keeps its optional `appBaseUrl`, since both `dev` paths still
use it, so bringing the option back for a real caller is a few lines. The two
same-origin and named-backend tests collapse into one, and the CHANGELOG entry
drops both this and the `--host`/`--port` flags removed earlier, which it still
described.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018G9vtxAUJV6aZ7FYv5cRjL
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