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 { Card } from '@fougere/schema';

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

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 @fronds/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 @fronds/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 Card.fromDescriptor<{ id: string; title: string; at: Date }>(…).toSchema() {}

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.

'@fronds/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 check — what the scan could not do

fougere check                 # from a project root; --root points elsewhere

It scans without booting, so it reports on a project it cannot start. Five rules today, each a stable name shared with the scan's own diagnostics:

findingmeans
directory-unreadable, handler-parse-failed, heritage-unresolvedwhat the scan could not do — an unreadable directory, a handler that would not parse, an extends it could not follow
operation-unboundan operation whose parameters have no binding plan: it is served, and it receives nothing
input-contract-ambiguoustwo parameters could be the input contract, so provenance cannot pick one — refused rather than settled by parameter order
cross-frond-importa relative import that resolves into another Frond — a colocation constraint nothing declares, which holds until the day that Frond is not on this disk
frond-holds-several-domainspast six entities, the same threshold fougere graph uses

Findings are blocking or warning, and only a blocking one exits non-zero — which is what makes it usable in CI. An unresolvable base class with no operation is ordinary, and a check that cries wolf stops being read.

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 devtools — what the running apps are dispatching

fougere devtools                      # every address this project declares
fougere devtools --url http://127.0.0.1:4100
fougere devtools --json               # one page, then stop

Reads the call log an app keeps when it mounted @fougere/calls, over /_fougere/call like any other consumer. The reader pulls: no app registers anywhere, and an app started alone depends on nothing. The price is knowing the addresses, and the project already states them — remotes: says where a call goes, so it also says where the other half of a call can be watched. Without it, the default is http://127.0.0.1:3000.

An app that never installed the package answers Unknown rpc operation 'calls'. It serves discover., and that address is reported as a refusal rather than as silence.

fougere completion — shell completion

fougere completion zsh >> ~/.zshrc

zsh and bash; the shell is auto-detected when the argument is absent. The names it completes come out of the same entities the commands ride on, so a new command needs no second list.

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.