From 3f0cf0a7cbffd434393e11dfa4fe481a217d924b Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 12 Jun 2026 13:23:57 +0200 Subject: [PATCH] feat(workout): give created apps their release command Write a standalone hemx-workout command into created Workout apps so new app owners keep the same dev, test, build, mobile-release, mobile-verify, and doctor surface outside the monorepo. req: examples/006 --- hemx-xtask/src/main.rs | 121 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index ea96079..3a80476 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -151,9 +151,10 @@ fn create_workout_app(destination: &Path) -> std::io::Result<()> { "hemx_workout_example", "hemx_workout_app", )?; + write_workout_app_command(destination)?; fs::write( destination.join("CREATED.md"), - "# Created Workout app\n\nRun locally with:\n\n```sh\ncargo run --manifest-path Cargo.toml --bin workout-app\n```\n\nBuild for production with:\n\n```sh\ncargo build --manifest-path Cargo.toml --release --bin workout-app\n```\n\nThis app owns command/event/projection state and keeps Android/iOS signing outside the repo. req: examples/006\n", + "# Created Workout app\n\nUse one command surface from this directory:\n\n```sh\n./hemx-workout dev\n./hemx-workout test\n./hemx-workout build\nHEMX_WORKOUT_ORIGIN=https://workout.example.com ./hemx-workout mobile-release\nHEMX_WORKOUT_ORIGIN=https://workout.example.com ./hemx-workout mobile-verify\n./hemx-workout doctor\n```\n\nThis app owns command/event/projection state and keeps Android/iOS SDKs, store submission targets, and signing outside the repo. req: examples/006\n", )?; Ok(()) } @@ -174,6 +175,119 @@ fn replace_in_file(path: &Path, from: &str, to: &str) -> std::io::Result<()> { fs::write(path, contents.replace(from, to)) } +fn write_workout_app_command(destination: &Path) -> std::io::Result<()> { + let script_path = destination.join("hemx-workout"); + fs::write(&script_path, workout_app_command_script())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut permissions = fs::metadata(&script_path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script_path, permissions)?; + } + Ok(()) +} + +fn workout_app_command_script() -> String { + format!( + r#"#!/usr/bin/env sh +set -eu +APP_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +CMD=${{1:-dev}} +OUT=${{HEMX_WORKOUT_MOBILE_OUT:-"$APP_DIR/target/hemx-mobile/workout"}} +ORIGIN=${{HEMX_WORKOUT_ORIGIN:-https://workout.example.invalid}} +APP_ID=${{HEMX_WORKOUT_APP_ID:-com.hemx.workout}} +APP_NAME=${{HEMX_WORKOUT_APP_NAME:-hemx Workout Copilot}} +VERSION=${{HEMX_WORKOUT_VERSION:-0.1.0}} +RUNTIME_PATH='{runtime_path}' +RUNTIME_SHA='{runtime_sha}' + +write_blockers() {{ + mkdir -p "$OUT" + {{ + echo '# Workout mobile external blockers' + echo + echo 'The release kit is generated, but these external inputs are still required for signed store artifacts:' + echo + [ -n "${{ANDROID_HOME:-${{ANDROID_SDK_ROOT:-}}}}" ] || echo '- Android SDK not found: set ANDROID_HOME or ANDROID_SDK_ROOT before producing signed Android artifacts' + command -v java >/dev/null 2>&1 || echo '- Java runtime not found: Android packaging requires a JDK' + [ -n "${{HEMX_WORKOUT_ANDROID_KEYSTORE:-}}" ] || echo '- Android signing key not configured: set HEMX_WORKOUT_ANDROID_KEYSTORE for store-ready signing' + [ -n "${{HEMX_WORKOUT_GOOGLE_PLAY_TRACK:-}}" ] || echo '- Google Play submission target not configured: set HEMX_WORKOUT_GOOGLE_PLAY_TRACK after choosing the Play Console track outside this app' + command -v xcodebuild >/dev/null 2>&1 || echo '- Xcode command line tools not found: iOS archive/export requires xcodebuild on macOS' + [ -n "${{HEMX_WORKOUT_IOS_TEAM_ID:-}}" ] || echo '- iOS signing team not configured: set HEMX_WORKOUT_IOS_TEAM_ID for App Store/TestFlight export' + [ -n "${{HEMX_WORKOUT_APP_STORE_CONNECT_TEAM:-}}" ] || echo '- App Store Connect submission team not configured: set HEMX_WORKOUT_APP_STORE_CONNECT_TEAM after choosing the Apple account outside this app' + }} > "$OUT/BLOCKERS.md" +}} + +case "$CMD" in + dev|run) + exec cargo run --manifest-path "$APP_DIR/Cargo.toml" --bin workout-app + ;; + test) + exec cargo test --manifest-path "$APP_DIR/Cargo.toml" + ;; + build) + exec cargo build --manifest-path "$APP_DIR/Cargo.toml" --release --bin workout-app + ;; + mobile-release) + cargo build --manifest-path "$APP_DIR/Cargo.toml" --release --bin workout-app + mkdir -p "$OUT/android" "$OUT/ios" + write_blockers + printf 'path\talgorithm\tdigest\n%s\tsha256\t%s\n' "$RUNTIME_PATH" "$RUNTIME_SHA" > "$OUT/asset-integrity.tsv" + cat > "$OUT/release-manifest.json" < "$OUT/android/twa-release.json" < "$OUT/ios/webview-release.json" <&2; exit 1; }} + case "$ORIGIN" in https://*) ;; *) echo 'HEMX_WORKOUT_ORIGIN must be HTTPS' >&2; exit 1;; esac + for f in release-manifest.json asset-integrity.tsv android/twa-release.json ios/webview-release.json BLOCKERS.md; do test -f "$OUT/$f" || {{ echo "missing $OUT/$f" >&2; exit 1; }}; done + grep -F "$RUNTIME_PATH" "$OUT/release-manifest.json" >/dev/null + grep -F "$RUNTIME_SHA" "$OUT/asset-integrity.tsv" >/dev/null + grep -F 'app-owned command/event/projection records' "$OUT/release-manifest.json" >/dev/null + grep -F 'host_result_kinds' "$OUT/android/twa-release.json" >/dev/null + grep -F 'host_result_kinds' "$OUT/ios/webview-release.json" >/dev/null + echo "workout-mobile-verified\tout=$OUT" + ;; + doctor) + write_blockers + cat "$OUT/BLOCKERS.md" + ;; + *) + echo 'usage: ./hemx-workout dev|test|build|mobile-release|mobile-verify|doctor' >&2 + exit 2 + ;; +esac +"#, + runtime_path = hemx_js::RUNTIME_JS_PATH, + runtime_sha = hemx_js::RUNTIME_JS_HASH, + ) +} + fn copy_dir(source: &Path, destination: &Path) -> std::io::Result<()> { fs::create_dir_all(destination)?; for entry in fs::read_dir(source)? { @@ -958,6 +1072,11 @@ mod tests { let main_rs = fs::read_to_string(destination.join("src/main.rs")).expect("main rs"); assert!(main_rs.contains("hemx_workout_app")); assert!(!main_rs.contains("hemx_workout_example")); + let command = fs::read_to_string(destination.join("hemx-workout")).expect("command"); + assert!(command.contains("mobile-release")); + assert!(command.contains("mobile-verify")); + assert!(command.contains(hemx_js::RUNTIME_JS_HASH)); + assert!(command.contains("app-owned command/event/projection records")); assert!(destination.join("src/lib.rs").exists()); assert!(destination.join("templates/workout.heml").exists()); let _ = fs::remove_dir_all(&destination);