Skip to content

10. phaze-cli

─── TypeScript toolchain (Node/TS — Babel → Oxc → Rolldown) ───────────
1. phaze-tsplugin 2. phaze-compile 3. phaze-vite
4. phaze-astro / 5. phaze-cloudflare (host adapter — pick one)
6. phaze-language-tools 7. phaze-vscode 8. phaze-glow 9. phaze-check
─── Rust toolchain (crates.io — Rust → Wasm, NOT the Babel/Vite pipeline) ──
10. phaze-cli ← codegen + native-shell orchestration ← you are here
└ runtime crates: phaze · phaze-core · phaze-macros · phaze-crypto · phaze-fb · phaze-native

phaze-cli is the one tooling package of the Rust toolchain — everything else in that ecosystem (phaze, phaze-core, phaze-macros, phaze-crypto, phaze-fb, phaze-native) is runtime, documented under Integrations › Rust. It’s an installable binary, not a runtime dependency:

Terminal window
cargo install phaze-cli # → `phaze` on PATH

It reads your handler source (and, ahead, your schemas) and emits typed artifacts for the TypeScript side of the app. That’s what makes phaze the only Rust HTTP framework with end-to-end Rust → TS types: the Rust handler source is the single source of truth for the client. For a native shell it is also the orchestrator, phaze native dev / build / bundle / info (below) — the app’s Vite and the shell’s cargo as one command. Ahead of both it scaffolds: a create-app command that lays down a web or a native project in the settled shape (see the roadmap).

phaze gen client is how Rust endpoints join Phaze Transport — the unified, typed data+API layer. The architecture: all endpoints are Rust (src/api/*.rs) bar a tiny TS set (introspection + the app-router actions that leverage the cache); Rust is the canonical API. Both runtimes feed one consumption face — phaze-compile populates it for TS actions, phaze gen client folds the Rust endpoints in — so a caller hits any endpoint the same way regardless of which runtime hosts it.

A third path keeps the Rust inside the fence: lang: rust marks the fence body itself as Rust, and a gen-rust build step hoists it into a rust-api handler at the fence’s rust: URL (the TS side proxies to it, exactly as an empty-body rust: knob would). It’s the “write the handler right here” option — distinct from proxying to an endpoint that already exists. And because the body is real Rust, it gets first-class editor support: live rust-analyzer hover and cargo check red squiggles, mapped back onto the .phaze through the sourcemap gen-rust emits — see phaze-vscode › Rust in lang: rust fence bodies.

Crucially, parity between the two runtimes is by a runtime-neutral wire contract, not shared code. Each Transport field defines a contract (sign: → X-Phaze-Sig: ed25519:<base64> over the body; flatbuffer: → application/x-flatbuf + the raw buffer), and both emitters conform to it — so a Rust-produced and a TS-produced response are interchangeable to one client-side consumer. That’s why gen client carries the field contracts, not just the fetch: a #[get("/…", sign = "ed25519")] endpoint’s generated client emits the same native subtle.verify the TS action arrow does (ed25519 is Web Crypto native → the browser verifies for free; see Security). Testing the round-trip is testing parity.

CommandWalksEmits
phaze gen clientsrc/api/**/*.rs (syn AST) — #[get/post/...] + handler signatures, respecting #[group("/prefix")]a typed TS client: camelCase fns, path interpolation, body/query handling, a PhazeError-aware fetch wrapper
phaze gen openapithe same metadataOpenAPI 3.1 JSON — paths keyed by {id}, request/response schemas derived from Json<T> / Form<T> / Path<T> / Query<T> / return types; user types via $ref

Both share one AST walker over src/api/**/*.rs — the Endpoint metadata model — so the client and the OpenAPI description never drift from each other or from the handlers.

Terminal window
phaze gen client --out src/lib/api.gen.ts
phaze gen openapi --out openapi.json

phaze native drives a Phaze app in a native shell: the app’s dev server and the shell’s cargo as one command, and the signed bundle. The shape is Tauri’s CLI, read from its source; the differences are the ones a shell with a provisioning profile needs. A Swift shell (the SwiftUI WebView, macOS 26) is outside dev and build today; bundle --binary <path> signs its binary (design, not shipped).

CommandDoesFlags
phaze native devStarts the app’s dev server (dev-command, default pnpm dev) in its own process group, refuses if dev-url is already answered by something else, waits for the port (1 s tries, 2 s apart, three minutes), then the shell as a dev build: cargo run --no-default-features — the crate’s other default features passed back, PHAZE_NATIVE_DEV_URL set for the shell’s build.rs to bake in — or, for a Swift package, swift build and the product launched with the variable in its environment. Then it watches the shell’s sources (src/, build.rs and the manifest of a crate; Sources/ and Package.swift of a package, polled once a second) and on a change stops the shell’s group, rebuilds and relaunches it; a build that fails is reported and waited out, the dev server stays up. View edits are Vite’s to reload. Stops both on exit, Ctrl-C or SIGTERM.--release · --features · --no-dev-server · --no-dev-server-wait · --no-watch · -- <app args> (default run-args)
phaze native buildThe app’s build (build-command, default pnpm build), the shell’s release build — cargo build --release with the embed default on, or swift build -c release — then bundle. --run launches what was built with run-args, the environment inherited, so the built app is tested from the command that produced it.--debug · --features · --no-app-build · --no-bundle · --run · the bundle flags
phaze native bundleFrom a built binary: the .app under target/<profile>/bundle/macos/, the plist stamped with bundle id, executable and version from the manifest, for a Swift shell the app’s shipped directory, dist/app, copied into Contents/Resources/dist (a Rust shell embeds it at compile time), a refusal unless the entitlements claim exactly <team>.<bundle-id>, xattr -crs, codesign --force --options runtime --entitlements … --sign …, codesign --verify --strict; then, when the APPLE_* variables are set, ditto → xcrun notarytool submit --wait → xcrun stapler staple; then a signed <product>_<version>_<arch>.dmg.--sign dev|release · --bundles app,dmg · --no-sign · --skip-stapling · --debug · [binary]
phaze native infoThe toolchain (macOS, Xcode, rustc, cargo, node, pnpm, flatc), the resolved table, and security find-identity’s codesigning identities.

Every command finds the shell’s manifest by walking up from the current directory, cargo’s own rule — a crate’s Cargo.toml, or a Swift package’s native.toml beside its Package.swift — so an app’s package.json can carry "wry:dev": "phaze native dev". --manifest-path overrides; an app with more than one shell names each, "swift:dev": "phaze native dev --manifest-path src-swift/native.toml" beside "tauri:dev": "tauri dev", and keeps no bare dev to be ambiguous between them.

The table they read, [package.metadata.native] in the shell crate’s Cargo.toml — cargo’s per-tool convention, the one cargo-bundle and cargo-deb use. A Swift shell keeps the same keys at the top level of its native.toml, plus the two cargo would have supplied: product, the executable swift build produces, and version, what the bundle’s plist is stamped with.

[package.metadata.native]
app = "app" # the phaze app: its vite.config.ts and package.json
dev-url = "http://localhost:5180" # bound strictly by the app's Vite config; 5173 is the web target's
run-args = ["tray"] # what `cargo run --` needs to open the shell
[package.metadata.native.macos]
product = "MyApp" # → target/<profile>/bundle/macos/MyApp.app
bundle-id = "com.example.myapp"
team = "XXXXXXXXXX"
info-plist = "release/macos/Info.plist"
entitlements = "release/macos/myapp.entitlements"
[package.metadata.native.macos.sign.dev]
identity = "Apple Development: Your Name (XXXXXXXXXX)"
profile = "~/Downloads/MyApp_Dev.provisionprofile"
[package.metadata.native.macos.sign.release]
identity = "Developer ID Application: Your Org (XXXXXXXXXX)"
profile = "~/Downloads/MyApp_Developer_ID.provisionprofile"

dev-command, build-command and out are optional. A signing pair is an identity and the provisioning profile it was issued with — the identity must be one the profile lists, since a valid certificate the profile does not list dies at launch with an AMFI kill that reads like “the entitlement does not work”. Neither is a secret; another Mac overrides with APPLE_SIGNING_IDENTITY and PHAZE_NATIVE_PROFILE. Notarization uses Tauri’s names, APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID, or APPLE_API_KEY + APPLE_API_ISSUER (+ APPLE_API_KEY_PATH), so every published CI recipe transfers.

The shell’s side of the contract is two things in a crate: a default-on embed feature gating include_dir! and the protocol handler, and a build.rs that re-exports PHAZE_NATIVE_DEV_URL as a rustc-env for the dev build to read with option_env!. A Swift shell has no feature to switch: it reads PHAZE_NATIVE_DEV_URL from its environment at launch and loads that instead of its own scheme. The phaze-native page has the reasoning; the create-app scaffold will lay both down.

phaze-cli grows along the gen and native surfaces, and ahead of them a create surface — the scaffolding its about line already reserves. Status of the next targets (✓ shipped · ▶ planned):

TargetStatusWhat it is
Scaffolding — a create-app command▶ plannedA React-style create-app (npm create vite shape): phaze create (name provisional) lays down a complete project boilerplate for either host, so a new app starts in the settled shape instead of assembling it by hand. Web (phaze-cloudflare): src/pages/, src/app.phaze, src/components/, src/transport/transport.phaze + src/transport.ts, src/api/, a vite.config.ts with cloudflare(), wrangler.jsonc, tsconfig.json, phaze-modules.d.ts, the check script. Native (phaze-native): the shell crate — wry, include_dir! over the built directory, the scheme handler that decodes by suffix and serves the exact-origin CORS header — plus the app it embeds: a vite.config.ts with native({ shell, compress }), src/app.phaze, src/views/(landing)/index.phaze, src/components/(app)/, the same tsconfig.json / phaze-modules.d.ts / check. Both boilerplates reproduce the two live project shapes; the scaffold introduces no convention of its own. For the native project it also lays down the shell’s side of the phaze native contract: the embed feature, the build.rs lines, release/macos/, and the [package.metadata.native] table.
Transport-aware client from Rust fields✓ wire contract ships · ▶ CLI codegen plannedPhaze Transport is a runtime-neutral wire contract: a sign = "ed25519" Rust #[get] already emits the detached X-Phaze-Sig the browser verifies — phaze-compile injects the inline-native subtle.verify from the declared field, with the same inlined PUBLIC_PHAZE_SIGN_KEY whether a TS action or a Rust handler signed it (one model, two runtimes). So a signed Rust endpoint is a first-class Transport citizen today. What’s planned for the CLI is narrower: gen client reads the handler’s outbound sign / encrypt fields so the generated typed client carries the inverse straight from the Rust source — the handler stays the single source of truth, instead of the field being re-declared on a TS action.
.fbs → runtime-free reads✓ reads ship · ▶ CLI .fbs parser plannedThe runtime-free FlatBuffer read already ships: a flatbuffer: action’s field reads inline at the call site to native DataView/TextDecoder ops (strip macro — zero shipped reader, zero npm flatbuffers, phaze runtime unchanged; Flat Buffers & SSR). Today the vtable layout is read from a flatc-generated reader (an opt-in flatbuffersReaderPlugin also swaps such a reader’s npm-flatbuffers import for a lean DataView ByteBuffer). Planned for the CLI: a pure-Rust .fbs parser (no flatc; mirrors parse.rs → Endpoint → emit.rs) so the layout comes straight from .fbs — feeding the same inline reads + the typed flat() client. Rust structs stay flatc-generated for now.
Further gen targets▶ plannede.g. typed query / route helpers — slot into the same walker.
  • Integrations › Rust — the phaze Rust HTTP runtime that phaze-cli generates clients for (handler shape, bindings, FlatBuffers, performance).
  • Tooling overview — where phaze-cli sits in the two-toolchain ecosystem.
  • phaze-compile — the Node-side transpiler it hands off to at the codegen seam.