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
alphatag —npx fougere@alpharuns it without installing,npm create fougereenters it atnew. 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
| Flag | What it does |
|---|---|
--frond blog,api:catalog | Fronds to add — template:name renames; the template name is the default name |
--app nuxt:web | same, for apps |
--bare | the empty shell, nothing composed |
--flat | one domain: no fronds/, no workspace — the app root is the Frond |
--local | link @fougere/* to a local Fougere checkout (development) |
--force | overwrite 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()binds127.0.0.1unlesshostssays 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
| code | what it means |
|---|---|
directory-unreadable, handler-parse-failed, heritage-unresolved | what the scan could not do — a rule about an absence is only sound if the analysis attests it looked |
operation-unbound | an operation declares parameters and has no binding plan: it is served, and it receives none of them |
cross-frond-import | a 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.