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 { 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
| 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.
'@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:
| finding | means |
|---|---|
directory-unreadable, handler-parse-failed, heritage-unresolved | what the scan could not do — an unreadable directory, a handler that would not parse, an extends it could not follow |
operation-unbound | an operation whose parameters have no binding plan: it is served, and it receives nothing |
input-contract-ambiguous | two parameters could be the input contract, so provenance cannot pick one — refused rather than settled by parameter order |
cross-frond-import | a 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-domains | past 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.