From 0f1673834bb6e4df664650d63011bba9a8a6ee81 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 17:26:23 +0200 Subject: [PATCH] fix(xtask): polish generated app next steps Give app new users app-local next steps and generated README/CREATED docs, rename visible v0 text, and fail early when the required hemplate checkout is missing instead of producing a broken path dependency. req: ceremony/005 --- README.md | 5 +++++ hemx-xtask/src/main.rs | 51 +++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 54d5432..e42827d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ SaaS tutorial app, an advanced Kanban milestone sketch, and a full techdemo. The v1 story now has tutorial, recipe, diagnostics, and stability docs; see `docs/v1-readiness.md` for the remaining close-gap audit. +Local checkout note: until the hemplate crates are published, this repository +expects `hemplate` checked out next to `hemx` as `../hemplate/hemplate`. The app +scaffolder fails with that exact path if the prerequisite is missing, instead of +creating an app that fails later with a vague Cargo path-dependency error. + ## The normal path For beginner and production-shaped app code, stay on this path. req: public_api/001 req: public_api/005 diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index 39a0245..9ad6bb3 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -85,15 +85,16 @@ fn run_app_new(operands: &[String]) -> ExitCode { if mobile { "app-new-mobile" } else { "app-new" }, destination.display() ); - println!( - "next\tcargo test --manifest-path {}/Cargo.toml", - destination.display() - ); + println!("next\tcd {}", destination.display()); if mobile { - println!( - "next\tHEMX_APP_ORIGIN=https://app.example.com {}/hemx-app mobile-release", - destination.display() - ); + println!("next\t./hemx-app test"); + println!("next\t./hemx-app build"); + println!("next\tHEMX_APP_ORIGIN=https://app.example.com ./hemx-app mobile-release"); + println!("next\tHEMX_APP_ORIGIN=https://app.example.com ./hemx-app mobile-verify"); + } else { + println!("next\tcargo test"); + println!("next\tcargo run"); + println!("next\tcargo build --release"); } ExitCode::SUCCESS } @@ -175,6 +176,7 @@ fn run_workout_new(destination: Option<&str>) -> ExitCode { fn create_app_scaffold(destination: &Path) -> std::io::Result<()> { let root = repo_root()?; + let hemplate = hemplate_checkout(&root)?; copy_dir(&root.join("examples/v0"), destination)?; let cargo_toml = destination.join("Cargo.toml"); let manifest = fs::read_to_string(&cargo_toml)?; @@ -186,7 +188,7 @@ fn create_app_scaffold(destination: &Path) -> std::io::Result<()> { .replace("edition.workspace = true", "edition = \"2021\"") .replace( "path = \"../../../hemplate/hemplate\"", - &format!("path = \"{}\"", root.join("../hemplate/hemplate").display()), + &format!("path = \"{}\"", hemplate.display()), ) .replace( "path = \"../../hemx\"", @@ -207,9 +209,14 @@ fn create_app_scaffold(destination: &Path) -> std::io::Result<()> { )?; replace_in_tree(destination, "hemx_v0_examples", "hemx_app")?; replace_in_tree(destination, "hemx-v0-examples", "hemx-app")?; + replace_in_tree(destination, "hemx v0 browser examples", "hemx app")?; + fs::write( + destination.join("README.md"), + "# hemx app\n\nThis app was created by `cargo run -p hemx-xtask -- app new PATH`. It is the generic checked-hypermedia starter: one page, form, keyed row partial, notice slot, Rust handlers, and tests using generated helpers instead of raw ids, opcodes, selector UI JavaScript, or manual registry plumbing. req: ceremony/005\n\n## Run\n\n```sh\ncargo run\n```\n\nOpen .\n\n## Test and build\n\n```sh\ncargo test\ncargo build --release\n```\n\n## Edit\n\n- `templates/todos.heml` owns the reusable partials and generated target names.\n- `src/main.rs` owns handlers and app state.\n- `src/lib.rs` re-exports generated `ui` helpers.\n\nThe reusable todo row partial is rendered in the initial page and updated through generated append/replace/remove/dynamic-batch effects. req: canonical_authoring/003\n", + )?; fs::write( destination.join("CREATED.md"), - "# Created hemx app\n\nThis scaffold is the generic checked-hypermedia starting point: one page, form, keyed row partial, notice slot, Rust handlers, and tests using generated helpers instead of raw ids, opcodes, selector UI JavaScript, or manual registry plumbing.\n\n```sh\ncargo test\ncargo run\n```\n\nThe reusable todo row partial is rendered in the initial page and updated through generated append/replace/remove/dynamic-batch effects. req: canonical_authoring/003 req: ceremony/005\n", + "# Created hemx app\n\nUse one app-owned command surface from this directory:\n\n```sh\ncargo test\ncargo run\ncargo build --release\n```\n\nThis scaffold is the generic checked-hypermedia starting point: one page, form, keyed row partial, notice slot, Rust handlers, and tests using generated helpers instead of raw ids, opcodes, selector UI JavaScript, or manual registry plumbing.\n\nThe reusable todo row partial is rendered in the initial page and updated through generated append/replace/remove/dynamic-batch effects. req: canonical_authoring/003 req: ceremony/005\n", )?; Ok(()) } @@ -256,6 +263,7 @@ fn create_mobile_app_scaffold(destination: &Path) -> std::io::Result<()> { fn create_workout_app(destination: &Path) -> std::io::Result<()> { let root = repo_root()?; + let hemplate = hemplate_checkout(&root)?; copy_dir(&root.join("examples/workout"), destination)?; let cargo_toml = destination.join("Cargo.toml"); let manifest = fs::read_to_string(&cargo_toml)?; @@ -274,7 +282,7 @@ fn create_workout_app(destination: &Path) -> std::io::Result<()> { .replace("edition.workspace = true", "edition = \"2021\"") .replace( "path = \"../../../hemplate/hemplate\"", - &format!("path = \"{}\"", root.join("../hemplate/hemplate").display()), + &format!("path = \"{}\"", hemplate.display()), ) .replace( "path = \"../../hemx\"", @@ -318,6 +326,21 @@ fn repo_root() -> std::io::Result { .to_path_buf()) } +fn hemplate_checkout(root: &Path) -> std::io::Result { + let hemplate = root.join("../hemplate/hemplate"); + if hemplate.join("Cargo.toml").exists() { + Ok(hemplate) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!( + "hemplate checkout not found at {}; clone hemplate next to hemx or adjust the generated Cargo.toml after creation", + hemplate.display() + ), + )) + } +} + fn replace_in_file(path: &Path, from: &str, to: &str) -> std::io::Result<()> { let contents = fs::read_to_string(path)?; fs::write(path, contents.replace(from, to)) @@ -1254,6 +1277,7 @@ mod tests { let main_rs = fs::read_to_string(destination.join("src/main.rs")).expect("main rs"); let template = fs::read_to_string(destination.join("templates/todos.heml")).expect("template"); + let readme = fs::read_to_string(destination.join("README.md")).expect("readme"); let created = fs::read_to_string(destination.join("CREATED.md")).expect("created docs"); assert!(manifest.contains("name = \"hemx-app\"")); @@ -1265,6 +1289,11 @@ mod tests { assert!(template.contains("data-hemx-slot=\"notice\"")); assert!(template.contains("data-hemx-slot=\"todo_row\"")); assert!(template.contains("h-key=\"row.id\"")); + assert!(readme.contains("# hemx app")); + assert!(readme.contains("cargo run")); + assert!(readme.contains("cargo build --release")); + assert!(readme.contains("templates/todos.heml")); + assert!(!readme.contains("cargo run -p hemx-app")); assert!(created.contains("keyed row partial")); assert!(created.contains("generated append/replace/remove/dynamic-batch effects")); assert!(destination.join("src/lib.rs").exists());