The CLI

@fougere/cli creates a workspace, hosts a Frond in a separate process, and invokes an operation from a shell. The CLI itself uses entities and handlers, so its options are validated from their schemas.

Alpha. On npm under the alpha tag — npx fougere@alpha runs it without installing, npm create fougere enters it at new. The commands and flags below are what ships.

fougere new — compose a workspace

fougere new shop --frond blog --app nuxt

The command creates Fronds first, followed by the apps that consume them. It produces a pnpm workspace:

shop/
  fougere.config.ts          ← db, auth, topology
  pnpm-workspace.yaml        ← packages: [fronds/*, apps/*]
  fronds/blog/               ← the domain
    entities/Post.ts
    handlers/PostHandler.ts
  apps/nuxt/                 ← the consumer
    nuxt.config.ts
    app/pages/index.vue
FlagWhat it does
--frond blog,api:catalogFronds to add — template:name renames; the template name is the default name
--app nuxt:websame, for apps
--barethe empty shell, nothing composed
--flatone domain: no fronds/, no workspace — the app root is the Frond
--locallink @fougere/* to a local Fougere checkout (development)
--forceoverwrite an existing directory

Arguments or interactive mode. With --frond and --app, the command asks no questions and can run in a script or CI job. Without these options, it prompts for templates in a TTY. Interactive mode is not available without a TTY.

Flat structure. fougere new shop --flat --frond blog writes one Nuxt app whose root carries the domain — no fronds/ segment, no pnpm-workspace.yaml, one pnpm install (the flat shape). --app is refused with it: the app is the root. A second domain later goes to fronds/billing/ and the root Frond stays where it is. The generated project also carries a tsconfig.frond.json whose include names the convention directories. pnpm typecheck can therefore check the domain without loading Nuxt.

The generated Frond contains an entity, two derived views (NewPost for input and PostCard for list output), and a business operation in addition to CRUD:

export class NewPost extends Post.pick('title', 'body') {}

export default class PostHandler extends Crud(Post) {
  async create(input: NewPost): Promise<Post> {}

  /** The draft→published transition — an operation, not a field write. */
  async publish(id: string): Promise<Post> {}
}

The views define the input and output contracts for these operations.

fougere serve — one Frond, its own process

fougere serve blog --port 4100

Starts that Frond alone and exposes it over JSON-RPC on POST /_fougere/call. The consuming app sets its address in remotes: to route calls to that host. See the gradient.

It does not follow remotes: itself: a host is the Frond, it does not route back out.

serve() binds 127.0.0.1 unless hosts says otherwise: a receiver reads identity off the wire, so the default keeps it on the machine and widening it is a decision written down.

fougere call — invoke an operation

fougere call post.list
fougere call post.create --title "Hello" --body ""

The command uses the same envelope, validation, and typed errors as other clients. A VALIDATION_FAILED error therefore keeps the format received by a page.

fougere sync — consume a remote Frond

fougere sync blog --from http://blog-service:4100

Asks the host rpc.discover, and writes local entity files rebuilt from the cards it answers with:

// .fougere/remotes/blog/entities/Post.ts — generated
import { reconstruct } from '@fougere/schema';

export class Post extends reconstruct<{
  id: string;
  title: string;
  createdAt: Date;
}>({ /* the card, verbatim */ }) {}

One class, like the one you would have written by hand: Post is the value — a judge that validates locally, without an additional request to the host — and the type of a row it hands back, so post.titel does not compile. Both are read off the same card.

Next to it, handlers/PostHandler.ts states what the frond serves, so a consumer can write Facade<PostHandler> without holding the handler's code:

export interface PostHandler {
  list(invocation?: Invocation): Promise<Post[] & { total?: number; hasMore?: boolean }>;
  findById(invocation?: Invocation): Promise<Post | undefined>;
}

The command also writes .fougere/remotes.json, which the Nuxt module reads to alias @frond/blog. Pages therefore use the same import path for a local or remote entity.

Re-running it removes what the host no longer serves. The barrel loses the export by itself, but the file used to stay — and exports lists './entities/*' as a wildcard, so @frond/blog/entities/Ticket.js kept resolving to a class that validates perfectly and that nothing behind it answers for. Only files carrying the generated header are removed; anything you put in that folder yourself is left alone.

The host returns entities associated with a façade, and the facts its fronds announce. An entity that is neither is not in the discovery result — a shape nobody exposed and nobody declared to leave stays home.

A fact has no operation, so it gets a class and no Handler interface beside it:

// .fougere/remotes/blog/entities/PostPublished.ts — generated
export class PostPublished extends reconstruct<{ id: string; title: string; at: Date }>(…) {}

To have an arriving fact judged, re-export that class into one of your own fronds — the boot validates against an entity of a scanned frond, and .fougere/remotes/ is a package, not a frond:

// fronds/search/entities/PostPublished.ts
export { default } from '../../../.fougere/remotes/blog/entities/PostPublished.js';

The rules

codewhat it means
directory-unreadable, handler-parse-failed, heritage-unresolvedwhat the scan could not do — a rule about an absence is only sound if the analysis attests it looked
operation-unboundan operation declares parameters and has no binding plan: it is served, and it receives none of them
cross-frond-importa relative import that resolves into another frond

The last one is a warning, not a refusal — it resolves today and the app runs. What it states is that a colocation constraint is holding the app together and nothing declares it: '../../user/entities/User.js' says these two folders are neighbours, in a string the scan, the identity card and remotes: all ignore. It keeps working right up to the day that folder is not there, and then it fails as a file path rather than as a model.

'@frond/user/entities/User.js' says the same dependency in terms the model reads, and it is the form fougere sync writes — so it survives the frond moving out. The name resolves the same way whether the frond is local or synced.

fougere graph — read the model

fougere graph

Prints entities, their references, and the number of incoming references. Beyond six entities, the command also suggests clusters calculated from that graph:

  Post → Author, Category (2 incoming)
  Author

The shortest path page covers current and possible future uses of this graph.

fougere build-frond — publish a Frond's entities

fougere build-frond blog

Compiles fronds/blog/entities/** to dist/ with type declarations and configures the Frond's package.json. A consumer in another repository can then install that package. See where the code lives.

Next: Philosophy.

Built with Fougere — this site runs on the framework it documents.