Skip to content

feat(projects): add forest projects:create:in-app - #806

Open
Gawtier wants to merge 3 commits into
mainfrom
feat/projects-create-in-app
Open

feat(projects): add forest projects:create:in-app#806
Gawtier wants to merge 3 commits into
mainfrom
feat/projects-create-in-app

Conversation

@Gawtier

@Gawtier Gawtier commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

New command forest projects:create:in-app — register a Forest project for an app the user hosts themselves (Rails, Express, NestJS, …). Unlike create:sql/demo/nosql, it does not scaffold an agent and does not introspect a database: the project is created with architecture: 'in-app' and no agent (exactly like the web UI's in-app onboarding), and the command prints the env/auth secrets + how to mount Forest in the existing app. The user's own running agent pushes its schema on first boot.

  • Makes architecture overridable on AbstractProjectCreateCommand (default stays 'microservice').
  • In-app specifics: requiresDatabase = false, agent = null (so the server keeps architecture: 'in-app'), dump is a no-op.
  • Rails guidance lists the 5 required gems. gem 'forest_admin_rails' alone installs but fails to boot (cannot load such file -- forest_admin_agent) because 1.36.0 doesn't declare its 4 companion gems as runtime deps — so the single-gem instruction would be wrong. (Worth a separate report to the Ruby SDK team.)

Tested

  • Unit: in-app.test.js (mirrors demo.test.js) — asserts the in-app create posts architecture: 'in-app' with no agent, skips all DB steps, scaffolds nothing, and prints the secrets + the 5-gem Rails guidance. createProject fixture gained an architecture param (defaults to 'microservice', backward-compatible).
    • Updated in 0a8d5cd: 6 cases in in-app.test.js now, covering the --format json contract (no prompting, pure-JSON stdout, refusal when logged out, error visibility, no partial document), plus new unit tests for Logger.reserveStdout and option-parser.getDefaultOptions. See the comment below for what that commit changes and why.
  • Live end-to-end on BOTH stacks, against real Forest projects created by this command, verified server-side (not just agent logs):
    • Node — Express + @forestadmin/agent@1.90.3 mounted with the CLI-issued secret → dev env is_active false→true, collections 0 → 8.
    • Rubyrails new (Rails 8.0.5 / Ruby 3.3.9) + the 5 forest gems + rails g forest_admin_rails:install <secret>is_active false→true, collections 0 → 2 (Company, Customer).
    • Test projects were torn down afterwards.

Note: the sql/nosql/create suites require a live Postgres test DB (they fail locally with SequelizeConnectionRefusedError, same as on main); they pass on CI.

Note

Add projects:create:in-app command with JSON mode and non-interactive defaults

  • Adds the projects:create:in-app command to register user-hosted applications with in-app architecture, skipping database and agent scaffolding.
  • Extends AbstractProjectCreateCommand with overridable interactive and architecture fields, retaining existing microservice and interactive defaults for other commands.
  • Updates getCommandLineOptions to accept an optional interactive parameter, resolving missing values from declared defaults without prompting when disabled.
  • Adds reserveStdout to Logger which routes normal log lines to stderr, keeping stdout free for JSON payloads in the new command.
  • Risk: getCommandLineOptions in option-parser.ts changes signature to add interactive; Logger in logger.js adds reserveStdout initialization. Existing callers retain default behavior.

Macroscope summarized 0a8d5cd.

Register a Forest project for an app the user hosts themselves (Rails,
Express, NestJS, …) — no agent scaffold, no database introspection. It
creates the project with architecture 'in-app' and no agent (like the web
UI), then prints the env/auth secrets and how to mount Forest in the
existing app. The running agent pushes its own schema on first boot.

- makes `architecture` overridable on the abstract create command
- in-app: requiresDatabase=false, agent=null, dump is a no-op
- Rails guidance lists the 5 required gems: `forest_admin_rails` alone
  installs but fails to boot (it doesn't declare its 4 companion gems as
  runtime deps), so the single-gem instruction is wrong.

Validated end-to-end on both stacks against a real Forest project: a Node
Express agent and a Rails 8 app each mounted with the CLI-issued secret and
pushed their schema (verified server-side: dev env went active and
collections appeared, 0 before boot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Gawtier
Gawtier force-pushed the feat/projects-create-in-app branch from a7f3e2a to 8b2552f Compare July 27, 2026 06:56
- create:in-app now defaults applicationPort to 3000 (the user's own app)
  instead of 3310 (scaffolded microservice agents), with an adapted prompt
- new --format json flag: stdout carries ONLY a parsable
  {"projectId", "envSecret", "authSecret"} document (logger stdout is muted
  in this mode, spinners already go to stderr) — the contract npx forest-start
  parses; human output is unchanged
- drop the unused OclifConfig import
- tests: assert exact fixture secret values, add negative assertions via a new
  `not` std helper (no 'Testing connection'/'Analyzing': the command must not
  touch a DB), assert the whole JSON stdout, and document the nock exact-body
  match as the guard for agent:null + architecture:in-app

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`--format json` promised that stdout carries nothing but a parsable JSON
document, but did not keep it:

- host/port were always prompted (their defaults are only applied by
  inquirer), so a scripted run got an inquirer question and ANSI escapes on
  stdout, exit 0, and no JSON at all;
- the login check runs before the mute was installed, so an unauthenticated
  run wrote 'Login required.' and a password prompt to stdout;
- the mute dropped stdout lines outright, and the real cause of a failure is
  logged with logger.log() — so in JSON mode errors were silently swallowed.

Fixes, in order:

- Logger.silentStdout becomes Logger.reserveStdout and *diverts* stdout lines
  to stderr instead of dropping them. Nothing is lost: diagnostics stay
  visible to the operator, they just stop polluting the parsable stream.
- option-parser gains getDefaultOptions(), an unattended counterpart to
  getInteractiveOptions() using each option's declared default. Selected via
  getCommandLineOptions(instance, { interactive }) — default true, so every
  other command is untouched.
- AbstractProjectCreateCommand exposes an `interactive` getter as the
  extension point; in-app returns false in JSON mode.
- in-app parses --format in run(), before the login check, and refuses with
  exit 10 rather than hanging on a password prompt.
- in-app refuses to emit a partial document: JSON.stringify drops undefined
  keys, so a missing secret would otherwise look like success with exit 0.

Verified on the built binary: the previously failing invocation now writes
nothing to stdout and surfaces the real cause on stderr, and the nominal run
emits a JSON.parse-able document with the declared port default applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PMerlet

PMerlet commented Sep 14, 2026

Copy link
Copy Markdown
Member

Pushed 0a8d5cd on top of 3309e8b (fast-forward, nothing of yours discarded). Context below, since it touches files beyond the new command.

Why

--format json documents a contract — "stdout carries only a machine-readable document" — that it did not keep. Three ways, all reproduced on the built binary:

  1. No JSON at all in the normal scripted case. applicationHost/applicationPort have prompters and their defaults are only applied by inquirer, so they were always prompted. forest projects:create:in-app x --format json < /dev/null wrote an inquirer question plus ANSI escapes to stdout and exited 0. The tests missed it because inquirer is mocked.
  2. Login leaked to stdout. checkAuthentication() runs before getCommandOptions(), so the mute was installed too late: an unauthenticated run wrote Login required. and a password prompt to stdout.
  3. Errors silently swallowed. The real cause is logged with logger.log(), which the mute dropped. Text mode printed Error: connect ECONNREFUSED …; JSON mode printed nothing about the cause.

What changed

  • Logger.silentStdoutLogger.reserveStdout, which diverts stdout lines to stderr instead of dropping them. Nothing is lost — diagnostics stay visible and stop polluting the parsable stream. This is what fixes (3).
  • option-parser gains getDefaultOptions(), an unattended counterpart to getInteractiveOptions() using each option's declared default, selected via getCommandLineOptions(instance, { interactive }).
  • AbstractProjectCreateCommand exposes an interactive getter as the extension point; in-app returns false in JSON mode.
  • in-app parses --format in run(), before the login check, and refuses with exit 10 rather than hanging on a password prompt.

Two things worth your attention

Shared files. Logger, option-parser, AbstractProjectCreateCommand and the test harness are touched. The changes are additive and default to current behaviour (interactive defaults to true, reserveStdout to false), and getCommandLineOptions has no other caller — but it goes beyond in-app, so it deserves a look.

One out-of-scope addition. While exercising the fix I found that a missing env secret surfaced as success: JSON.stringify drops undefined keys, so JSON mode emitted {"projectId":…} with exit 0, and text mode printed FOREST_ENV_SECRET=undefined. generateProject() now refuses both. Isolated and tested — easy to drop if you want the PR minimal. Note the base class still prefixes Cannot generate your project. when that guard fires even though the project was created; the next line corrects it. Fixing that message would touch every create:*, so I left it alone.

Verified

CI 6/6. Full suite locally: 114 failures / 1131 against 114 / 1119 on 3309e8b — same pre-existing Postgres failures, +12 tests. Each fix was mutation-tested (reverted individually) to confirm the new tests actually fail without it.

On the built binary, against a local stub API:

scenario before after
json, no flags, stdin closed exit 0, prompt + ANSI on stdout exit 0, pure JSON, :3000 default applied
json, server unreachable exit 0, cause hidden exit 1, stdout empty, ECONNREFUSED on stderr
json, logged out prompt on stdout exit 10, stdout empty
json, update available oclif warning goes to stderr, stdout stays parsable

Rails gems

Left exactly as you wrote them. I re-verified your finding and it still holds on 1.43.0 (published 4 days ago, 7 minors after the 1.36.0 you tested): lib/forest_admin_rails.rb:6 requires forest_admin_agent, and the gemspec declares 11 runtime deps, none of them a forest_* gem. Filed as PRD-1223 so the CLI workaround can be removed once the gemspec is fixed.

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.

2 participants