forte-cli 0.3.40

CLI for the Forte fullstack web framework
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# `forte init` Specification

Status: **DRAFT v2 — Decisions resolved; awaiting implementation**.

This document is the single source of truth for what `forte init <name>` must produce. The current implementation (`forte/cli/src/cli/init.rs`) diverges from this spec in known ways tracked in §8 *Decisions*; closing those gaps is the implementation work that follows this spec.

Note on related documents:
- `forte/cli/PLAN.md` and `forte/cli/PROGRESS.md` are **historical design notes** from the original CLI bring-up. They contain stale decisions that the running apps no longer follow (e.g. `rs/Cargo.toml [package].name = "backend"` as a fixed value; rolldown; the `[project].name` Forte.toml schema). Where they disagree with this spec, **this spec wins**. PLAN.md / PROGRESS.md are not normative for this work.

Ground truth for the spec is the union of:
- Hard requirements deduced by reading every CLI subcommand and `forte-codegen` (see §6 *Why each file is required*).
- The two running applications:
  - `~/fn0/fn0/control/` (Forte project for the fn0 control plane).
  - `~/amgi/web/` (Forte project for mottomite).

Where the two apps disagree, this spec takes the most minimal layout that still satisfies the CLI's hard requirements. Project-specific extensions (Tailwind, custom Vite config, env codegen, etc.) are explicitly out of init's scope.

---

## 1. Goals and non-goals

### Goals
- `forte init <name>` produces a directory `<name>/` that can immediately run, in order, with no manual edits:
  1. `cd <name> && forte dev` — boots a working dev server serving a hello-world page.
  2. `forte build` — produces `<name>/dist/{backend.wasm,server.js}` with zero errors.
  3. `forte deploy [--name X]` — succeeds against a control plane the user is logged in to.
  4. `forte add page foo` and `forte add action bar` — add a working page / action that compiles.
- All hardcoded paths and filenames in `forte-codegen`, `forte-rs-to-ts`, `forte dev`, `forte build`, and `forte deploy` resolve correctly without manual fixup.
- The output is buildable without a parent `Cargo.toml` workspace (the project is a standalone, self-contained directory).

### Non-goals
- Init does NOT install a Rust toolchain, a Node.js runtime, or the bundled `forte-rs-to-ts` binary. Those are responsibilities of the developer's environment or of other CLI subcommands.
- Init does NOT pre-create any file that the CLI itself generates on the first `forte dev` / `forte build` (those files are listed in §5).
- Init does NOT pick a styling solution, a state management library, a router, a meta-framework, an ORM, or any opinion beyond what the CLI's codegen makes mandatory.
- Init does NOT bake a `Forte.toml` `project_id` — that is filled in by `forte deploy` on first deploy.

---

## 2. CLI invocation contract

```
forte init <name>
```

- `<name>` is a non-empty string usable as a directory name AND as a Cargo package name (kebab-case allowed, will be used verbatim as the `name = "..."` value of `rs/Cargo.toml`'s `[package]`).
- If `<name>` already exists as a directory, init aborts with a clear error and does not write anything.
- On success, init prints (verbatim, last lines):
  ```
  Created project '<name>'

  Next steps:
    cd <name>
    forte dev
  ```

---

## 3. Project layout

The complete tree that init writes (◆ = file written by init, generated directories like `target/`, `dist/`, `.forte/`, `node_modules/`, `fe/dist/` are NOT created — Rust / Vite / Forte CLI will create them on first build):

```
<name>/
├── .gitignore                ◆
├── Forte.toml                ◆  (empty file; populated by forte deploy)
├── rs/
│   ├── .cargo/
│   │   └── config.toml       ◆
│   ├── .gitignore            ◆
│   ├── Cargo.toml            ◆
│   ├── build.rs              ◆
│   └── src/
│       ├── lib.rs            ◆
│       └── pages/
│           └── index/
│               └── mod.rs    ◆
└── fe/
    ├── .gitignore            ◆
    ├── package.json          ◆
    ├── tsconfig.json         ◆
    ├── public/
    │   └── robots.txt        ◆
    └── src/
        ├── app.tsx           ◆
        └── pages/
            └── index/
                └── page.tsx  ◆
```

Hard requirements driving this shape:
- The project root MUST NOT contain a `Cargo.toml`. The Rust crate at `rs/` is a standalone crate, not a workspace member. Both running apps confirm this.
- The `rs/` and `fe/` directory names are hardcoded across `forte-codegen` (`generate_routes.rs:31` derives `<manifest_dir>/../fe/src/paths.generated.ts`), `forte-rs-to-ts` (`main.rs:56-58` writes to `../fe/src/{pages,hooks/.generated,actions/.generated}`), and `forte build`/`forte dev`. They are NOT customizable.
- `fe/src/app.tsx` MUST exist — its presence is the literal trigger for `is_new_mode()` (`forte/cli/src/cli/fe_runtime.rs:5-7`), which is the codepath all current CLI logic targets. Without it, the CLI falls back to a legacy mode that init does not support.

---

## 4. File-by-file content

All file contents below are normative. They are derived from the requirements of the consuming CLI code, not copied from either running app verbatim.

### 4.1 `<name>/.gitignore`

```
/target
/dist
/.forte
```

Rationale: `/target` covers the case where the user runs `cargo` at the root; `/dist` is written by `forte build`; `/.forte` is written by `forte-rs-to-ts` (`<project>/.forte/rs-to-ts-target/`).

### 4.2 `<name>/Forte.toml`

```
```

(Zero-byte file.) `forte deploy` deserializes this as `ForteConfig { project_id: Option<String> }`; an empty file is valid and equivalent to `project_id = None`. On first deploy, the CLI writes `project_id = "..."` into this file.

### 4.3 `<name>/rs/.gitignore`

```
/target
```

### 4.4 `<name>/rs/.cargo/config.toml`

```toml
[build]
target = "wasm32-wasip2"
```

Rationale: `forte build` and `forte dev` both pass `--target wasm32-wasip2` explicitly when invoking cargo (see invariant table §6), so this file is not strictly required by the CLI itself. It IS required for the common case of a user running `cargo check` / `cargo clippy` / `cargo build` directly inside `rs/` without thinking about targets. Both running apps either ship this file (fn0/control) or fail those direct-cargo commands (amgi/web has no `.cargo/config.toml` and direct `cargo build` in `rs/` would attempt the host triple, which `forte-sdk` does not support). We pick the explicit version.

### 4.5 `<name>/rs/Cargo.toml`

Template (with `<name>` substituted and dependency sources resolved per §8.2):

```toml
[package]
name = "<name>"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
anyhow = "1"
cookie = "0.18"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
http = "1"
tracing = "0.1"
forte-json = "<version>"
forte-sdk = "<version>"
doc-db = { package = "fn0-doc-db", version = "<version>" }

[build-dependencies]
forte-codegen = "<version>"
```

The `rs/Cargo.toml` ALSO carries an empty `[workspace]` table at the top, ahead of `[package]`. This makes the crate its own single-member workspace and prevents cargo from walking up the filesystem to discover a parent workspace (the original v1 draft missed this — adding it is mandatory; without it, projects created inside an existing workspace get pulled into the parent and codegen's `--target` assumptions break).

When the CLI is built with the `--dev` flag (or equivalent build-time switch — see §8.2), the four `forte-*`/`doc-db` lines instead use `path = "<absolute path>"` resolved from the CLI's own `CARGO_MANIFEST_DIR`.

Hard requirements:
- The crate's `[package].name` is read by `find_wasm_binary` in both `dev.rs` and `build.rs`. The wasm artifact path is derived as `target/wasm32-wasip2/release/<name with - replaced by _>.wasm`. The name is **user-supplied via the CLI argument**, not the fixed string `"backend"` (this overrides the stale convention in `PROGRESS.md`).
- `crate-type = ["cdylib"]` is mandatory; `forte-codegen` generates a `cdylib` entry point against the WASI HTTP world.
- `edition = "2024"` matches what `forte-codegen` formats output for (`rustfmt --edition=2024`).
- `doc-db` MUST be declared with the `package = "fn0-doc-db"` rename so that user code reads `use doc_db::...;` while the published crate name is `fn0-doc-db` (`doc-db/Cargo.toml:2`).

`tracing` IS included even though the seed index handler does not reference it: `forte-codegen` emits `#[forte_sdk::tracing::instrument(...)]` attributes on the generated dispatcher functions, and the expansion calls `<future>.instrument(span)` — which needs the `tracing::Instrument` trait visible at the call site. Without `tracing` in the user crate, the first `forte build` fails with `E0433: failed to resolve: use crate::route_generated::tracing::Instrument`. (The original v1 draft excluded `tracing`; e2e proved that to be a load-bearing dep.)

NOT included in the template:
- `serde_with`, `chrono`, `base64`, etc. — application-specific.

### 4.6 `<name>/rs/build.rs`

```rust
fn main() {
    forte_codegen::generate_routes();
}
```

Hard requirement: `forte-codegen::generate_routes()` writes `rs/src/route_generated.rs` and rewrites the FORTE-MANAGED block in `rs/src/lib.rs`. Without this call, the project will not compile after init.

NOT included: `forte_codegen::generate_env()`. It is opt-in (requires a `<name>/.env` file). amgi/web uses it; fn0/control does not. Users who want `.env`-derived constants add the call themselves.

### 4.7 `<name>/rs/src/lib.rs`

```rust
// === FORTE-MANAGED START ===
// Auto-managed by `forte build`. Do not edit between the START/END markers.
mod route_generated;
// === FORTE-MANAGED END ===
```

Hard requirements:
- The two literal marker comments are matched by string in `forte/codegen/src/generate_routes.rs:101-107`. **`generate_routes()` PANICS if either marker is missing.**
- The body between markers is overwritten on every build by codegen. Init writes a minimal seed (`mod route_generated;`) so that the marker block is well-formed for codegen to find on the very first build.

**First-compile note (decision §8.1 option A):** Immediately after `forte init`, the file `rs/src/route_generated.rs` does NOT yet exist. The seed `mod route_generated;` line therefore points at a non-existent file, and a *standalone* `cargo check` / `cargo clippy` in `rs/` will fail. The first action after init MUST be `forte dev` or `forte build`; their build-script step runs `forte-codegen::generate_routes()` which writes `route_generated.rs` **before** rustc compiles the crate, so the first build succeeds. This is the same gap both running apps have; the CLI's "Next steps" message tells the user the correct first action.

The CLI's two managed comments inside the marker block are tolerated by the comment-style rules: they are non-obvious, encode a real invariant (machine-managed boundary), and would confuse a reader who tries to edit between them.

### 4.8 `<name>/rs/src/pages/index/mod.rs`

```rust
use anyhow::Result;
use forte_sdk::ForteRequest;
use serde::Serialize;

#[derive(Serialize)]
pub enum Props {
    Ok { message: String },
}

pub async fn handler(_req: ForteRequest<'_>) -> Result<Props> {
    Ok(Props::Ok {
        message: "Hello from Forte!".to_string(),
    })
}
```

Hard requirements:
- Page detection in `forte/codegen/src/generate_routes/discover.rs:266-298` matches on `pub async fn handler(...) -> Result<...>` returning a type that references `Props` or `Redirect`.
- The handler signature MUST match what the generated dispatcher in `forte/codegen/src/generate_routes/codegen.rs` actually calls. The seed handler uses `ForteRequest<'_>` (matches dispatcher). The unrelated `forte add page` template in `add.rs:76-124` currently uses `(headers, jar)` — that template is stale and is tracked as a separate fix in §8.7.
- `Props` MUST be `Serialize` and the variant `Ok { message: String }` MUST exist for `forte-rs-to-ts` to emit a usable `Props` TypeScript type that the seed `page.tsx` consumes.

### 4.9 `<name>/fe/.gitignore`

```
/node_modules
/dist
/.forte
```

Rationale: `/.forte` is the runtime directory written by `fe_runtime::ensure()` and codegen.

### 4.10 `<name>/fe/package.json`

```json
{
  "name": "<name>-frontend",
  "private": true,
  "type": "module",
  "dependencies": {
    "react": "^19.2",
    "react-dom": "^19.2",
    "zod": "^4"
  },
  "devDependencies": {
    "@types/react": "^19.2",
    "@types/react-dom": "^19.2",
    "@vitejs/plugin-react": "^6",
    "typescript": "^5.9",
    "vite": "^8"
  }
}
```

Hard requirements:
- `react`, `react-dom`: the bundled `fe/.forte/{server.tsx,client.tsx}` imports them.
- `zod`: the bundled `fe/.forte/forte-react.ts` imports it.
- `@vitejs/plugin-react`, `vite`: required by the bundled `fe/.forte/vite.config.ts`.
- `typescript`, `@types/react`, `@types/react-dom`: required by `npx vite build` and `tsc` (Vite invokes TS via the plugin).

Init MUST write `package.json` first, then run `npm install` **once** in `<name>/fe/`. A single invocation is sufficient because `package.json` already lists every package; the current `init.rs:73-110` runs two separate `npm install` commands without `package.json` listing them up front, and that pattern goes away.

Version pins:
- fn0/control: `typescript ^6.0.3`. amgi/web: `typescript ^5.9.3`. We pin to `^5.9` for stability (TS 6.0 is recent and breaks some downstream tooling); revisit in a follow-up.
- React 19.2.x is consistent across both apps and is the lowest version that ships the new SSR API used by `fe_runtime/server.tsx`.

### 4.11 `<name>/fe/tsconfig.json`

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src", ".forte"]
}
```

Differences from current `init.rs`:
- `"noEmit": true` — Vite handles emit, `tsc` is for type-checking only. amgi/web ships this; fn0/control uses `"outDir": "dist"` which conflicts with Vite's output directory.
- `"include": ["src", ".forte"]` — the runtime files in `fe/.forte/` are TS and need type checking once generated. The `.forte/` directory is absent right after init; this is harmless (tsc treats missing globs as empty).

### 4.12 `<name>/fe/public/robots.txt`

```
User-agent: *
Allow: /
```

Minimal stub. `forte build` does not require this file but `dev.rs:324` passes `<name>/fe/public/` to the dev server as `public_dir`, and Vite errors if the directory is missing.

### 4.13 `<name>/fe/src/app.tsx`

```tsx
export function Head() {
    return (
        <>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Forte App</title>
        </>
    );
}
```

Hard requirements:
- The presence of this file is what `is_new_mode()` checks (`fe_runtime.rs:5-7`).
- The export `Head` is imported by the bundled `fe/.forte/server.tsx:11` (`import { Head } from "../src/app";`). Missing the export breaks SSR.

### 4.14 `<name>/fe/src/pages/index/page.tsx`

```tsx
import type { Props } from "./.props";

export default function IndexPage(props: Props) {
    if (props.t !== "Ok") {
        return <div>Error loading page</div>;
    }

    return (
        <div>
            <h1>Welcome to Forte</h1>
            <p>{props.message}</p>
        </div>
    );
}
```

Hard requirements:
- The default export is consumed by the generated `fe/.forte/routes.generated.ts`.
- `./.props` is the file emitted by `forte-rs-to-ts` next to this `page.tsx`. The `Props` TypeScript type is a Zod-derived **flat** discriminated union: `{ t: "Ok", message: string }` — NOT `{ t, v }`. Variant fields are spread onto the top-level object rather than nested under `v`. fn0/control's index page.tsx confirms this shape. (The v1 spec draft had `{ t, v }` which is incorrect.)

---

## 5. Files init MUST NOT create (the CLI generates them)

These files appear in fresh projects after the first `forte dev` or `forte build`, NOT in the init output:

- `<name>/rs/src/route_generated.rs` — written by `forte-codegen::generate_routes()`.
- `<name>/rs/src/env_generated.rs` — written by `forte-codegen::generate_env()` if used.
- `<name>/rs/src/{actions,admin,queue_task}/mod.rs` — written by codegen when corresponding directories have at least one item.
- `<name>/fe/src/paths.generated.ts` — written by codegen.
- `<name>/fe/src/pages/**/.props.ts` and `.input.ts` etc. — written by `forte-rs-to-ts`.
- `<name>/fe/src/hooks/.generated/*`, `<name>/fe/src/actions/.generated/*` — written by `forte-rs-to-ts`.
- `<name>/fe/.forte/{server.tsx,client.tsx,vite.config.ts,forte-react.ts,routes.generated.ts}` — written by `fe_runtime::ensure()` and codegen.
- `<name>/fe/dist/`, `<name>/dist/`, `<name>/rs/Cargo.lock`, `<name>/rs/target/`, `<name>/.forte/rs-to-ts-target/` — built artifacts.

The current `init.rs` does not create any of these — that part is correct. This section is normative for future changes.

---

## 6. Why each file is required (cross-cutting invariants)

A future contributor changing the CLI MUST preserve these invariants or update this spec:

| Invariant | Enforced by |
| --- | --- |
| Root has no `Cargo.toml` | `forte-rs-to-ts/src/main.rs:75` sets `current_dir = <project>/rs`; both running apps. |
| `rs/` is the Rust crate root | `forte-codegen/src/generate_routes.rs:16` (`CARGO_MANIFEST_DIR`), `forte-rs-to-ts/src/main.rs:66`. |
| `fe/` is sibling of `rs/` | `forte-codegen/src/generate_routes.rs:31` (`../fe/src/paths.generated.ts`), `forte-rs-to-ts/src/main.rs:56-58`. |
| `fe/src/app.tsx` exists and exports `Head` | `forte/cli/src/cli/fe_runtime.rs:5-7`; bundled `fe_runtime/server.tsx:11`. |
| `rs/src/lib.rs` has both `// === FORTE-MANAGED START ===` and `// === FORTE-MANAGED END ===` markers | `forte-codegen/src/generate_routes.rs:101-107` panics if missing. |
| `rs/Cargo.toml` has `[lib] crate-type = ["cdylib"]` and `[package].name = <name>` | `forte/cli/src/cli/build.rs` and `dev.rs` (`find_wasm_binary`). |
| `rs/Cargo.toml`'s `doc-db` dep uses `package = "fn0-doc-db"` | `doc-db/Cargo.toml:2`. |
| `Forte.toml` exists (even if empty) | `forte/cli/src/cli/build.rs:17-19` hard-fails if missing. |
| The CLI invokes cargo with `--target wasm32-wasip2` explicitly | `forte/cli/src/cli/build.rs:215-222`, `dev.rs:215-222`. The project's `rs/.cargo/config.toml` is therefore for developer ergonomics only, not for the CLI's correctness. |

---

## 7. Side effects init performs

In addition to writing the files in §4, init performs these actions in this order:

1. Create directory `<name>/` and all subdirectories listed in §3.
2. Write all files listed in §4.
3. Run `npm install` **once** in `<name>/fe/` (creates `<name>/fe/node_modules/` and `<name>/fe/package-lock.json`). This is the only network call init makes. If `npm install` fails, init aborts with a non-zero exit code; partially-written files remain on disk (init does NOT roll back).
4. Print the "Next steps" message (§2).

Init does NOT run `cargo` (no first-build trigger), does NOT download `forte-rs-to-ts` (deferred until `forte dev`/`build`), does NOT initialize git.

---

## 8. Decisions

All previously-open items are resolved. Each decision below is binding on the implementation.

### 8.1 First-compile bootstrap of `rs/src/lib.rs` ✅ Decided

**Decision: option A — Accept the gap.** Init writes `mod route_generated;` between the FORTE-MANAGED markers even though `route_generated.rs` does not yet exist. The first action after `forte init` is `forte dev` or `forte build`; the build-script step writes `route_generated.rs` before rustc compiles the crate.

Implication for §9 acceptance: `cargo clippy` in `rs/` is verified AFTER the first `forte build` has run, not on a pristine init output.

### 8.2 Path deps vs. published-crate deps in `rs/Cargo.toml` ✅ Decided

**Decision: option B — emit version deps by default; offer `--dev` for path deps.**

- Default `forte init <name>` writes `forte-json = "<v>"`, `forte-sdk = "<v>"`, `doc-db = { package = "fn0-doc-db", version = "<v>" }`, `forte-codegen = "<v>"` — i.e. version deps from crates.io. This is the form that works when the user installs `forte` via `cargo install` or homebrew.
- `forte init --dev <name>` (new flag) writes `path = "<absolute path>"` deps resolved from the CLI binary's `CARGO_MANIFEST_DIR`, matching today's behavior. This is for in-repo development inside the fn0 monorepo only.
- The exact pinned versions baked into the default template are read from the workspace `Cargo.toml` at CLI-build time (single source of truth). Today's pinned versions: `forte-json = 0.1.1`, `forte-sdk = 0.3.5`, `forte-codegen = 0.1.3`, `fn0-doc-db = 0.4.2`.

### 8.3 `forte-rs-to-ts` version pin unification ✅ Decided

**Decision: unify on `0.1.8` and publish a matching binary release.**

- `forte/cli/src/cli/build.rs:38` already pins `0.1.8`.
- `forte/cli/src/cli/dev.rs:40` is updated from `0.1.7` to `0.1.8` (this commit).
- `forte/rs-to-ts/Cargo.toml:3` already declares `version = "0.1.8"`.
- A new tag `forte-rs-to-ts-v0.1.8` is pushed; the `release-forte-rs-to-ts.yml` GitHub Actions workflow then builds and publishes the cross-platform archives that both CLI subcommands resolve via `fn0_release_url`.

Rationale: source version, CLI pins, and the GitHub release MUST all match, because `tools::fn0_release_url` synthesizes the download URL from the version constant. The current 0.1.7/0.1.8 split is a latent bug — uncached environments would 404 on `forte build`.

### 8.4 `wasm-component-ld` toolchain note ✅ Decided

**Decision: document the requirement in the fn0 README; do NOT add a runtime check to init.**

The Forte CLI invokes `cargo build --target wasm32-wasip2` and relies on cargo's native component linking. Stable Rust ≥ 1.84 ships a `wasm-component-ld` that decodes `wit-bindgen 0.50.0` output; older toolchains do not. This is an environment requirement, not an init concern.

### 8.5 Should init initialize git? ✅ Decided

**Decision: no.** Users who want git run `git init` themselves. Init stays minimal.

### 8.6 Should init create `<name>/env.yaml` and `<name>/.env`? ✅ Decided

**Decision: no.** Empty optional files are noise. Document them in `forte deploy`'s help output and in the README.

### 8.7 `forte add` templates are stale ✅ Decided (fixed in the init rewrite PR)

Three problems in `forte/cli/src/cli/add.rs`, all fixed together:

1. **Page backend template**: was `pub async fn handler(_headers: HeaderMap, _jar: CookieJar) -> Result<Props>`. The dispatcher calls `handler(req: ForteRequest)`. Fixed to use `ForteRequest<'_>` (and `(req, _path_params: PathParams)` for dynamic-segment pages, with the param struct renamed from `Params` to `PathParams` to match codegen's `discover.rs`).

2. **Action backend template**: was `pub async fn action(input: <Name>Input) -> Result<<Name>Output>`. `forte-codegen::discover_actions` requires the names `Input`, `Output`, and `handler`. Fixed to `pub async fn handler(req: ForteRequest<'_, Input>) -> Output` with the canonical type names.

3. **Action frontend wrapper removed entirely**: `forte-rs-to-ts` already emits a fully-typed Zod + `callAction()` wrapper at `fe/src/actions/.generated/<name>.ts` (both running apps consume actions exclusively via that path — `import { submit } from "../../actions/.generated/submit"`). The hand-rolled wrapper that `forte add action` used to write to `fe/src/actions/<name>.ts` was redundant, used stale type names (`<Name>Input`/`<Name>Output`), and imported from a path (`./{action_path}.types`) that does not exist. `forte add action` now writes only the backend `.rs` file and prints the import path the user should use.

4. **Action frontend wrapper URL**: the now-removed wrapper had `/_action/<path>` instead of `/__forte_action/<path>`. Moot post-removal, but worth flagging for anyone restoring the wrapper.

### 8.9 Default-mode (version-deps) build needed `fn0-doc-db 0.4.3` publish ✅ Resolved

The published `fn0-doc-db 0.4.2` on crates.io pinned `forte-sdk = "=0.3.4"` while the workspace had moved to `forte-sdk 0.3.5`. Default-mode `forte init` produced a project whose first `forte build` failed dependency resolution:

```
error: failed to select a version for `forte-sdk`.
    ... required by package `fn0-doc-db v0.4.2`
versions that meet the requirements `=0.3.4` are: 0.3.4
previously selected package `forte-sdk v0.3.5`
```

Resolved by bumping `doc-db/Cargo.toml` 0.4.2 → 0.4.3 and `cargo publish -p fn0-doc-db`. The 0.4.3 release on crates.io now carries `forte-sdk = "=0.3.5"`. E2E confirmed: `forte init demo` (default mode) followed by `forte build` now resolves cleanly from crates.io.

### 8.10 `forte` symlink targeted the wrong target directory ✅ Resolved

`forte/cli/build.rs` previously synthesized the symlink target as `<manifest_dir>/target/<profile>/forte`, which only existed when forte/cli was built standalone. Workspace builds (the default) put the binary at `<workspace_root>/target/<profile>/forte`, so the symlink pointed at a stale path. Fixed: `build.rs` now walks up from `CARGO_MANIFEST_DIR` to find the nearest `Cargo.toml` containing `[workspace]` and uses that root's `target/` (honors `CARGO_TARGET_DIR` if set).

### 8.8 Relationship to `PLAN.md` / `PROGRESS.md` ✅ Decided

Those documents are **historical** and contain stale decisions (`name = "backend"` fixed; rolldown; `[project].name` Forte.toml schema). When this spec is accepted, add a header line to both files: "Historical design notes. For the current `forte init` specification, see `INIT_SPEC.md`." Do NOT delete them; they carry useful context for the rest of the CLI surface (HMR, hot swap, etc.). Do NOT propagate their stale init claims into new code.

---

## 9. Acceptance criteria for the implementation PR

The `init.rs` rewrite is complete when, in a clean directory with no prior `forte` cache:

1. `forte init demo` writes exactly the tree in §3 plus `fe/node_modules/` and `fe/package-lock.json`, and runs `npm install` exactly **once**.
2. `forte dev` boots without error and serves `http://localhost:<port>/` showing "Welcome to Forte" + "Hello from Forte!".
3. `forte build` exits 0 and `ls dist/` shows `backend.wasm` and `server.js`.
4. `forte deploy --name demo-test` against a logged-in control plane succeeds end-to-end and writes `project_id` into `Forte.toml`.
5. `forte add page about && forte add action submit` writes the expected files and `forte build` still passes (this implicitly validates the §8.7 `add.rs` template fix).
6. After the first `forte build`, running `cargo clippy --release --target wasm32-wasip2` inside `rs/` exits 0 with zero warnings. (`cargo clippy` invokes `build.rs`, which writes `route_generated.rs` before rustc compiles the crate, so clippy passes even on a freshly-initted project — but only after build artifacts are in place; on the pristine init output before any build, clippy is expected to fail per §8.1.)
7. `forte init --dev demo-dev` writes path-dep variants of the four `forte-*`/`doc-db` lines in `rs/Cargo.toml` and is buildable inside the fn0 monorepo without further edits.

Each criterion must be reproduced once and recorded in the PR description.

## 10. Verification log

All seven §9 criteria reproduced and passing as of 2026-05-20:

1. ✅ `forte init --dev demo-dev` produced exactly the §3 tree plus `fe/node_modules/` + `fe/package-lock.json`; `npm install` ran once.
2. ✅ `forte dev` served `http://localhost:3000/` and `/about` with HTTP 200 and the expected hello-world content.
3. ✅ `forte build` exited 0; `dist/` contained `backend.wasm` (1.6 MB) + `server.js`.
4. ✅ `forte deploy --name demo-test` succeeded end-to-end, wrote `project_id = "7d4eez0e"` into `Forte.toml`, uploaded 4 static assets + the bundle tar; the deployed site is live at `https://7d4eez0e.fn0.dev/`.
5. ✅ `forte add page about` + `forte add action submit` + `forte build` passed; `POST /__forte_action/submit` returned `{"t":"Ok","result":"Received: hi"}`.
6. ✅ `cargo clippy --release --target wasm32-wasip2` in `rs/` exited 0 with zero warnings (after the first build).
7. ✅ `forte init --dev demo-dev` emitted path-deps and built inside the monorepo; default-mode `forte init demo` emitted version-deps and built cleanly from crates.io after `fn0-doc-db 0.4.3` was published (§8.9).