use crate::{error::CliError, ui, Result};
use forgedb_codegen::{
ApiGenerator, FfiGenerator, GoGenerator, GoSdkGenerator, NapiGenerator, OpenApiGenerator,
PyO3Generator, PythonSdkGenerator, RustGenerator, RustSdkGenerator, StubGenerator,
TypeScriptGenerator, WasmGenerator,
};
use forgedb_parser::Parser;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenerateMode {
Sdk,
Runtime,
Replica,
}
impl GenerateMode {
fn flag(self) -> &'static str {
match self {
GenerateMode::Sdk => "--sdk",
GenerateMode::Runtime => "--runtime",
GenerateMode::Replica => "--replica",
}
}
}
pub struct GenerateOptions {
pub target: String,
pub mode: Option<GenerateMode>,
pub check: bool,
pub output: Option<String>,
pub schema: Option<String>,
pub config_targets: Option<Vec<String>>,
pub gen_config: forgedb_codegen::GenConfig,
pub force: bool,
pub from: Option<u32>,
pub to: Option<u32>,
}
pub fn run(options: GenerateOptions) -> Result<()> {
ui::header("🔨", "Generating code from schema");
if options.target.to_lowercase() == "transform" {
let from = options.from.ok_or_else(|| {
CliError::Other("generate transform requires --from <version>".to_string())
})?;
let to = options.to.ok_or_else(|| {
CliError::Other("generate transform requires --to <version>".to_string())
})?;
let output = PathBuf::from(
options
.output
.as_deref()
.unwrap_or("migrations/transform"),
);
return crate::commands::migrate::emit_transform(from, to, &output, options.force);
}
let schema_path = match options.schema.as_deref() {
Some(p) => p.to_string(),
None => find_schema_file()?,
};
ui::info(&format!("Using schema: {}", schema_path));
let schema_content = fs::read_to_string(&schema_path)
.map_err(|e| CliError::SchemaNotFound(format!("{}: {}", schema_path, e)))?;
let mut parser = Parser::new(&schema_content)
.map_err(|e| CliError::SchemaValidation(format!("Lexer error: {}", e)))?;
let schema = parser
.parse()
.map_err(|e| CliError::SchemaValidation(format!("Parser error: {}", e)))?;
ui::success(&format!(
"Parsed schema ({} models, {} total fields)",
schema.models.len(),
schema.models.iter().map(|m| m.fields.len()).sum::<usize>()
));
let format_version = forgedb_migrations::current_format_version("migrations");
let output_dir = options.output.as_deref().unwrap_or("./generated");
let committed_path = PathBuf::from(output_dir);
let output_path = if options.check {
std::env::temp_dir().join(format!("forgedb-check-{}", std::process::id()))
} else {
committed_path.clone()
};
let force = options.force || options.check;
if options.check {
let _ = fs::remove_dir_all(&output_path);
}
fs::create_dir_all(&output_path)?;
let target = resolve_target(&options.target, options.mode)?;
ui::detail(&format!("output dir: {}", committed_path.display()));
ui::detail(&format!("format version: {}", format_version));
ui::detail(&format!("resolved target: {}", target));
let mut generated_files = Vec::new();
match target.as_str() {
"all" => {
let allowed = options.config_targets.as_deref();
generated_files.extend(generate_all(
&schema,
&output_path,
force,
allowed,
format_version,
options.gen_config,
)?);
}
"rust" => {
let result =
RustGenerator::generate_with_config(&schema, format_version, options.gen_config)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let path = output_path.join("database.rs");
write_file(&path, &result.code, force)?;
generated_files.push((path, result));
}
"typescript" => {
let result = TypeScriptGenerator::generate(&schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let path = output_path.join("types.ts");
write_file(&path, &result.code, force)?;
generated_files.push((path, result));
write_ts_package_scaffold(&output_path)?;
}
"api" => {
let result = ApiGenerator::generate_with_config(&schema, options.gen_config)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let path = output_path.join("api.rs");
write_file(&path, &result.code, force)?;
generated_files.push((path, result));
}
"openapi" => {
let result = OpenApiGenerator::generate(&schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let path = output_path.join("openapi.json");
write_file(&path, &result.code, force)?;
generated_files.push((path, result));
}
"stubs" => {
let result = StubGenerator::generate(&schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let stubs_dir = output_path.join("stubs");
fs::create_dir_all(&stubs_dir)?;
let path = stubs_dir.join("README.md");
write_file(&path, &result.code, force)?;
generated_files.push((path, result));
}
"wasm" => {
generated_files.extend(generate_wasm_replica(
&schema,
&output_path,
force,
format_version,
options.gen_config,
)?);
}
"ffi" => {
generated_files.extend(generate_ffi_engine(
&schema,
&output_path,
force,
format_version,
)?);
}
"pyo3" => {
generated_files.extend(generate_pyo3_binding(
&schema,
&output_path,
force,
format_version,
)?);
}
"napi" => {
generated_files.extend(generate_napi_binding(
&schema,
&output_path,
force,
format_version,
)?);
}
"go" => {
generated_files.extend(generate_go_binding(
&schema,
&output_path,
force,
format_version,
)?);
}
"rust-sdk" => {
generated_files.extend(generate_rust_sdk(&schema, &output_path, force)?);
}
"python-sdk" => {
generated_files.extend(generate_python_sdk(&schema, &output_path, force)?);
}
"go-sdk" => {
generated_files.extend(generate_go_sdk(&schema, &output_path, force)?);
}
_ => {
return Err(CliError::Other(format!(
"Unknown target: {}. Valid: standalone (all, rust, api, openapi, stubs, ffi, transform) \
or runtime × mode (python|node|bun|browser with --sdk/--runtime/--replica)",
target
)));
}
}
if options.check {
let mut missing = Vec::new();
let mut stale = Vec::new();
for (scratch_file, code) in &generated_files {
let rel = scratch_file
.strip_prefix(&output_path)
.unwrap_or(scratch_file);
let committed = committed_path.join(rel);
match fs::read_to_string(&committed) {
Ok(existing) if existing == code.code => {}
Ok(_) => stale.push(committed),
Err(_) => missing.push(committed),
}
}
let _ = fs::remove_dir_all(&output_path);
if missing.is_empty() && stale.is_empty() {
ui::success(&format!(
"Generated code is up to date ({} artifact(s) checked).",
generated_files.len()
));
return Ok(());
}
println!();
for path in &missing {
ui::error(&format!(" missing: {}", path.display()));
}
for path in &stale {
ui::error(&format!(" stale: {}", path.display()));
}
println!();
ui::error(&format!(
"Generated code is out of date ({} missing, {} stale) — run `forgedb generate` to update.",
missing.len(),
stale.len()
));
return Err(CliError::CodeGeneration(
"generated code is out of date".to_string(),
));
}
ui::success(&format!("Generated {} files:", generated_files.len()));
for (path, result) in &generated_files {
ui::info(&format!(
" ✓ {} ({} lines) - {}",
path.display(),
result.line_count(),
result.description
));
}
Ok(())
}
fn generate_all(
schema: &forgedb_parser::Schema,
output_path: &PathBuf,
force: bool,
target_filter: Option<&[String]>,
format_version: u32,
gen_config: forgedb_codegen::GenConfig,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let enabled = |name: &str| -> bool {
target_filter.map_or(true, |ts| ts.iter().any(|t| t.as_str() == name))
};
let mut files = Vec::new();
if enabled("rust") {
let rust_result = RustGenerator::generate_with_config(schema, format_version, gen_config)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let rust_path = output_path.join("database.rs");
write_file(&rust_path, &rust_result.code, force)?;
files.push((rust_path, rust_result));
}
if enabled("typescript") {
let ts_result = TypeScriptGenerator::generate(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let ts_path = output_path.join("types.ts");
write_file(&ts_path, &ts_result.code, force)?;
files.push((ts_path, ts_result));
write_ts_package_scaffold(output_path)?;
}
if enabled("api") {
let api_result = ApiGenerator::generate_with_config(schema, gen_config)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let api_path = output_path.join("api.rs");
write_file(&api_path, &api_result.code, force)?;
files.push((api_path, api_result));
}
if enabled("openapi") {
let openapi_result = OpenApiGenerator::generate(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let openapi_path = output_path.join("openapi.json");
write_file(&openapi_path, &openapi_result.code, force)?;
files.push((openapi_path, openapi_result));
}
if enabled("stubs") {
let stub_result = StubGenerator::generate(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let stubs_dir = output_path.join("stubs");
fs::create_dir_all(&stubs_dir)?;
let stub_path = stubs_dir.join("README.md");
write_file(&stub_path, &stub_result.code, force)?;
files.push((stub_path, stub_result));
}
if target_filter.is_some_and(|ts| ts.iter().any(|t| t.as_str() == "wasm")) {
files.extend(generate_wasm_replica(
schema,
output_path,
force,
format_version,
gen_config,
)?);
}
if target_filter.is_some_and(|ts| ts.iter().any(|t| t.as_str() == "ffi")) {
files.extend(generate_ffi_engine(schema, output_path, force, format_version)?);
}
if target_filter.is_some_and(|ts| ts.iter().any(|t| t.as_str() == "rust-sdk")) {
files.extend(generate_rust_sdk(schema, output_path, force)?);
}
if target_filter.is_some_and(|ts| ts.iter().any(|t| t.as_str() == "python-sdk")) {
files.extend(generate_python_sdk(schema, output_path, force)?);
}
if target_filter.is_some_and(|ts| ts.iter().any(|t| t.as_str() == "go-sdk")) {
files.extend(generate_go_sdk(schema, output_path, force)?);
}
Ok(files)
}
fn generate_ffi_engine(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
format_version: u32,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let ffi_dir = output_path.join("ffi");
let src_dir = ffi_dir.join("src");
fs::create_dir_all(&src_dir)?;
let mut files = Vec::new();
let ffi_result = FfiGenerator::generate(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let lib_path = src_dir.join("lib.rs");
write_file(&lib_path, &ffi_result.code, force)?;
files.push((lib_path, ffi_result));
let rust_result = RustGenerator::generate_with_format_version(schema, format_version)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let db_path = src_dir.join("database.rs");
write_file(&db_path, &rust_result.code, force)?;
files.push((db_path, rust_result));
write_ffi_engine_scaffold(&ffi_dir)?;
Ok(files)
}
fn generate_pyo3_binding(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
format_version: u32,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let pyo3_dir = output_path.join("pyo3");
let src_dir = pyo3_dir.join("src");
fs::create_dir_all(&src_dir)?;
let mut files = Vec::new();
let py_result =
PyO3Generator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let lib_path = src_dir.join("lib.rs");
write_file(&lib_path, &py_result.code, force)?;
files.push((lib_path, py_result));
let rust_result = RustGenerator::generate_with_format_version(schema, format_version)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let db_path = src_dir.join("database.rs");
write_file(&db_path, &rust_result.code, force)?;
files.push((db_path, rust_result));
let cargo_path = pyo3_dir.join("Cargo.toml");
if !cargo_path.exists() {
fs::write(
&cargo_path,
PyO3Generator::cargo_toml_scaffold("forgedb-python"),
)?;
ui::info(&format!(
" ✓ {} (PyO3 binding scaffold)",
cargo_path.display()
));
}
let pyproject_path = pyo3_dir.join("pyproject.toml");
if !pyproject_path.exists() {
fs::write(&pyproject_path, PYO3_PYPROJECT_SCAFFOLD)?;
ui::info(&format!(
" ✓ {} (maturin pyproject)",
pyproject_path.display()
));
}
Ok(files)
}
fn generate_napi_binding(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
format_version: u32,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let napi_dir = output_path.join("napi");
let src_dir = napi_dir.join("src");
fs::create_dir_all(&src_dir)?;
let mut files = Vec::new();
let napi_result =
NapiGenerator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let lib_path = src_dir.join("lib.rs");
write_file(&lib_path, &napi_result.code, force)?;
files.push((lib_path, napi_result));
let rust_result = RustGenerator::generate_with_format_version(schema, format_version)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let db_path = src_dir.join("database.rs");
write_file(&db_path, &rust_result.code, force)?;
files.push((db_path, rust_result));
let cargo_path = napi_dir.join("Cargo.toml");
if !cargo_path.exists() {
fs::write(&cargo_path, NapiGenerator::cargo_toml_scaffold("forgedb-node"))?;
ui::info(&format!(" ✓ {} (NAPI-RS binding scaffold)", cargo_path.display()));
}
let build_path = napi_dir.join("build.rs");
if !build_path.exists() {
fs::write(&build_path, NapiGenerator::build_rs_scaffold())?;
ui::info(&format!(" ✓ {} (napi-build script)", build_path.display()));
}
let package_path = napi_dir.join("package.json");
if !package_path.exists() {
fs::write(&package_path, NapiGenerator::package_json_scaffold())?;
ui::info(&format!(" ✓ {} (@napi-rs/cli package)", package_path.display()));
}
Ok(files)
}
fn generate_go_binding(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
format_version: u32,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let mut files = generate_ffi_engine(schema, output_path, force, format_version)?;
let go_dir = output_path.join("go");
fs::create_dir_all(&go_dir)?;
let go_result =
GoGenerator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let go_path = go_dir.join("forgedb.go");
write_file(&go_path, &go_result.code, force)?;
files.push((go_path, go_result));
let async_result = GoGenerator::generate_async_bridge();
let async_path = go_dir.join("forgedb_async.go");
write_file(&async_path, &async_result.code, force)?;
files.push((async_path, async_result));
let needs_arrow = GoGenerator::needs_arrow(schema);
if let Some(arrow_result) = GoGenerator::generate_arrow(schema) {
let arrow_path = go_dir.join("forgedb_arrow.go");
write_file(&arrow_path, &arrow_result.code, force)?;
files.push((arrow_path, arrow_result));
ui::warning(
"the Go Arrow export uses the external module `github.com/apache/arrow-go/v18` \
(added to go.mod) — run `go mod tidy` in the `go/` dir before `go build`",
);
}
let header_result =
GoGenerator::generate_header(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let header_path = go_dir.join("forgedb.h");
write_file(&header_path, &header_result.code, force)?;
files.push((header_path, header_result));
let go_mod_path = go_dir.join("go.mod");
if !go_mod_path.exists() {
fs::write(
&go_mod_path,
GoGenerator::go_mod_scaffold("forgedb", needs_arrow),
)?;
ui::info(&format!(" ✓ {} (Go module scaffold)", go_mod_path.display()));
}
let readme_path = go_dir.join("README.md");
if !readme_path.exists() {
fs::write(&readme_path, GoGenerator::readme_scaffold())?;
ui::info(&format!(" ✓ {} (Go binding README)", readme_path.display()));
}
Ok(files)
}
fn generate_rust_sdk(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let sdk_dir = output_path.join("rust-sdk");
let src_dir = sdk_dir.join("src");
fs::create_dir_all(&src_dir)?;
let mut files = Vec::new();
let result =
RustSdkGenerator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let lib_path = src_dir.join("lib.rs");
write_file(&lib_path, &result.code, force)?;
files.push((lib_path, result));
let cargo_path = sdk_dir.join("Cargo.toml");
if !cargo_path.exists() {
fs::write(
&cargo_path,
RustSdkGenerator::cargo_toml_scaffold("forgedb-client"),
)?;
ui::info(&format!(" ✓ {} (Rust SDK scaffold)", cargo_path.display()));
}
Ok(files)
}
fn generate_python_sdk(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let sdk_dir = output_path.join("python-sdk");
fs::create_dir_all(&sdk_dir)?;
let mut files = Vec::new();
let result =
PythonSdkGenerator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let py_path = sdk_dir.join("forgedb_client.py");
write_file(&py_path, &result.code, force)?;
files.push((py_path, result));
let pyproject_path = sdk_dir.join("pyproject.toml");
if !pyproject_path.exists() {
fs::write(&pyproject_path, PythonSdkGenerator::pyproject_scaffold())?;
ui::info(&format!(
" ✓ {} (Python SDK pyproject)",
pyproject_path.display()
));
}
Ok(files)
}
fn generate_go_sdk(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let sdk_dir = output_path.join("go-sdk");
fs::create_dir_all(&sdk_dir)?;
let mut files = Vec::new();
let result =
GoSdkGenerator::generate(schema).map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let go_path = sdk_dir.join("client.go");
write_file(&go_path, &result.code, force)?;
files.push((go_path, result));
let mod_path = sdk_dir.join("go.mod");
if !mod_path.exists() {
fs::write(&mod_path, GoSdkGenerator::go_mod_scaffold("forgedb-client"))?;
ui::info(&format!(" ✓ {} (Go SDK module scaffold)", mod_path.display()));
}
let readme_path = sdk_dir.join("README.md");
if !readme_path.exists() {
fs::write(&readme_path, GoSdkGenerator::readme_scaffold())?;
ui::info(&format!(" ✓ {} (Go SDK README)", readme_path.display()));
}
Ok(files)
}
const PYO3_PYPROJECT_SCAFFOLD: &str = r#"[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
[project]
name = "forgedb"
version = "0.1.0"
requires-python = ">=3.8"
description = "Generated ForgeDB Python binding"
[tool.maturin]
features = ["pyo3/extension-module"]
"#;
fn write_ffi_engine_scaffold(ffi_dir: &Path) -> Result<()> {
let path = ffi_dir.join("Cargo.toml");
if !path.exists() {
fs::write(&path, FfiGenerator::cargo_toml_scaffold("forgedb-ffi-engine"))?;
ui::info(&format!(" ✓ {} (native FFI engine scaffold)", path.display()));
}
Ok(())
}
fn generate_wasm_replica(
schema: &forgedb_parser::Schema,
output_path: &Path,
force: bool,
format_version: u32,
gen_config: forgedb_codegen::GenConfig,
) -> Result<Vec<(PathBuf, forgedb_codegen::GeneratedCode)>> {
let replica_dir = output_path.join("replica");
let src_dir = replica_dir.join("src");
fs::create_dir_all(&src_dir)?;
let mut files = Vec::new();
let wasm_result = WasmGenerator::generate(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let lib_path = src_dir.join("lib.rs");
write_file(&lib_path, &wasm_result.code, force)?;
files.push((lib_path, wasm_result));
let rust_result = RustGenerator::generate_with_format_version(schema, format_version)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let db_path = src_dir.join("database.rs");
write_file(&db_path, &rust_result.code, force)?;
files.push((db_path, rust_result));
let client_dir = replica_dir.join("client");
fs::create_dir_all(&client_dir)?;
let client_result = WasmGenerator::generate_client(schema)
.map_err(|e| CliError::CodeGeneration(e.to_string()))?;
let client_path = client_dir.join("replica-client.ts");
write_file(&client_path, &client_result.code, force)?;
files.push((client_path, client_result));
let worker_path = client_dir.join("replica-worker.js");
write_file(
&worker_path,
&WasmGenerator::worker_bootstrap_with_config(gen_config),
force,
)?;
ui::info(&format!(" ✓ {} (static worker bootstrap)", worker_path.display()));
write_wasm_replica_scaffold(&replica_dir)?;
Ok(files)
}
fn write_wasm_replica_scaffold(replica_dir: &Path) -> Result<()> {
let path = replica_dir.join("Cargo.toml");
if !path.exists() {
fs::write(&path, WasmGenerator::cargo_toml_scaffold("forgedb-replica"))?;
ui::info(&format!(" ✓ {} (wasm replica scaffold)", path.display()));
}
Ok(())
}
fn write_ts_package_scaffold(output_path: &Path) -> Result<()> {
let files = [
("package.json", TypeScriptGenerator::package_json_scaffold()),
("tsconfig.json", TypeScriptGenerator::tsconfig_scaffold()),
];
for (name, content) in files {
let path = output_path.join(name);
if !path.exists() {
fs::write(&path, content)?;
ui::info(&format!(" ✓ {} (npm SDK scaffold)", path.display()));
}
}
Ok(())
}
fn write_file(path: &PathBuf, content: &str, force: bool) -> Result<()> {
if path.exists() && !force {
return Err(CliError::Other(format!(
"File exists: {}. Use --force to overwrite",
path.display()
)));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, content)?;
Ok(())
}
fn resolve_target(raw: &str, mode: Option<GenerateMode>) -> Result<String> {
let target = raw.to_lowercase();
if target == "rust" {
return match mode {
None => Ok("rust".to_string()),
Some(GenerateMode::Sdk) => Ok("rust-sdk".to_string()),
Some(m) => Err(CliError::Other(format!(
"`generate rust {}` is not supported — `generate rust` emits the database core, \
`generate rust --sdk` emits the REST client crate",
m.flag()
))),
};
}
const STANDALONE: &[&str] = &["all", "api", "openapi", "stubs", "ffi", "transform"];
if STANDALONE.contains(&target.as_str()) {
if let Some(m) = mode {
return Err(CliError::Other(format!(
"`generate {target}` is a standalone artifact and takes no mode flag ({} was given)",
m.flag()
)));
}
return Ok(target);
}
match target.as_str() {
"typescript" => {
return Err(CliError::Other(
"`generate typescript` was renamed — use `generate node --sdk` or `generate bun --sdk`"
.to_string(),
));
}
"wasm" => {
return Err(CliError::Other(
"`generate wasm` was renamed — use `generate browser --replica`".to_string(),
));
}
_ => {}
}
let runtimes = ["python", "node", "bun", "browser", "go"];
if !runtimes.contains(&target.as_str()) {
return Err(CliError::Other(format!(
"Unknown target: {target}. Valid: standalone (all, rust, api, openapi, stubs, ffi, transform) \
or a runtime (python, node, bun, browser) with --sdk/--runtime/--replica"
)));
}
let mode = mode.ok_or_else(|| {
CliError::Other(format!(
"`generate {target}` needs a mode — one of --sdk, --runtime, --replica"
))
})?;
match (target.as_str(), mode) {
("node", GenerateMode::Sdk) | ("bun", GenerateMode::Sdk) => Ok("typescript".to_string()),
("node", GenerateMode::Runtime) | ("bun", GenerateMode::Runtime) => Ok("napi".to_string()),
("python", GenerateMode::Runtime) => Ok("pyo3".to_string()),
("go", GenerateMode::Runtime) => Ok("go".to_string()),
("python", GenerateMode::Sdk) => Ok("python-sdk".to_string()),
("go", GenerateMode::Sdk) => Ok("go-sdk".to_string()),
("browser", GenerateMode::Replica) => Ok("wasm".to_string()),
("node", GenerateMode::Replica) | ("bun", GenerateMode::Replica) => Err(CliError::Other(
"`generate node|bun --replica` (server-side WASM replica, #121) is not yet implemented"
.to_string(),
)),
(rt, m) => Err(CliError::Other(format!(
"`generate {rt} {}` is not a supported runtime × mode combination",
m.flag()
))),
}
}
fn find_schema_file() -> Result<String> {
let candidates = ["schema.forge", "schema.lang", "schema.forgedb"];
for candidate in &candidates {
if Path::new(candidate).exists() {
return Ok(candidate.to_string());
}
}
Err(CliError::SchemaNotFound(
"No schema file found. Expected one of: schema.forge, schema.lang, schema.forgedb"
.to_string(),
))
}