diff --git a/.github/workflows/platform-checks.yml b/.github/workflows/platform-checks.yml index ece83f79..5a8fed72 100644 --- a/.github/workflows/platform-checks.yml +++ b/.github/workflows/platform-checks.yml @@ -24,13 +24,8 @@ jobs: - name: Set up Rust uses: dtolnay/rust-toolchain@stable - - name: Install Linux DBus development package - run: | - sudo apt-get update - sudo apt-get install -y pkg-config libdbus-1-dev - - name: Test generated platform scripts - run: cargo test -p fission-cli + run: cargo test -p cargo-fission web: name: Web / wasm @@ -46,11 +41,6 @@ jobs: with: targets: wasm32-unknown-unknown - - name: Install Linux DBus development package - run: | - sudo apt-get update - sudo apt-get install -y pkg-config libdbus-1-dev - - name: Check wasm core crates run: | cargo check -p fission-core --target wasm32-unknown-unknown @@ -62,7 +52,7 @@ jobs: run: cargo install wasm-pack --version 0.13.1 --locked - name: Diagnose web toolchain - run: cargo run -p fission-cli --bin fission -- doctor web --project-dir examples/web-smoke + run: cargo run -p cargo-fission --bin fission -- doctor web --project-dir examples/web-smoke - name: Build web-smoke wasm package run: ./examples/web-smoke/platforms/web/build-wasm.sh @@ -86,11 +76,6 @@ jobs: with: targets: aarch64-linux-android - - name: Install Linux DBus development package - run: | - sudo apt-get update - sudo apt-get install -y pkg-config libdbus-1-dev - - name: Set up Android SDK uses: android-actions/setup-android@v3 @@ -103,7 +88,7 @@ jobs: "ndk;${ANDROID_NDK_VERSION}" - name: Diagnose Android toolchain - run: cargo run -p fission-cli --bin fission -- doctor android --project-dir examples/mobile-smoke --strict + run: cargo run -p cargo-fission --bin fission -- doctor android --project-dir examples/mobile-smoke --strict - name: Check mobile-smoke for Android run: | @@ -136,7 +121,7 @@ jobs: targets: aarch64-apple-ios-sim,aarch64-apple-ios - name: Diagnose iOS toolchain - run: cargo run -p fission-cli --bin fission -- doctor ios --project-dir examples/mobile-smoke --strict + run: cargo run -p cargo-fission --bin fission -- doctor ios --project-dir examples/mobile-smoke --strict - name: Check mobile-smoke for iOS simulator run: cargo check -p mobile-smoke --target aarch64-apple-ios-sim diff --git a/.github/workflows/publish-website.yml b/.github/workflows/publish-website.yml index 9ad2d143..ad8d476b 100644 --- a/.github/workflows/publish-website.yml +++ b/.github/workflows/publish-website.yml @@ -42,11 +42,22 @@ jobs: - name: Configure GitHub Pages uses: actions/configure-pages@v5 + - name: Verify static site dependency boundary + run: | + if cargo tree -p cargo-fission --target x86_64-unknown-linux-gnu -i libdbus-sys --locked | grep -q 'libdbus-sys'; then + echo "Static site commands must not pull libdbus-sys through the Fission command dependency graph." >&2 + exit 1 + fi + if cargo tree -p fission-command-site --locked | grep -E 'keyring|aws-sdk|dbus|secret-service'; then + echo "fission-command-site must remain independent of credentials and provider SDKs." >&2 + exit 1 + fi + - name: Check static site routes - run: cargo run -p fission-cli --bin fission -- site check --project-dir documentation --release + run: cargo run -p cargo-fission --bin fission -- site check --project-dir documentation --release - name: Package static site - run: cargo run -p fission-cli --bin fission -- package --project-dir documentation --target site --format static --release + run: cargo run -p cargo-fission --bin fission -- package --project-dir documentation --target site --format static --release - name: Verify generated static site run: | diff --git a/Cargo.lock b/Cargo.lock index ce9a006d..beee3770 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,6 +1126,20 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cargo-fission" +version = "0.1.1" +dependencies = [ + "anyhow", + "clap", + "fission-command-core", + "fission-command-package", + "fission-command-release", + "fission-command-run", + "fission-command-site", + "fission-command-ui", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2095,21 +2109,29 @@ dependencies = [ ] [[package]] -name = "fission-cli" +name = "fission-command-core" +version = "0.1.1" +dependencies = [ + "anyhow", + "clap", + "serde", + "toml", +] + +[[package]] +name = "fission-command-package" version = "0.1.1" dependencies = [ "anyhow", "aws-config", "aws-sdk-s3", - "base64", - "chacha20poly1305", "clap", - "fission", - "fission-shell-site", + "fission-command-core", + "fission-command-run", + "fission-command-site", + "fission-credentials", "flate2", - "getrandom 0.2.17", "jsonwebtoken", - "keyring", "reqwest", "serde", "serde_json", @@ -2117,10 +2139,60 @@ dependencies = [ "tar", "tokio", "toml", - "toml_edit 0.23.9", "zip", ] +[[package]] +name = "fission-command-release" +version = "0.1.1" +dependencies = [ + "anyhow", + "base64", + "clap", + "fission-command-core", + "fission-command-package", + "fission-command-ui", + "fission-credentials", + "jsonwebtoken", + "reqwest", + "serde", + "serde_json", + "sha2", + "toml", + "toml_edit 0.23.9", +] + +[[package]] +name = "fission-command-run" +version = "0.1.1" +dependencies = [ + "anyhow", + "fission-command-core", + "fission-command-site", + "serde", + "serde_json", +] + +[[package]] +name = "fission-command-site" +version = "0.1.1" +dependencies = [ + "anyhow", + "fission-shell-site", + "toml", +] + +[[package]] +name = "fission-command-ui" +version = "0.1.1" +dependencies = [ + "anyhow", + "fission", + "fission-command-core", + "fission-command-run", + "serde", +] + [[package]] name = "fission-core" version = "0.1.1" @@ -2143,6 +2215,20 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "fission-credentials" +version = "0.1.1" +dependencies = [ + "anyhow", + "base64", + "chacha20poly1305", + "fission-command-core", + "getrandom 0.2.17", + "keyring", + "serde", + "serde_json", +] + [[package]] name = "fission-design-system-codegen" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 6b18012e..970fcc20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,14 @@ members = [ "crates/shell/fission-shell-site", "crates/shell/fission-shell-terminal", "crates/tools/fission-diagnostics", - "crates/tools/fission-cli", + "crates/tools/cargo-fission", + "crates/tools/fission-command-package", + "crates/tools/fission-command-release", + "crates/tools/fission-command-run", + "crates/tools/fission-command-core", + "crates/tools/fission-command-site", + "crates/tools/fission-command-ui", + "crates/tools/fission-credentials", "crates/tools/fission-design-system-codegen", "crates/tools/fission-test", "crates/tools/fission-test-driver", diff --git a/README.md b/README.md index 4e620b26..c7630bac 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,20 @@ The project documentation and marketing site are now a Fission static site under Run locally: ```sh -cargo fission site serve --project-dir documentation +fission site serve --project-dir documentation ``` Build for deployment: ```sh -cargo fission site build --project-dir documentation +fission site build --project-dir documentation ``` Useful checks: ```sh -cargo fission site routes --project-dir documentation -cargo fission site check --project-dir documentation +fission site routes --project-dir documentation +fission site check --project-dir documentation ``` The static site supports custom widget routes, Markdown/MDX content routes, sidebars, generated table-of-contents navigation, copied assets, favicon support, light and dark themes, generated CSS, optional client-side search, optional code highlighting, sitemap and robots output, JSON-LD structured data, and internal-link validation. @@ -136,7 +136,7 @@ Fission now ships a first-party scaffolding CLI for the basic project lifecycle: fission init my-app # Cargo subcommand alias -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` The CLI currently does three things: @@ -642,7 +642,14 @@ fission/ │ │ ├── fission-shell-mobile/ # Mobile shell (iOS / Android) │ │ └── fission-shell-web/ # Web shell (WASM / browser) │ └── tools/ -│ ├── fission-cli/ # `fission` / `cargo fission` scaffolding CLI +│ ├── cargo-fission/ # Package that installs the `fission` command +│ ├── fission-command-core/ # Shared command models and project manifest helpers +│ ├── fission-command-run/ # Run, build, test, logs, and doctor workflows +│ ├── fission-command-site/ # Static site build/check/serve workflows +│ ├── fission-command-package/ # Packaging, readiness, and distribution workflows +│ ├── fission-command-release/ # Release metadata, auth, signing, beta, and review workflows +│ ├── fission-command-ui/ # Terminal UI for the same command model +│ ├── fission-credentials/ # Local credential vault helpers │ ├── fission-diagnostics/ # Structured diagnostic logging │ ├── fission-test/ # Test utilities │ └── fission-test-driver/ # LiveTestClient and test protocol @@ -734,7 +741,14 @@ For the iOS, Android, and browser smoke commands, see `docs/platform-smoke-tests | [`fission-shell-desktop`](crates/shell/fission-shell-desktop) | Desktop shell wrapper around the shared winit runtime | | [`fission-shell-mobile`](crates/shell/fission-shell-mobile) | Mobile shell (iOS / Android) -- simulator/emulator smoke paths verified | | [`fission-shell-web`](crates/shell/fission-shell-web) | Web shell (WASM + browser) -- checked-in browser smoke path and CLI scaffolding | -| [`fission-cli`](crates/tools/fission-cli) | Project scaffolding CLI and `cargo fission` entrypoint | +| [`cargo-fission`](crates/tools/cargo-fission) | Package that installs the single `fission` project command | +| [`fission-command-core`](crates/tools/fission-command-core) | Shared command models and project manifest helpers | +| [`fission-command-run`](crates/tools/fission-command-run) | Run, build, test, logs, and doctor workflows | +| [`fission-command-site`](crates/tools/fission-command-site) | Static site build/check/serve workflows | +| [`fission-command-package`](crates/tools/fission-command-package) | Packaging, readiness, and distribution workflows | +| [`fission-command-release`](crates/tools/fission-command-release) | Release metadata, auth, signing, beta, and review workflows | +| [`fission-command-ui`](crates/tools/fission-command-ui) | Terminal UI app for the same command model | +| [`fission-credentials`](crates/tools/fission-credentials) | Local credential vault helpers used by release/publish commands | | [`fission-diagnostics`](crates/tools/fission-diagnostics) | Structured diagnostic logging and performance tracing | | [`fission-test`](crates/tools/fission-test) | Test utilities and helpers | | [`fission-test-driver`](crates/tools/fission-test-driver) | LiveTestClient and JSON test protocol | diff --git a/crates/shell/fission-shell-web/README.md b/crates/shell/fission-shell-web/README.md index b560d0d2..a4ab497f 100644 --- a/crates/shell/fission-shell-web/README.md +++ b/crates/shell/fission-shell-web/README.md @@ -45,7 +45,7 @@ cargo install wasm-pack ./examples/web-smoke/platforms/web/run-browser.sh ``` -Build a generated app after `cargo fission add-target web`: +Build a generated app after `fission add-target web`: ```sh ./platforms/web/run-browser.sh diff --git a/crates/tools/cargo-fission/Cargo.toml b/crates/tools/cargo-fission/Cargo.toml new file mode 100644 index 00000000..bf6846d1 --- /dev/null +++ b/crates/tools/cargo-fission/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cargo-fission" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Project scaffolding CLI for Fission" + +[[bin]] +name = "fission" +path = "src/main.rs" + +[[bin]] +name = "cargo-fission" +path = "src/bin/cargo-fission.rs" + +[lib] +name = "fission_cli" +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +fission-command-package = { path = "../fission-command-package", version = "0.1.1" } +fission-command-release = { path = "../fission-command-release", version = "0.1.1" } +fission-command-run = { path = "../fission-command-run", version = "0.1.1" } +fission-command-site = { path = "../fission-command-site", version = "0.1.1" } +fission-command-ui = { path = "../fission-command-ui", version = "0.1.1" } diff --git a/crates/tools/fission-cli/README.md b/crates/tools/cargo-fission/README.md similarity index 74% rename from crates/tools/fission-cli/README.md rename to crates/tools/cargo-fission/README.md index 13247447..ec38d611 100644 --- a/crates/tools/fission-cli/README.md +++ b/crates/tools/cargo-fission/README.md @@ -1,6 +1,6 @@ -# fission-cli +# Fission command -`fission-cli` provides the first-party Fission project scaffolding commands: +This Cargo package installs the first-party `fission` command: - `fission init` - `fission add-target` @@ -10,7 +10,7 @@ - `fission build` - `fission test` - `fission logs` -- `cargo fission ...` +- `fission ...` ## Usage @@ -37,44 +37,44 @@ fission init my-app --local-path /path/to/fission Add more platform targets: ```sh -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` Check local SDKs, emulators, browsers, and Rust targets: ```sh -cargo fission doctor web ios android --project-dir my-app +fission doctor web ios android --project-dir my-app ``` List runnable devices and targets: ```sh -cargo fission devices --project-dir my-app -cargo fission devices --project-dir my-app --json +fission devices --project-dir my-app +fission devices --project-dir my-app --json ``` Run and attach to app output/logs: ```sh -cargo fission run --project-dir my-app -cargo fission run --project-dir my-app --target web -cargo fission run --project-dir my-app --target android --device emulator-5554 -cargo fission run --project-dir my-app --target ios --device +fission run --project-dir my-app +fission run --project-dir my-app --target web +fission run --project-dir my-app --target android --device emulator-5554 +fission run --project-dir my-app --target ios --device ``` `fission run` attaches by default. Use `--detach` to start the app and return, then use `fission logs` to attach later where the platform supports it: ```sh -cargo fission run --project-dir my-app --target web --detach -cargo fission logs --project-dir my-app --target web --follow +fission run --project-dir my-app --target web --detach +fission logs --project-dir my-app --target web --follow ``` Build or run smoke tests without launching the full attached workflow: ```sh -cargo fission build --project-dir my-app --target web --release -cargo fission test --project-dir my-app --target web -cargo fission test --project-dir my-app --target ios --headless +fission build --project-dir my-app --target web --release +fission test --project-dir my-app --target web +fission test --project-dir my-app --target ios --headless ``` ## Current platform status diff --git a/crates/tools/fission-cli/src/bin/cargo-fission.rs b/crates/tools/cargo-fission/src/bin/cargo-fission.rs similarity index 100% rename from crates/tools/fission-cli/src/bin/cargo-fission.rs rename to crates/tools/cargo-fission/src/bin/cargo-fission.rs diff --git a/crates/tools/fission-cli/src/cli.rs b/crates/tools/cargo-fission/src/cli.rs similarity index 95% rename from crates/tools/fission-cli/src/cli.rs rename to crates/tools/cargo-fission/src/cli.rs index d951bc64..2d6c50e8 100644 --- a/crates/tools/fission-cli/src/cli.rs +++ b/crates/tools/cargo-fission/src/cli.rs @@ -1,5 +1,7 @@ -use crate::{publish, release, Target}; use clap::{Parser, Subcommand}; +use fission_command_core::{DistributionProvider, Target}; +use fission_command_package as package; +use fission_command_release as release; use std::path::PathBuf; #[derive(Parser, Debug)] @@ -124,7 +126,7 @@ pub(crate) enum Command { target: Target, /// Package format. #[arg(long, value_enum)] - format: publish::PackageFormat, + format: package::PackageFormat, /// Project directory; defaults to the current working directory. #[arg(long, default_value = ".")] project_dir: PathBuf, @@ -139,10 +141,10 @@ pub(crate) enum Command { Distribute { /// Lifecycle action; defaults to publish. #[arg(value_enum)] - action: Option, + action: Option, /// Distribution provider. #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, /// Artifact manifest emitted by `fission package`. #[arg(long)] artifact: Option, @@ -172,16 +174,16 @@ pub(crate) enum Command { Readiness { /// Readiness area to check. #[arg(value_enum)] - kind: publish::ReadinessKind, + kind: package::ReadinessKind, /// Target to package/check. #[arg(long, value_enum)] target: Option, /// Package format. #[arg(long, value_enum)] - format: Option, + format: Option, /// Distribution provider. #[arg(long, value_enum)] - provider: Option, + provider: Option, /// Artifact manifest emitted by `fission package`. #[arg(long)] artifact: Option, @@ -248,7 +250,7 @@ pub(crate) enum Command { #[arg(long)] follow: bool, }, - /// Open the interactive Fission CLI terminal UI. + /// Open the interactive Fission command terminal UI. Ui { /// Project directory; defaults to the current working directory. #[arg(long, default_value = ".")] diff --git a/crates/tools/fission-cli/src/lib.rs b/crates/tools/cargo-fission/src/lib.rs similarity index 84% rename from crates/tools/fission-cli/src/lib.rs rename to crates/tools/cargo-fission/src/lib.rs index a51e0e64..4ec125be 100644 --- a/crates/tools/fission-cli/src/lib.rs +++ b/crates/tools/cargo-fission/src/lib.rs @@ -3,16 +3,9 @@ use clap::Parser; use std::path::Path; mod cli; -mod doctor; -mod project; -mod publish; -mod release; -mod ui; -mod workflow; -pub(crate) use project::{ - cargo_package_name, ios_executable_name, read_project_config, FissionProject, Target, -}; +#[cfg(test)] +use fission_command_core::{read_project_config, Target}; use cli::{Cli, Command, SiteCommand}; @@ -25,7 +18,7 @@ where if let Some(bin) = argv.first() { if let Some(name) = Path::new(bin).file_name().and_then(|value| value.to_str()) { if name == "cargo-fission" { - argv[0] = std::ffi::OsString::from("cargo fission"); + argv[0] = std::ffi::OsString::from("fission"); if argv.get(1).and_then(|value| value.to_str()) == Some("fission") { argv.remove(1); } @@ -39,17 +32,19 @@ where name, app_id, local_path, - } => project::init_project(&path, name, app_id, local_path), + } => fission_command_core::init_project(&path, name, app_id, local_path), Command::AddTarget { targets, project_dir, - } => project::add_targets(&project_dir, &targets), + } => fission_command_core::add_targets(&project_dir, &targets), Command::Doctor { targets, project_dir, strict, - } => doctor::run_doctor(&project_dir, &targets, strict), - Command::Devices { project_dir, json } => workflow::list_devices(&project_dir, json), + } => fission_command_run::doctor::run_doctor(&project_dir, &targets, strict), + Command::Devices { project_dir, json } => { + fission_command_run::list_devices(&project_dir, json) + } Command::Run { target, device, @@ -60,7 +55,7 @@ where port, no_open, headless, - } => workflow::run_app(workflow::RunOptions { + } => fission_command_run::run_app(fission_command_run::RunOptions { project_dir, target, device, @@ -75,7 +70,7 @@ where target, project_dir, release, - } => workflow::build_app(workflow::BuildOptions { + } => fission_command_run::build_app(fission_command_run::BuildOptions { project_dir, target, release, @@ -84,7 +79,7 @@ where target, project_dir, headless, - } => workflow::test_app(workflow::TestOptions { + } => fission_command_run::test_app(fission_command_run::TestOptions { project_dir, target, headless, @@ -93,19 +88,19 @@ where SiteCommand::Build { project_dir, release, - } => workflow::site_build(&project_dir, release), + } => fission_command_site::build(&project_dir, release), SiteCommand::Check { project_dir, release, - } => workflow::site_check(&project_dir, release), + } => fission_command_site::check(&project_dir, release), SiteCommand::Serve { project_dir, host, port, release, no_open, - } => workflow::site_serve(&project_dir, release, host, port, !no_open), - SiteCommand::Routes { project_dir } => workflow::site_routes(&project_dir), + } => fission_command_site::serve(&project_dir, release, host, port, !no_open), + SiteCommand::Routes { project_dir } => fission_command_site::routes(&project_dir), }, Command::Package { target, @@ -113,7 +108,7 @@ where project_dir, release, json, - } => publish::package(publish::PackageOptions { + } => fission_command_package::package(fission_command_package::PackageOptions { project_dir, target, format, @@ -131,10 +126,10 @@ where yes, project_dir, json, - } => publish::distribute(publish::DistributeOptions { + } => fission_command_package::distribute(fission_command_package::DistributeOptions { project_dir, provider, - action: action.unwrap_or(publish::DistributeAction::Publish), + action: action.unwrap_or(fission_command_package::DistributeAction::Publish), artifact, site, deploy, @@ -153,7 +148,7 @@ where track, project_dir, json, - } => publish::readiness(publish::ReadinessOptions { + } => fission_command_package::readiness(fission_command_package::ReadinessOptions { project_dir, kind, target, @@ -164,19 +159,19 @@ where track, json, }), - Command::ReleaseConfig { command } => release::release_config(command), - Command::ReleaseContent { command } => release::release_content(command), - Command::Beta { command } => release::beta(command), - Command::Signing { command } => release::signing(command), - Command::Reviews { command } => release::reviews(command), - Command::ReleaseWorkflow { command } => release::release_workflow(command), - Command::Auth { command } => release::auth(command), + Command::ReleaseConfig { command } => fission_command_release::release_config(command), + Command::ReleaseContent { command } => fission_command_release::release_content(command), + Command::Beta { command } => fission_command_release::beta(command), + Command::Signing { command } => fission_command_release::signing(command), + Command::Reviews { command } => fission_command_release::reviews(command), + Command::ReleaseWorkflow { command } => fission_command_release::release_workflow(command), + Command::Auth { command } => fission_command_release::auth(command), Command::Logs { target, device, project_dir, follow, - } => workflow::attach_logs(workflow::LogOptions { + } => fission_command_run::attach_logs(fission_command_run::LogOptions { project_dir, target, device, @@ -188,7 +183,7 @@ where exit_after_render, width, height, - } => ui::run_ui(ui::UiOptions { + } => fission_command_ui::run_ui(fission_command_ui::UiOptions { project_dir, screenshot, exit_after_render, @@ -200,7 +195,7 @@ where host, port, open, - } => workflow::serve_web(workflow::ServeWebOptions { + } => fission_command_run::serve_web(fission_command_run::ServeWebOptions { project_dir, host, port, @@ -215,7 +210,8 @@ mod tests { use std::{fs, path::PathBuf}; fn unique_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("fission-cli-{}-{}", name, std::process::id())); + let dir = + std::env::temp_dir().join(format!("cargo-fission-{}-{}", name, std::process::id())); let _ = fs::remove_dir_all(&dir); dir } @@ -242,11 +238,11 @@ mod tests { assert!(dir.join("platforms/macos/README.md").exists()); assert!(dir.join("platforms/linux/README.md").exists()); let readme = std::fs::read_to_string(dir.join("README.md")).unwrap(); - assert!(readme.contains("cargo fission devices --project-dir .")); - assert!(readme.contains("cargo fission run --project-dir .")); - assert!(readme.contains("cargo fission logs --target ")); - assert!(readme.contains("cargo fission build --target ")); - assert!(readme.contains("cargo fission test --target ")); + assert!(readme.contains("fission devices --project-dir .")); + assert!(readme.contains("fission run --project-dir .")); + assert!(readme.contains("fission logs --target ")); + assert!(readme.contains("fission build --target ")); + assert!(readme.contains("fission test --target ")); let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap(); assert!(manifest.contains("default-features = false")); assert!(manifest.contains("features = [\"desktop\"]")); @@ -311,11 +307,11 @@ mod tests { let android_run_script = std::fs::read_to_string(dir.join("platforms/android/run-emulator.sh")).unwrap(); assert!(android_run_script.contains("ANDROID_EMULATOR_API_LEVEL")); - assert!(android_run_script.contains("cargo fission doctor android")); + assert!(android_run_script.contains("fission doctor android")); assert!( std::fs::read_to_string(dir.join("platforms/android/README.md")) .unwrap() - .contains("cargo fission run --target android") + .contains("fission run --target android") ); let android_test_script = std::fs::read_to_string(dir.join("platforms/android/test-emulator.sh")).unwrap(); @@ -337,7 +333,7 @@ mod tests { )); assert!(std::fs::read_to_string(dir.join("platforms/ios/README.md")) .unwrap() - .contains("cargo fission run --target ios")); + .contains("fission run --target ios")); assert!( std::fs::read_to_string(dir.join("platforms/ios/test-sim.sh")) .unwrap() @@ -360,7 +356,7 @@ mod tests { assert!(web_test_script.contains("/json/list")); assert!(std::fs::read_to_string(dir.join("platforms/web/README.md")) .unwrap() - .contains("cargo fission run --target web")); + .contains("fission run --target web")); } #[test] diff --git a/crates/tools/fission-cli/src/main.rs b/crates/tools/cargo-fission/src/main.rs similarity index 100% rename from crates/tools/fission-cli/src/main.rs rename to crates/tools/cargo-fission/src/main.rs diff --git a/crates/tools/fission-cli/Cargo.toml b/crates/tools/fission-cli/Cargo.toml deleted file mode 100644 index c34c93a1..00000000 --- a/crates/tools/fission-cli/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "fission-cli" -version = "0.1.1" -edition = "2021" -license = "MIT" -repository = "https://github.com/worka-ai/fission" -description = "Project scaffolding CLI for Fission" - -[[bin]] -name = "fission" -path = "src/main.rs" - -[[bin]] -name = "cargo-fission" -path = "src/bin/cargo-fission.rs" - -[dependencies] -anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } -flate2 = "1.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -sha2 = "0.10" -tar = "0.4" -toml = "0.8" -toml_edit = "0.23" -fission = { path = "../../authoring/fission", version = "0.1.1", default-features = false, features = ["terminal-shell"] } -fission-shell-site = { path = "../../shell/fission-shell-site", version = "0.1.1" } -base64 = "0.22" -chacha20poly1305 = "0.10" -getrandom = { version = "0.2", features = ["std"] } -keyring = { version = "3", default-features = false, features = ["apple-native", "windows-native", "linux-native", "crypto-rust"] } -reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] } -aws-config = "1" -aws-sdk-s3 = "1" -tokio = { version = "1", features = ["rt", "rt-multi-thread"] } -jsonwebtoken = "9" -zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/crates/tools/fission-cli/src/ui/components/mod.rs b/crates/tools/fission-cli/src/ui/components/mod.rs deleted file mode 100644 index 77bc53fb..00000000 --- a/crates/tools/fission-cli/src/ui/components/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod chrome; -mod controls; -mod data; -mod dialog; -mod output; - -pub(crate) use chrome::AppShell; -pub(crate) use controls::{ActionButton, ButtonTone, FormTextField, TogglePill}; -pub(crate) use data::{DeviceTable, KeyValueRow, TargetPicker}; -pub(crate) use dialog::ConfirmationDialog; -pub(crate) use output::OutputPanel; diff --git a/crates/tools/fission-command-core/Cargo.toml b/crates/tools/fission-command-core/Cargo.toml new file mode 100644 index 00000000..2c44a8cf --- /dev/null +++ b/crates/tools/fission-command-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fission-command-core" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Shared project and target model for the Fission command" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" diff --git a/crates/tools/fission-cli/src/project.rs b/crates/tools/fission-command-core/src/lib.rs similarity index 90% rename from crates/tools/fission-cli/src/project.rs rename to crates/tools/fission-command-core/src/lib.rs index 162cb589..547b2b84 100644 --- a/crates/tools/fission-cli/src/project.rs +++ b/crates/tools/fission-command-core/src/lib.rs @@ -10,7 +10,7 @@ const DEFAULT_APP_ICON_PNG: &[u8] = include_bytes!("../../../../docs/fission_log #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, ValueEnum, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub(crate) enum Target { +pub enum Target { Android, Ios, Linux, @@ -21,7 +21,7 @@ pub(crate) enum Target { } impl Target { - pub(crate) fn as_str(self) -> &'static str { + pub fn as_str(self) -> &'static str { match self { Self::Android => "android", Self::Ios => "ios", @@ -33,7 +33,7 @@ impl Target { } } - pub(crate) fn scaffold_relative_path(self) -> &'static str { + pub fn scaffold_relative_path(self) -> &'static str { match self { Self::Android => "platforms/android/README.md", Self::Ios => "platforms/ios/README.md", @@ -46,16 +46,57 @@ impl Target { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] +pub enum DistributionProvider { + #[value(name = "app-store")] + AppStore, + #[value(name = "github-pages")] + GithubPages, + #[value(name = "github-releases")] + GithubReleases, + #[value(name = "cloudflare-pages")] + CloudflarePages, + Dropbox, + #[value(name = "google-drive")] + GoogleDrive, + #[value(name = "microsoft-store")] + MicrosoftStore, + Netlify, + #[value(name = "onedrive")] + OneDrive, + #[value(name = "play-store")] + PlayStore, + S3, +} + +impl DistributionProvider { + pub fn as_str(self) -> &'static str { + match self { + Self::AppStore => "app-store", + Self::GithubPages => "github-pages", + Self::GithubReleases => "github-releases", + Self::CloudflarePages => "cloudflare-pages", + Self::Dropbox => "dropbox", + Self::GoogleDrive => "google-drive", + Self::MicrosoftStore => "microsoft-store", + Self::Netlify => "netlify", + Self::OneDrive => "onedrive", + Self::PlayStore => "play-store", + Self::S3 => "s3", + } + } +} + #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct FissionProject { - pub(crate) app: AppConfig, - pub(crate) targets: BTreeSet, +pub struct FissionProject { + pub app: AppConfig, + pub targets: BTreeSet, } #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct AppConfig { - pub(crate) name: String, - pub(crate) app_id: String, +pub struct AppConfig { + pub name: String, + pub app_id: String, } #[derive(Debug, Deserialize)] @@ -65,7 +106,7 @@ struct CargoManifest { #[derive(Debug, Deserialize)] struct CargoPackage { - pub(crate) name: String, + pub name: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -74,7 +115,7 @@ enum WritePolicy { PreserveExisting, } -pub(crate) fn init_project( +pub fn init_project( root: &Path, name: Option, app_id: Option, @@ -178,7 +219,7 @@ fn initial_project_config( }) } -pub(crate) fn cargo_package_name(root: &Path) -> Option { +pub fn cargo_package_name(root: &Path) -> Option { let manifest = fs::read_to_string(root.join("Cargo.toml")).ok()?; let manifest: CargoManifest = toml::from_str(&manifest).ok()?; manifest.package.map(|package| package.name) @@ -205,7 +246,7 @@ fn detect_project_targets(root: &Path) -> BTreeSet { targets } -pub(crate) fn add_targets(project_dir: &Path, targets: &[Target]) -> Result<()> { +pub fn add_targets(project_dir: &Path, targets: &[Target]) -> Result<()> { if targets.is_empty() { bail!("no targets provided"); } @@ -242,7 +283,7 @@ fn write_project_config(root: &Path, project: &FissionProject) -> Result<()> { write_file(&root.join("fission.toml"), &(data + "\n")) } -pub(crate) fn read_project_config(root: &Path) -> Result { +pub fn read_project_config(root: &Path) -> Result { let path = root.join("fission.toml"); let data = fs::read_to_string(&path).with_context(|| { format!( @@ -364,11 +405,11 @@ fn scaffold_target_with_policy( "Runnable emulator target. The CLI generates a NativeActivity manifest plus shell scripts that build, install, and launch the Fission app on an Android emulator.", &[ "Install the Rust target: `rustup target add aarch64-linux-android`.", - "Run `cargo fission doctor android --project-dir .` to check SDK, NDK, emulator, and Rust target setup.", - "Run `cargo fission devices --project-dir .` to list connected Android devices and configured emulators.", - "Run `cargo fission run --target android --project-dir .` to build, install, launch, and attach to logs.", - "Run `cargo fission run --target android --device --project-dir .` to launch on a specific device.", - "Run `cargo fission test --target android --project-dir .` for an emulator launch plus test-control health check.", + "Run `fission doctor android --project-dir .` to check SDK, NDK, emulator, and Rust target setup.", + "Run `fission devices --project-dir .` to list connected Android devices and configured emulators.", + "Run `fission run --target android --project-dir .` to build, install, launch, and attach to logs.", + "Run `fission run --target android --device --project-dir .` to launch on a specific device.", + "Run `fission test --target android --project-dir .` for an emulator launch plus test-control health check.", "Run `./platforms/android/run-emulator.sh` from the project root to build, package, install, and launch the app on the configured emulator.", "Override `ANDROID_HOME`, `ANDROID_NDK`, `ANDROID_MIN_API_LEVEL`, `ANDROID_TARGET_API_LEVEL`, `ANDROID_AVD_NAME`, or `ANDROID_SYSTEM_IMAGE` if your local SDK setup differs.", "Set `ANDROID_EMULATOR_HEADLESS=1` for background/CI runs, or `ANDROID_EMULATOR_RESTART=1` to relaunch a hidden emulator visibly.", @@ -384,12 +425,12 @@ fn scaffold_target_with_policy( "Simulator target. The CLI generates a simulator app bundle template plus shell scripts that build, install, launch, and smoke-test the Fission app with `simctl`.", &[ "Install the Rust targets: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim`.", - "Run `cargo fission doctor ios --project-dir .` to check Xcode, simulator, and Rust target setup.", + "Run `fission doctor ios --project-dir .` to check Xcode, simulator, and Rust target setup.", "Confirm the simulator SDK path with `xcrun --sdk iphonesimulator --show-sdk-path`.", - "Run `cargo fission devices --project-dir .` to list available iOS simulators.", - "Run `cargo fission run --target ios --project-dir .` to build, install, launch, and attach to simulator logs.", - "Run `cargo fission run --target ios --device --project-dir .` to launch on a specific simulator.", - "Run `cargo fission test --target ios --project-dir .` for a simulator launch plus test-control health check.", + "Run `fission devices --project-dir .` to list available iOS simulators.", + "Run `fission run --target ios --project-dir .` to build, install, launch, and attach to simulator logs.", + "Run `fission run --target ios --device --project-dir .` to launch on a specific simulator.", + "Run `fission test --target ios --project-dir .` for a simulator launch plus test-control health check.", "Run `./platforms/ios/run-sim.sh` from the project root to build, install, and launch the app on the first available iPhone simulator.", "The generated bundle uses `assets/app-icon.png` as its default app icon.", "Set `FISSION_TEST_CONTROL_PORT=` before `run-sim.sh` to expose the in-app test control server on the host.", @@ -407,11 +448,11 @@ fn scaffold_target_with_policy( "Install the Rust target: `rustup target add wasm32-unknown-unknown`.", "Install `wasm-pack` once: `cargo install wasm-pack`.", "Install Node.js 22+ so the smoke test can inspect Chrome/Chromium CDP runtime and console output.", - "Run `cargo fission doctor web --project-dir .` to check wasm-pack, Node.js, Chrome/Chromium, and Rust target setup.", - "Run `cargo fission devices --project-dir .` to confirm Chrome/Chromium detection.", - "Run `cargo fission run --target web --project-dir .` to build, serve, open, and attach to the local server.", - "Run `cargo fission run --target web --detach --project-dir .` to keep the local server running in the background.", - "Run `cargo fission test --target web --project-dir .` for a headless Chrome/Chromium CDP smoke test.", + "Run `fission doctor web --project-dir .` to check wasm-pack, Node.js, Chrome/Chromium, and Rust target setup.", + "Run `fission devices --project-dir .` to confirm Chrome/Chromium detection.", + "Run `fission run --target web --project-dir .` to build, serve, open, and attach to the local server.", + "Run `fission run --target web --detach --project-dir .` to keep the local server running in the background.", + "Run `fission test --target web --project-dir .` for a headless Chrome/Chromium CDP smoke test.", "Run `./platforms/web/run-browser.sh` from the project root to build the wasm package and serve the app locally.", "Set `FISSION_WEB_PORT=` or `FISSION_WEB_HOST=` if the default `127.0.0.1:8123` does not suit your machine.", "Set `FISSION_WEB_OPEN=1` if you want the helper script to open a browser tab automatically.", @@ -422,7 +463,7 @@ fn scaffold_target_with_policy( Target::Site => { write_file_with_policy( &root.join("content/getting-started.md"), - "---\ntitle: Site content\ndescription: Static site content rendered by the Fission static site shell.\n---\n\n# Site content\n\nAdd Markdown files under `content/`. `cargo fission site build` renders them through real Fission widgets, lowers the nodes to Core IR, and emits static HTML.\n", + "---\ntitle: Site content\ndescription: Static site content rendered by the Fission static site shell.\n---\n\n# Site content\n\nAdd Markdown files under `content/`. `fission site build` renders them through real Fission widgets, lowers the nodes to Core IR, and emits static HTML.\n", write_policy, )?; platform_readme( @@ -430,9 +471,9 @@ fn scaffold_target_with_policy( "Static multi-page website target. The site shell renders Markdown content through real Fission widgets, lowers nodes to Core IR, and emits semantic static HTML.", &[ "Add Markdown or MDX content under `content/`.", - "Run `cargo fission site routes --project-dir .` to list generated routes.", - "Run `cargo fission site build --project-dir .` to render HTML into `target/fission/site`.", - "Run `cargo fission site serve --project-dir .` to build and serve the generated site locally.", + "Run `fission site routes --project-dir .` to list generated routes.", + "Run `fission site build --project-dir .` to render HTML into `target/fission/site`.", + "Run `fission site serve --project-dir .` to build and serve the generated site locally.", "Unsupported interactive widgets fail during the static render instead of silently falling back to JavaScript.", ], ) @@ -446,9 +487,9 @@ fn scaffold_target_with_policy( }, "Runnable target. Desktop platforms share the default `src/main.rs` entrypoint through `DesktopApp`.", &[ - "Run `cargo fission run --project-dir .` from the project root to launch the desktop app and attach output.", - "Run `cargo fission build --project-dir . --release` for a release desktop build.", - "Run `cargo fission test --project-dir .` for the app crate's Rust tests.", + "Run `fission run --project-dir .` from the project root to launch the desktop app and attach output.", + "Run `fission build --project-dir . --release` for a release desktop build.", + "Run `fission test --project-dir .` for the app crate's Rust tests.", "This target uses the default Vello desktop shell path.", ], ), @@ -693,7 +734,7 @@ fn render_project_readme(project: &FissionProject) -> String { targets.push_str(&format!("- `{}`\n", target.as_str())); } format!( - "# {}\n\nGenerated by `fission init`.\n\n## Targets\n\n{}\n## Commands\n\n- `cargo fission doctor --project-dir .` -- check local SDKs, browsers, emulators, and Rust targets\n- `cargo fission devices --project-dir .` -- list runnable desktop, browser, simulator, emulator, and device targets\n- `cargo fission run --project-dir .` -- launch the desktop app and attach to output\n- `cargo fission run --target web --project-dir .` -- launch the web app and attach to the local server\n- `cargo fission run --target ios --project-dir .` -- build, install, launch, and attach to simulator logs\n- `cargo fission run --target android --project-dir .` -- build, install, launch, and attach to Android logs\n- `cargo fission run --target --device --detach --project-dir .` -- launch without attaching\n- `cargo fission logs --target --device --project-dir . --follow` -- attach later where supported\n- `cargo fission build --target --project-dir . --release` -- build a target without launching it\n- `cargo fission test --target --project-dir .` -- run the generated platform smoke test\n- `cargo fission add-target web ios android --project-dir .` -- scaffold more targets\n- `cat platforms//README.md` -- inspect target-specific prerequisites and environment variables\n\n## Assets\n\n- `assets/app-icon.png` is the default app icon seed copied from Fission's `docs/fission_logo.png`\n\n## Status\n\nDesktop, web, iOS simulator, and Android emulator workflows are runnable through `cargo fission run`. The platform scripts remain checked in so CI and advanced users can call the lower-level build, run, and smoke-test steps directly when needed.\n", + "# {}\n\nGenerated by `fission init`.\n\n## Targets\n\n{}\n## Commands\n\n- `fission doctor --project-dir .` -- check local SDKs, browsers, emulators, and Rust targets\n- `fission devices --project-dir .` -- list runnable desktop, browser, simulator, emulator, and device targets\n- `fission run --project-dir .` -- launch the desktop app and attach to output\n- `fission run --target web --project-dir .` -- launch the web app and attach to the local server\n- `fission run --target ios --project-dir .` -- build, install, launch, and attach to simulator logs\n- `fission run --target android --project-dir .` -- build, install, launch, and attach to Android logs\n- `fission run --target --device --detach --project-dir .` -- launch without attaching\n- `fission logs --target --device --project-dir . --follow` -- attach later where supported\n- `fission build --target --project-dir . --release` -- build a target without launching it\n- `fission test --target --project-dir .` -- run the generated platform smoke test\n- `fission add-target web ios android --project-dir .` -- scaffold more targets\n- `cat platforms//README.md` -- inspect target-specific prerequisites and environment variables\n\n## Assets\n\n- `assets/app-icon.png` is the default app icon seed copied from Fission's `docs/fission_logo.png`\n\n## Status\n\nDesktop, web, iOS simulator, and Android emulator workflows are runnable through `fission run`. The platform scripts remain checked in so CI and advanced users can call the lower-level build, run, and smoke-test steps directly when needed.\n", project.app.name, targets ) } @@ -719,7 +760,7 @@ fn normalize_crate_name(name: &str) -> String { .to_string() } -pub(crate) fn ios_executable_name(project: &FissionProject) -> String { +pub fn ios_executable_name(project: &FissionProject) -> String { project.app.name.replace('-', "_") } @@ -1213,7 +1254,7 @@ RESTART_EMULATOR="${{ANDROID_EMULATOR_RESTART:-0}}" for tool in "$ADB" "$EMULATOR_BIN" "$AVDMANAGER"; do if [[ ! -x "$tool" ]]; then - printf 'Required Android tool is missing or not executable: %s\nRun `cargo fission doctor android --project-dir .` for setup help.\n' "$tool" >&2 + printf 'Required Android tool is missing or not executable: %s\nRun `fission doctor android --project-dir .` for setup help.\n' "$tool" >&2 exit 1 fi done @@ -1497,7 +1538,7 @@ raise SystemExit(f"web server did not serve {url}: {last_error}") PY CHROME=$(detect_chrome) || { - printf 'Chrome/Chromium was not found. Set FISSION_CHROME=/path/to/chrome or run `cargo fission doctor web --project-dir .`.\n' >&2 + printf 'Chrome/Chromium was not found. Set FISSION_CHROME=/path/to/chrome or run `fission doctor web --project-dir .`.\n' >&2 exit 1 } diff --git a/crates/tools/fission-command-package/Cargo.toml b/crates/tools/fission-command-package/Cargo.toml new file mode 100644 index 00000000..4f694e0f --- /dev/null +++ b/crates/tools/fission-command-package/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "fission-command-package" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Packaging, readiness, and distribution workflows for the Fission command" + +[dependencies] +anyhow = "1.0" +aws-config = "1" +aws-sdk-s3 = "1" +clap = { version = "4.5", features = ["derive"] } +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +fission-command-run = { path = "../fission-command-run", version = "0.1.1" } +fission-command-site = { path = "../fission-command-site", version = "0.1.1" } +fission-credentials = { path = "../fission-credentials", version = "0.1.1" } +flate2 = "1.0" +jsonwebtoken = "9" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +tar = "0.4" +tokio = { version = "1", features = ["rt", "rt-multi-thread"] } +toml = "0.8" +zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/crates/tools/fission-cli/src/publish/files.rs b/crates/tools/fission-command-package/src/files.rs similarity index 96% rename from crates/tools/fission-cli/src/publish/files.rs rename to crates/tools/fission-command-package/src/files.rs index fc368c98..775b2d7c 100644 --- a/crates/tools/fission-cli/src/publish/files.rs +++ b/crates/tools/fission-command-package/src/files.rs @@ -1,9 +1,9 @@ use super::*; -use crate::release; use anyhow::{bail, Context, Result}; use aws_config::{BehaviorVersion, Region}; use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::types::ObjectCannedAcl; +use fission_credentials as credentials; use reqwest::blocking::Client; use reqwest::header::{CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, LOCATION}; use serde_json::{json, Value}; @@ -56,7 +56,7 @@ pub(super) fn publish_google_drive( manifest: &ArtifactManifest, ) -> Result { let cfg = google_drive_config(config, &options.site)?; - let token = release::provider_secret( + let token = credentials::provider_secret( DistributionProvider::GoogleDrive, &["GOOGLE_DRIVE_ACCESS_TOKEN"], )? @@ -84,10 +84,10 @@ pub(super) fn publish_onedrive( ) -> Result { let cfg = onedrive_config(config, &options.site)?; let token = - release::provider_secret(DistributionProvider::OneDrive, &["ONEDRIVE_ACCESS_TOKEN"])? + credentials::provider_secret(DistributionProvider::OneDrive, &["ONEDRIVE_ACCESS_TOKEN"])? .context( - "OneDrive upload requires ONEDRIVE_ACCESS_TOKEN or a stored onedrive credential", - )?; + "OneDrive upload requires ONEDRIVE_ACCESS_TOKEN or a stored onedrive credential", + )?; let client = Client::new(); let mut uploaded = Vec::new(); for item in upload_items(manifest, artifact_path)? { @@ -110,8 +110,11 @@ pub(super) fn publish_dropbox( manifest: &ArtifactManifest, ) -> Result { let cfg = dropbox_config(config, &options.site)?; - let token = release::provider_secret(DistributionProvider::Dropbox, &["DROPBOX_ACCESS_TOKEN"])? - .context("Dropbox upload requires DROPBOX_ACCESS_TOKEN or a stored dropbox credential")?; + let token = + credentials::provider_secret(DistributionProvider::Dropbox, &["DROPBOX_ACCESS_TOKEN"])? + .context( + "Dropbox upload requires DROPBOX_ACCESS_TOKEN or a stored dropbox credential", + )?; let client = Client::new(); let mut uploaded = Vec::new(); for item in upload_items(manifest, artifact_path)? { @@ -151,7 +154,7 @@ pub(super) fn google_drive_status( config: &PublishManifest, ) -> Result { let cfg = google_drive_config(config, &options.site)?; - let token = release::provider_secret( + let token = credentials::provider_secret( DistributionProvider::GoogleDrive, &["GOOGLE_DRIVE_ACCESS_TOKEN"], )? @@ -191,10 +194,10 @@ pub(super) fn onedrive_status( ) -> Result { let cfg = onedrive_config(config, &options.site)?; let token = - release::provider_secret(DistributionProvider::OneDrive, &["ONEDRIVE_ACCESS_TOKEN"])? + credentials::provider_secret(DistributionProvider::OneDrive, &["ONEDRIVE_ACCESS_TOKEN"])? .context( - "OneDrive status requires ONEDRIVE_ACCESS_TOKEN or a stored onedrive credential", - )?; + "OneDrive status requires ONEDRIVE_ACCESS_TOKEN or a stored onedrive credential", + )?; let root = cfg .root .as_deref() @@ -234,8 +237,11 @@ pub(super) fn dropbox_status( options: &DistributeOptions, _config: &PublishManifest, ) -> Result { - let token = release::provider_secret(DistributionProvider::Dropbox, &["DROPBOX_ACCESS_TOKEN"])? - .context("Dropbox status requires DROPBOX_ACCESS_TOKEN or a stored dropbox credential")?; + let token = + credentials::provider_secret(DistributionProvider::Dropbox, &["DROPBOX_ACCESS_TOKEN"])? + .context( + "Dropbox status requires DROPBOX_ACCESS_TOKEN or a stored dropbox credential", + )?; let response = Client::new() .post("https://api.dropboxapi.com/2/users/get_current_account") .bearer_auth(token.trim()) @@ -846,7 +852,7 @@ fn secret_check( let found_env = env_names .iter() .find(|name| std::env::var_os(name).is_some()); - let found_secret = release::provider_secret(provider, env_names) + let found_secret = credentials::provider_secret(provider, env_names) .ok() .flatten() .is_some(); diff --git a/crates/tools/fission-cli/src/publish/github_releases.rs b/crates/tools/fission-command-package/src/github_releases.rs similarity index 98% rename from crates/tools/fission-cli/src/publish/github_releases.rs rename to crates/tools/fission-command-package/src/github_releases.rs index bccf955d..a8410799 100644 --- a/crates/tools/fission-cli/src/publish/github_releases.rs +++ b/crates/tools/fission-command-package/src/github_releases.rs @@ -1,6 +1,6 @@ use super::*; -use crate::release; use anyhow::{bail, Context, Result}; +use fission_credentials as credentials; use serde_json::{json, Value}; use std::env; use std::fs; @@ -453,7 +453,7 @@ fn require_gh_authenticated(project_dir: &Path) -> Result<()> { fn gh_auth_available(project_dir: &Path) -> bool { env::var_os("GH_TOKEN").is_some() || env::var_os("GITHUB_TOKEN").is_some() - || release::provider_secret(DistributionProvider::GithubReleases, &[]) + || credentials::provider_secret(DistributionProvider::GithubReleases, &[]) .ok() .flatten() .is_some() @@ -492,7 +492,8 @@ fn run_gh_owned(project_dir: &Path, args: &[String]) -> Result { fn gh_env() -> Vec<(&'static str, String)> { let mut envs = Vec::new(); if env::var_os("GH_TOKEN").is_none() && env::var_os("GITHUB_TOKEN").is_none() { - if let Ok(Some(token)) = release::provider_secret(DistributionProvider::GithubReleases, &[]) + if let Ok(Some(token)) = + credentials::provider_secret(DistributionProvider::GithubReleases, &[]) { envs.push(("GH_TOKEN", token)); } diff --git a/crates/tools/fission-cli/src/publish/mod.rs b/crates/tools/fission-command-package/src/lib.rs similarity index 96% rename from crates/tools/fission-cli/src/publish/mod.rs rename to crates/tools/fission-command-package/src/lib.rs index d3e1d4f1..c29be759 100644 --- a/crates/tools/fission-cli/src/publish/mod.rs +++ b/crates/tools/fission-command-package/src/lib.rs @@ -1,6 +1,7 @@ -use crate::{release, FissionProject, Target}; use anyhow::{bail, Context, Result}; use clap::ValueEnum; +use fission_command_core::{DistributionProvider, FissionProject, Target}; +use fission_credentials as credentials; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; @@ -21,7 +22,7 @@ mod stores; const ARTIFACT_MANIFEST: &str = "artifact-manifest.json"; #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] -pub(crate) enum PackageFormat { +pub enum PackageFormat { Aab, Apk, App, @@ -35,7 +36,7 @@ pub(crate) enum PackageFormat { } impl PackageFormat { - pub(crate) fn as_str(self) -> &'static str { + pub fn as_str(self) -> &'static str { match self { Self::Aab => "aab", Self::Apk => "apk", @@ -52,48 +53,7 @@ impl PackageFormat { } #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] -pub(crate) enum DistributionProvider { - #[value(name = "app-store")] - AppStore, - #[value(name = "github-pages")] - GithubPages, - #[value(name = "github-releases")] - GithubReleases, - #[value(name = "cloudflare-pages")] - CloudflarePages, - Dropbox, - #[value(name = "google-drive")] - GoogleDrive, - #[value(name = "microsoft-store")] - MicrosoftStore, - Netlify, - #[value(name = "onedrive")] - OneDrive, - #[value(name = "play-store")] - PlayStore, - S3, -} - -impl DistributionProvider { - pub(crate) fn as_str(self) -> &'static str { - match self { - Self::AppStore => "app-store", - Self::GithubPages => "github-pages", - Self::GithubReleases => "github-releases", - Self::CloudflarePages => "cloudflare-pages", - Self::Dropbox => "dropbox", - Self::GoogleDrive => "google-drive", - Self::MicrosoftStore => "microsoft-store", - Self::Netlify => "netlify", - Self::OneDrive => "onedrive", - Self::PlayStore => "play-store", - Self::S3 => "s3", - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] -pub(crate) enum DistributeAction { +pub enum DistributeAction { Setup, Publish, Status, @@ -102,46 +62,46 @@ pub(crate) enum DistributeAction { } #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] -pub(crate) enum ReadinessKind { +pub enum ReadinessKind { Package, Distribute, Release, } #[derive(Clone, Debug)] -pub(crate) struct PackageOptions { - pub(crate) project_dir: PathBuf, - pub(crate) target: Target, - pub(crate) format: PackageFormat, - pub(crate) release: bool, - pub(crate) json: bool, +pub struct PackageOptions { + pub project_dir: PathBuf, + pub target: Target, + pub format: PackageFormat, + pub release: bool, + pub json: bool, } #[derive(Clone, Debug)] -pub(crate) struct DistributeOptions { - pub(crate) project_dir: PathBuf, - pub(crate) provider: DistributionProvider, - pub(crate) action: DistributeAction, - pub(crate) artifact: Option, - pub(crate) site: String, - pub(crate) deploy: Option, - pub(crate) track: Option, - pub(crate) dry_run: bool, - pub(crate) yes: bool, - pub(crate) json: bool, +pub struct DistributeOptions { + pub project_dir: PathBuf, + pub provider: DistributionProvider, + pub action: DistributeAction, + pub artifact: Option, + pub site: String, + pub deploy: Option, + pub track: Option, + pub dry_run: bool, + pub yes: bool, + pub json: bool, } #[derive(Clone, Debug)] -pub(crate) struct ReadinessOptions { - pub(crate) project_dir: PathBuf, - pub(crate) kind: ReadinessKind, - pub(crate) target: Option, - pub(crate) format: Option, - pub(crate) provider: Option, - pub(crate) artifact: Option, - pub(crate) site: String, - pub(crate) track: Option, - pub(crate) json: bool, +pub struct ReadinessOptions { + pub project_dir: PathBuf, + pub kind: ReadinessKind, + pub target: Option, + pub format: Option, + pub provider: Option, + pub artifact: Option, + pub site: String, + pub track: Option, + pub json: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -398,7 +358,7 @@ struct NetlifyConfig { base_path: Option, } -pub(crate) fn package(options: PackageOptions) -> Result<()> { +pub fn package(options: PackageOptions) -> Result<()> { let manifest = package::package_artifact(&options)?; if options.json { println!("{}", serde_json::to_string_pretty(&manifest)?); @@ -418,7 +378,7 @@ pub(crate) fn package(options: PackageOptions) -> Result<()> { Ok(()) } -pub(crate) fn distribute(options: DistributeOptions) -> Result<()> { +pub fn distribute(options: DistributeOptions) -> Result<()> { let config = load_publish_manifest(&options.project_dir)?; match options.action { DistributeAction::Setup => setup_provider(&options, &config), @@ -430,7 +390,7 @@ pub(crate) fn distribute(options: DistributeOptions) -> Result<()> { } } -pub(crate) fn readiness(options: ReadinessOptions) -> Result<()> { +pub fn readiness(options: ReadinessOptions) -> Result<()> { let checks = match options.kind { ReadinessKind::Package => { readiness_package(&options.project_dir, options.target, options.format) @@ -707,7 +667,7 @@ fn cloudflare_pages_lifecycle( )], }); } - let token = release::provider_secret( + let token = credentials::provider_secret( DistributionProvider::CloudflarePages, &["CLOUDFLARE_API_TOKEN"], )? @@ -716,7 +676,7 @@ fn cloudflare_pages_lifecycle( "https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/{project_name}/deployments/{deploy}/rollback" ); let response = reqwest::blocking::Client::builder() - .user_agent("fission-cli-publish/0.1") + .user_agent("cargo-fission-publish/0.1") .build()? .post(url) .bearer_auth(token) @@ -1144,7 +1104,7 @@ fn cloudflare_pages_status( .project_name .as_deref() .context("distribution.cloudflare_pages..project_name is required")?; - let token = release::provider_secret( + let token = credentials::provider_secret( DistributionProvider::CloudflarePages, &["CLOUDFLARE_API_TOKEN"], )? @@ -1153,7 +1113,7 @@ fn cloudflare_pages_status( "https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/{project_name}/deployments" ); let response = reqwest::blocking::Client::builder() - .user_agent("fission-cli-publish/0.1") + .user_agent("cargo-fission-publish/0.1") .build()? .get(url) .bearer_auth(token) @@ -1265,7 +1225,7 @@ fn readiness_package( "fission.toml exists", "Run `fission init .` or point --project-dir at a Fission project.", )); - if let Ok(project) = crate::read_project_config(project_dir) { + if let Ok(project) = fission_command_core::read_project_config(project_dir) { checks.push(check( "release.package.target_configured", CheckSeverity::Error, @@ -1659,7 +1619,7 @@ fn readiness_github_pages( CheckSeverity::Info, if env::var_os("GH_TOKEN").is_some() || env::var_os("GITHUB_TOKEN").is_some() - || release::provider_secret(DistributionProvider::GithubPages, &[]) + || credentials::provider_secret(DistributionProvider::GithubPages, &[]) .ok() .flatten() .is_some() @@ -2089,7 +2049,7 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Build Fission static package - run: cargo fission package --project-dir {package_project_dir} --target site --format static --release + run: fission package --project-dir {package_project_dir} --target site --format static --release - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v3 @@ -2224,7 +2184,7 @@ fn required_provider_secret( remediation: &str, ) -> ReadinessCheck { let env_name = env_names.iter().find(|name| env::var_os(name).is_some()); - let vault_present = release::provider_secret(provider, &[]) + let vault_present = credentials::provider_secret(provider, &[]) .ok() .flatten() .is_some(); @@ -2690,7 +2650,7 @@ upload_provider = "crash-service" let workflow = fs::read_to_string(dir.join(".github/workflows/fission-pages.yml")).unwrap(); assert!(workflow.contains("actions/upload-pages-artifact")); assert!(workflow.contains("actions/deploy-pages")); - assert!(workflow.contains("cargo fission package")); + assert!(workflow.contains("fission package")); } #[test] diff --git a/crates/tools/fission-cli/src/publish/package.rs b/crates/tools/fission-command-package/src/package.rs similarity index 98% rename from crates/tools/fission-cli/src/publish/package.rs rename to crates/tools/fission-command-package/src/package.rs index 2a020c24..1447def2 100644 --- a/crates/tools/fission-cli/src/publish/package.rs +++ b/crates/tools/fission-command-package/src/package.rs @@ -1,6 +1,6 @@ use super::*; -use crate::{cargo_package_name, read_project_config, workflow, FissionProject, Target}; use anyhow::{bail, Context, Result}; +use fission_command_core::{cargo_package_name, read_project_config, FissionProject, Target}; use flate2::write::GzEncoder; use flate2::Compression; use serde::Deserialize; @@ -87,7 +87,7 @@ pub(super) fn package_static(options: &PackageOptions) -> Result Result { - workflow::site_build(&options.project_dir, options.release)?; + fission_command_site::build(&options.project_dir, options.release)?; site_output_dir(&options.project_dir)? } Target::Web => { - workflow::build_app(workflow::BuildOptions { + fission_command_run::build_app(fission_command_run::BuildOptions { project_dir: options.project_dir.clone(), target: Some(Target::Web), release: options.release, @@ -928,7 +928,7 @@ fn ensure_package_target( let project = read_project_config(&options.project_dir)?; if !project.targets.contains(&options.target) { bail!( - "target `{}` is not configured for this app; run `cargo fission add-target {} --project-dir {}`", + "target `{}` is not configured for this app; run `fission add-target {} --project-dir {}`", options.target.as_str(), options.target.as_str(), options.project_dir.display() diff --git a/crates/tools/fission-cli/src/publish/static_hosts.rs b/crates/tools/fission-command-package/src/static_hosts.rs similarity index 98% rename from crates/tools/fission-cli/src/publish/static_hosts.rs rename to crates/tools/fission-command-package/src/static_hosts.rs index 888bf623..4b24a361 100644 --- a/crates/tools/fission-cli/src/publish/static_hosts.rs +++ b/crates/tools/fission-command-package/src/static_hosts.rs @@ -1,6 +1,6 @@ use super::*; -use crate::release; use anyhow::{bail, Context, Result}; +use fission_credentials as credentials; use reqwest::blocking::Client; use serde_json::Value; use std::fs; @@ -249,14 +249,14 @@ fn add_zip_entries( } fn netlify_token() -> Result { - release::provider_secret(DistributionProvider::Netlify, &["NETLIFY_AUTH_TOKEN"])? + credentials::provider_secret(DistributionProvider::Netlify, &["NETLIFY_AUTH_TOKEN"])? .context("NETLIFY_AUTH_TOKEN or Fission vault credentials are required for Netlify") } fn http_client() -> Result { Client::builder() .timeout(Duration::from_secs(300)) - .user_agent("fission-cli-release/0.1") + .user_agent("cargo-fission-release/0.1") .build() .context("failed to build Netlify HTTP client") } diff --git a/crates/tools/fission-cli/src/publish/stores.rs b/crates/tools/fission-command-package/src/stores.rs similarity index 99% rename from crates/tools/fission-cli/src/publish/stores.rs rename to crates/tools/fission-command-package/src/stores.rs index 62384016..c6023407 100644 --- a/crates/tools/fission-cli/src/publish/stores.rs +++ b/crates/tools/fission-command-package/src/stores.rs @@ -1,6 +1,6 @@ use super::*; -use crate::release; use anyhow::{bail, Context, Result}; +use fission_credentials as credentials; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use reqwest::blocking::{Client, Response}; use serde::{Deserialize, Serialize}; @@ -1075,7 +1075,7 @@ fn google_play_access_token(cfg: &PlayStoreConfig, client: &Client) -> Result Result { let key_source = env_value("APP_STORE_CONNECT_API_KEY") .or_else(|| env_value("APP_STORE_CONNECT_API_KEY_PATH")) .or(cfg.api_key_path.clone()) - .or_else(|| release::provider_secret(DistributionProvider::AppStore, &[]).ok().flatten()) + .or_else(|| credentials::provider_secret(DistributionProvider::AppStore, &[]).ok().flatten()) .context("APP_STORE_CONNECT_API_KEY, APP_STORE_CONNECT_API_KEY_PATH, distribution.app_store.api_key_path, or vault credentials are required")?; if looks_like_bearer_token(&key_source) { return Ok(key_source); @@ -1317,7 +1317,7 @@ fn microsoft_store_success(value: &Value, operation: &str) -> Result<()> { fn http_client() -> Result { Client::builder() .timeout(Duration::from_secs(300)) - .user_agent("fission-cli-release/0.1") + .user_agent("cargo-fission-release/0.1") .build() .context("failed to build release HTTP client") } @@ -1498,7 +1498,7 @@ fn microsoft_store_client_secret() -> Option { env_value("MICROSOFT_STORE_CLIENT_SECRET") .or_else(|| env_value("PARTNER_CENTER_CLIENT_SECRET")) .or_else(|| { - release::provider_secret(DistributionProvider::MicrosoftStore, &[]) + credentials::provider_secret(DistributionProvider::MicrosoftStore, &[]) .ok() .flatten() }) @@ -1549,7 +1549,7 @@ fn secret_check( remediation: &str, ) -> ReadinessCheck { let env_name = env_names.iter().find(|name| env::var_os(name).is_some()); - let vault_present = release::provider_secret(provider, &[]) + let vault_present = credentials::provider_secret(provider, &[]) .ok() .flatten() .is_some(); diff --git a/crates/tools/fission-command-release/Cargo.toml b/crates/tools/fission-command-release/Cargo.toml new file mode 100644 index 00000000..fd393121 --- /dev/null +++ b/crates/tools/fission-command-release/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fission-command-release" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Release metadata, signing, beta, review, and auth workflows for the Fission command" + +[dependencies] +anyhow = "1.0" +base64 = "0.22" +clap = { version = "4.5", features = ["derive"] } +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +fission-command-package = { path = "../fission-command-package", version = "0.1.1" } +fission-command-ui = { path = "../fission-command-ui", version = "0.1.1" } +fission-credentials = { path = "../fission-credentials", version = "0.1.1" } +jsonwebtoken = "9" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +toml = "0.8" +toml_edit = "0.23" diff --git a/crates/tools/fission-cli/src/release/content.rs b/crates/tools/fission-command-release/src/content.rs similarity index 95% rename from crates/tools/fission-cli/src/release/content.rs rename to crates/tools/fission-command-release/src/content.rs index 3cc0c881..539261b7 100644 --- a/crates/tools/fission-cli/src/release/content.rs +++ b/crates/tools/fission-command-release/src/content.rs @@ -1,6 +1,6 @@ use super::*; use anyhow::{Context, Result}; -use base64::engine::general_purpose::STANDARD; +use base64::{engine::general_purpose::STANDARD, Engine as _}; use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -114,7 +114,7 @@ struct RenderedAsset { pub(super) fn validate_release_content_model( project_dir: &Path, - provider: Option, + provider: Option, ) -> LifecycleReport { let mut report = base_report("release-content.validate", provider, None); report.checks.push(path_check( @@ -196,7 +196,7 @@ pub(super) fn capture_release_content( pub(super) fn render_release_content( project_dir: &Path, - provider: publish::DistributionProvider, + provider: DistributionProvider, ) -> Result { let config = load_content_config(project_dir)?; let mut report = base_report("release-content.render", Some(provider), None); @@ -483,7 +483,7 @@ fn run_test_control_steps( ) -> Result<()> { let client = Client::builder() .timeout(Duration::from_secs(30)) - .user_agent("fission-cli-release-content/0.1") + .user_agent("cargo-fission-release-content/0.1") .build()?; wait_for_test_control(&client, port, timeout)?; checks.push(ok_check( @@ -722,7 +722,7 @@ fn write_capture_failure_receipt( fn validate_screenshots( project_dir: &Path, config: &ContentToml, - provider: Option, + provider: Option, checks: &mut Vec, ) { let screenshots = config @@ -803,7 +803,7 @@ fn validate_screenshots( fn validate_provider_assets( project_dir: &Path, config: &ContentToml, - provider: Option, + provider: Option, checks: &mut Vec, ) { let assets = config @@ -811,7 +811,7 @@ fn validate_provider_assets( .as_ref() .and_then(|release| release.assets.as_ref()); match provider { - Some(publish::DistributionProvider::PlayStore) => { + Some(DistributionProvider::PlayStore) => { if let Some(play) = assets.and_then(|assets| assets.play_store.as_ref()) { check_optional_path( project_dir, @@ -836,7 +836,7 @@ fn validate_provider_assets( ); } } - Some(publish::DistributionProvider::AppStore) => { + Some(DistributionProvider::AppStore) => { if let Some(app) = assets.and_then(|assets| assets.app_store.as_ref()) { check_optional_path( project_dir, @@ -861,7 +861,7 @@ fn validate_provider_assets( } } } - Some(publish::DistributionProvider::MicrosoftStore) => { + Some(DistributionProvider::MicrosoftStore) => { if let Some(ms) = assets.and_then(|assets| assets.microsoft_store.as_ref()) { check_optional_path( project_dir, @@ -949,7 +949,7 @@ fn hex_lower(bytes: &[u8]) -> String { } fn validate_rendered_asset_rules( - provider: publish::DistributionProvider, + provider: DistributionProvider, provider_dir: &Path, checks: &mut Vec, ) { @@ -1015,7 +1015,7 @@ fn collect_rendered_asset_files(root: &Path, files: &mut Vec } fn validate_image_asset( - provider: publish::DistributionProvider, + provider: DistributionProvider, path: &Path, checks: &mut Vec, ) { @@ -1095,7 +1095,7 @@ fn validate_image_asset( } fn validate_video_asset( - provider: publish::DistributionProvider, + provider: DistributionProvider, path: &Path, checks: &mut Vec, ) { @@ -1126,54 +1126,50 @@ fn validate_video_asset( }); } -fn provider_screenshot_count(provider: publish::DistributionProvider) -> (usize, usize) { +fn provider_screenshot_count(provider: DistributionProvider) -> (usize, usize) { match provider { - publish::DistributionProvider::AppStore => (1, 10), - publish::DistributionProvider::PlayStore => (2, 8), - publish::DistributionProvider::MicrosoftStore => (1, 10), + DistributionProvider::AppStore => (1, 10), + DistributionProvider::PlayStore => (2, 8), + DistributionProvider::MicrosoftStore => (1, 10), _ => (1, usize::MAX), } } -fn provider_image_extensions(provider: publish::DistributionProvider) -> &'static [&'static str] { +fn provider_image_extensions(provider: DistributionProvider) -> &'static [&'static str] { match provider { - publish::DistributionProvider::PlayStore => &["png", "jpg", "jpeg", "webp"], - publish::DistributionProvider::AppStore => &["png", "jpg", "jpeg"], - publish::DistributionProvider::MicrosoftStore => &["png", "jpg", "jpeg"], + DistributionProvider::PlayStore => &["png", "jpg", "jpeg", "webp"], + DistributionProvider::AppStore => &["png", "jpg", "jpeg"], + DistributionProvider::MicrosoftStore => &["png", "jpg", "jpeg"], _ => &["png", "jpg", "jpeg", "webp"], } } -fn provider_video_extensions(provider: publish::DistributionProvider) -> &'static [&'static str] { +fn provider_video_extensions(provider: DistributionProvider) -> &'static [&'static str] { match provider { - publish::DistributionProvider::AppStore => &["mov", "m4v", "mp4"], - publish::DistributionProvider::MicrosoftStore => &["mp4"], + DistributionProvider::AppStore => &["mov", "m4v", "mp4"], + DistributionProvider::MicrosoftStore => &["mp4"], _ => &["mp4"], } } -fn provider_max_image_bytes(provider: publish::DistributionProvider) -> u64 { +fn provider_max_image_bytes(provider: DistributionProvider) -> u64 { match provider { - publish::DistributionProvider::PlayStore => 8 * 1024 * 1024, - publish::DistributionProvider::AppStore => 10 * 1024 * 1024, - publish::DistributionProvider::MicrosoftStore => 50 * 1024 * 1024, + DistributionProvider::PlayStore => 8 * 1024 * 1024, + DistributionProvider::AppStore => 10 * 1024 * 1024, + DistributionProvider::MicrosoftStore => 50 * 1024 * 1024, _ => 10 * 1024 * 1024, } } -fn provider_dimension_check( - provider: publish::DistributionProvider, - width: u32, - height: u32, -) -> bool { +fn provider_dimension_check(provider: DistributionProvider, width: u32, height: u32) -> bool { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { let min = width.min(height); let max = width.max(height); min >= 320 && max <= 3840 && max <= min * 2 } - publish::DistributionProvider::AppStore => width >= 320 && height >= 320, - publish::DistributionProvider::MicrosoftStore => width >= 1366 && height >= 768, + DistributionProvider::AppStore => width >= 320 && height >= 320, + DistributionProvider::MicrosoftStore => width >= 1366 && height >= 768, _ => width > 0 && height > 0, } } @@ -1419,8 +1415,7 @@ feature_graphic = "release-content/screenshots/raw/en-US/home.png" fn render_release_content_copies_raw_assets_and_writes_manifest() { let dir = unique_dir("render"); write_content_project(&dir); - let report = - render_release_content(&dir, publish::DistributionProvider::PlayStore).unwrap(); + let report = render_release_content(&dir, DistributionProvider::PlayStore).unwrap(); assert_ne!(report.status, "blocked"); assert!(dir .join("release-content/screenshots/rendered/play-store/en-US/home.png") @@ -1451,7 +1446,7 @@ feature_graphic = "release-content/screenshots/raw/en-US/home.png" fs::write(provider_dir.join("two.png"), png_header(1440, 2560)).unwrap(); let mut checks = Vec::new(); validate_rendered_asset_rules( - publish::DistributionProvider::PlayStore, + DistributionProvider::PlayStore, &dir.join("release-content/screenshots/rendered/play-store"), &mut checks, ); diff --git a/crates/tools/fission-cli/src/release.rs b/crates/tools/fission-command-release/src/lib.rs similarity index 76% rename from crates/tools/fission-cli/src/release.rs rename to crates/tools/fission-command-release/src/lib.rs index a43fd878..7c9fe736 100644 --- a/crates/tools/fission-cli/src/release.rs +++ b/crates/tools/fission-command-release/src/lib.rs @@ -1,12 +1,9 @@ -use crate::{publish, Target}; use anyhow::{bail, Context, Result}; -use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; -use chacha20poly1305::{ - aead::{Aead, KeyInit}, - XChaCha20Poly1305, XNonce, -}; use clap::Subcommand; -use serde::{Deserialize, Serialize}; +use fission_command_core::{DistributionProvider, Target}; +use fission_command_package as publish; +use fission_credentials as credentials; +use serde::Serialize; use std::env; use std::fs; use std::io::{self, IsTerminal, Read}; @@ -25,8 +22,19 @@ mod signing_ops; mod store_ops; mod workflow_ops; +fn provider_secret(provider: DistributionProvider, env_names: &[&str]) -> Result> { + credentials::provider_secret(provider, env_names) +} + +fn now_unix_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + #[derive(Subcommand, Debug)] -pub(crate) enum ReleaseConfigCommand { +pub enum ReleaseConfigCommand { /// Open release configuration in an editor or the Fission terminal UI. Edit { #[arg(long, default_value = ".")] @@ -37,7 +45,7 @@ pub(crate) enum ReleaseConfigCommand { /// Import provider metadata into local release files. Import { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] locales: Option, #[arg(long)] @@ -50,7 +58,7 @@ pub(crate) enum ReleaseConfigCommand { /// Diff local release metadata against provider state. Diff { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long, default_value = ".")] project_dir: PathBuf, #[arg(long)] @@ -59,7 +67,7 @@ pub(crate) enum ReleaseConfigCommand { /// Validate fission.toml and referenced release files. Validate { #[arg(long, value_enum)] - provider: Option, + provider: Option, #[arg(long, default_value = ".")] project_dir: PathBuf, #[arg(long)] @@ -68,7 +76,7 @@ pub(crate) enum ReleaseConfigCommand { /// Push release metadata to a provider. Push { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] locales: Option, #[arg(long)] @@ -116,7 +124,7 @@ pub(crate) enum ReleaseConfigCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum ReleaseContentCommand { +pub enum ReleaseContentCommand { /// Capture screenshots/videos from configured release scenarios. Capture { #[arg(long, value_enum)] @@ -131,7 +139,7 @@ pub(crate) enum ReleaseContentCommand { /// Render store-ready screenshot/video assets from raw captures. Render { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long, default_value = ".")] project_dir: PathBuf, #[arg(long)] @@ -140,7 +148,7 @@ pub(crate) enum ReleaseContentCommand { /// Validate release-content assets and manifests. Validate { #[arg(long, value_enum)] - provider: Option, + provider: Option, #[arg(long, default_value = ".")] project_dir: PathBuf, #[arg(long)] @@ -149,7 +157,7 @@ pub(crate) enum ReleaseContentCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum BetaCommand { +pub enum BetaCommand { /// Manage beta groups/flights/tracks. Groups { #[command(subcommand)] @@ -163,7 +171,7 @@ pub(crate) enum BetaCommand { /// Distribute an artifact to a beta track/group. Distribute { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] artifact: PathBuf, #[arg(long)] @@ -180,10 +188,10 @@ pub(crate) enum BetaCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum BetaGroupsCommand { +pub enum BetaGroupsCommand { List { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long, default_value = ".")] project_dir: PathBuf, #[arg(long)] @@ -191,7 +199,7 @@ pub(crate) enum BetaGroupsCommand { }, Sync { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long, default_value = "fission.toml")] from: PathBuf, #[arg(long, default_value = ".")] @@ -204,10 +212,10 @@ pub(crate) enum BetaGroupsCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum BetaTestersCommand { +pub enum BetaTestersCommand { Import { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] group: Option, #[arg(long)] @@ -223,7 +231,7 @@ pub(crate) enum BetaTestersCommand { }, Export { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] group: Option, #[arg(long)] @@ -238,7 +246,7 @@ pub(crate) enum BetaTestersCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum SigningCommand { +pub enum SigningCommand { Status { #[arg(long, value_enum)] target: Target, @@ -272,10 +280,10 @@ pub(crate) enum SigningCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum ReviewsCommand { +pub enum ReviewsCommand { List { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] since: Option, #[arg(long, default_value = ".")] @@ -285,7 +293,7 @@ pub(crate) enum ReviewsCommand { }, Reply { #[arg(long, value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] review: String, #[arg(long)] @@ -300,7 +308,7 @@ pub(crate) enum ReviewsCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum ReleaseWorkflowCommand { +pub enum ReleaseWorkflowCommand { /// List configured release workflows. List { #[arg(long, default_value = ".")] @@ -321,32 +329,32 @@ pub(crate) enum ReleaseWorkflowCommand { } #[derive(Subcommand, Debug)] -pub(crate) enum AuthCommand { +pub enum AuthCommand { Setup { #[arg(value_enum)] - provider: Option, + provider: Option, #[arg(long)] json: bool, }, Login { #[arg(value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, }, Status { #[arg(value_enum)] - provider: Option, + provider: Option, #[arg(long)] json: bool, }, Logout { #[arg(value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] yes: bool, }, Import { #[arg(value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, #[arg(long)] from: String, #[arg(long)] @@ -354,7 +362,7 @@ pub(crate) enum AuthCommand { }, Rotate { #[arg(value_enum)] - provider: publish::DistributionProvider, + provider: DistributionProvider, }, Audit { #[arg(long)] @@ -380,16 +388,7 @@ struct LifecycleCheck { remediation: Vec, } -#[derive(Debug, Serialize, Deserialize)] -struct VaultRecord { - schema_version: u32, - provider: String, - created_at_unix_seconds: u64, - nonce: String, - ciphertext: String, -} - -pub(crate) fn release_config(command: ReleaseConfigCommand) -> Result<()> { +pub fn release_config(command: ReleaseConfigCommand) -> Result<()> { match command { ReleaseConfigCommand::Edit { project_dir, tui } => edit_release_config(&project_dir, tui), ReleaseConfigCommand::Validate { @@ -442,7 +441,7 @@ pub(crate) fn release_config(command: ReleaseConfigCommand) -> Result<()> { } } -pub(crate) fn release_content(command: ReleaseContentCommand) -> Result<()> { +pub fn release_content(command: ReleaseContentCommand) -> Result<()> { match command { ReleaseContentCommand::Validate { provider, @@ -472,7 +471,7 @@ pub(crate) fn release_content(command: ReleaseContentCommand) -> Result<()> { } } -pub(crate) fn beta(command: BetaCommand) -> Result<()> { +pub fn beta(command: BetaCommand) -> Result<()> { match command { BetaCommand::Groups { command } => match command { BetaGroupsCommand::List { @@ -545,7 +544,7 @@ pub(crate) fn beta(command: BetaCommand) -> Result<()> { } } -pub(crate) fn signing(command: SigningCommand) -> Result<()> { +pub fn signing(command: SigningCommand) -> Result<()> { match command { SigningCommand::Status { target, @@ -568,7 +567,7 @@ pub(crate) fn signing(command: SigningCommand) -> Result<()> { } } -pub(crate) fn reviews(command: ReviewsCommand) -> Result<()> { +pub fn reviews(command: ReviewsCommand) -> Result<()> { match command { ReviewsCommand::List { provider, @@ -594,7 +593,7 @@ pub(crate) fn reviews(command: ReviewsCommand) -> Result<()> { } } -pub(crate) fn release_workflow(command: ReleaseWorkflowCommand) -> Result<()> { +pub fn release_workflow(command: ReleaseWorkflowCommand) -> Result<()> { match command { ReleaseWorkflowCommand::List { project_dir, json } => { workflow_ops::list(&project_dir, json) @@ -608,7 +607,7 @@ pub(crate) fn release_workflow(command: ReleaseWorkflowCommand) -> Result<()> { } } -pub(crate) fn auth(command: AuthCommand) -> Result<()> { +pub fn auth(command: AuthCommand) -> Result<()> { match command { AuthCommand::Status { provider, json } => { print_report(auth_report("auth.status", provider), json) @@ -623,7 +622,7 @@ pub(crate) fn auth(command: AuthCommand) -> Result<()> { provider.as_str() ); } - let path = vault_record_path(provider)?; + let path = credentials::vault_record_path(provider)?; if path.exists() { fs::remove_file(&path)?; println!( @@ -651,8 +650,8 @@ pub(crate) fn auth(command: AuthCommand) -> Result<()> { fs::metadata(path) .with_context(|| format!("credential file {path} does not exist"))?; } - let secret = read_secret_source(&from)?; - store_provider_secret(provider, secret.as_bytes())?; + let secret = credentials::read_secret_source(&from)?; + credentials::store_provider_secret(provider, secret.as_bytes())?; println!( "Stored {} credentials in the encrypted Fission release vault", provider.as_str() @@ -660,14 +659,14 @@ pub(crate) fn auth(command: AuthCommand) -> Result<()> { Ok(()) } AuthCommand::Rotate { provider } => { - rotate_provider_secret(provider)?; + credentials::rotate_provider_secret(provider)?; println!("Rotated {} vault encryption record", provider.as_str()); Ok(()) } } } -fn login_provider(provider: publish::DistributionProvider) -> Result<()> { +fn login_provider(provider: DistributionProvider) -> Result<()> { print_login_instructions(provider); let secret = if io::stdin().is_terminal() { println!("Paste the provider token, service-account JSON, API key contents, or a file:/env: source, then press Enter:"); @@ -683,11 +682,11 @@ fn login_provider(provider: publish::DistributionProvider) -> Result<()> { bail!("no credential was provided for {}", provider.as_str()); } let resolved = if secret.starts_with("env:") || secret.starts_with("file:") { - read_secret_source(&secret)? + credentials::read_secret_source(&secret)? } else { secret }; - store_provider_secret(provider, resolved.as_bytes())?; + credentials::store_provider_secret(provider, resolved.as_bytes())?; println!( "Stored {} credentials in the encrypted Fission release vault", provider.as_str() @@ -695,68 +694,49 @@ fn login_provider(provider: publish::DistributionProvider) -> Result<()> { Ok(()) } -fn print_login_instructions(provider: publish::DistributionProvider) { +fn print_login_instructions(provider: DistributionProvider) { match provider { - publish::DistributionProvider::PlayStore => println!( + DistributionProvider::PlayStore => println!( "Google Play uses an Android Publisher API service-account JSON file or a short-lived access token." ), - publish::DistributionProvider::AppStore => println!( + DistributionProvider::AppStore => println!( "App Store Connect uses an issuer id, key id, and .p8 API private key; paste the key contents or import APP_STORE_CONNECT_API_KEY_PATH separately." ), - publish::DistributionProvider::MicrosoftStore => println!( + DistributionProvider::MicrosoftStore => println!( "Microsoft Store uses Partner Center/Entra credentials; paste the client secret or pipe it from your secret manager." ), - publish::DistributionProvider::GithubPages => println!( + DistributionProvider::GithubPages => println!( "GitHub Pages uses a GitHub token with repository Pages/workflow permissions when direct API access is needed." ), - publish::DistributionProvider::GithubReleases => println!( + DistributionProvider::GithubReleases => println!( "GitHub Releases uses the GitHub CLI. Run `gh auth login`, set GH_TOKEN/GITHUB_TOKEN, or import a token into the Fission vault." ), - publish::DistributionProvider::CloudflarePages => println!( + DistributionProvider::CloudflarePages => println!( "Cloudflare Pages uses an API token with Pages project edit/deploy permissions." ), - publish::DistributionProvider::Netlify => println!( + DistributionProvider::Netlify => println!( "Netlify uses a personal access token with deploy permissions for the configured site." ), - publish::DistributionProvider::S3 => println!( + DistributionProvider::S3 => println!( "S3-compatible uploads normally use AWS_PROFILE or access-key environment variables; paste a provider credential only for local vault-backed workflows." ), - publish::DistributionProvider::GoogleDrive => println!( + DistributionProvider::GoogleDrive => println!( "Google Drive uses an OAuth access token for the target account or service account flow you manage outside the project." ), - publish::DistributionProvider::OneDrive => println!( + DistributionProvider::OneDrive => println!( "OneDrive uses a Microsoft Graph OAuth access token for the target account." ), - publish::DistributionProvider::Dropbox => println!( + DistributionProvider::Dropbox => println!( "Dropbox uses an OAuth access token with files.content.write/read scopes." ), } } -pub(crate) fn provider_secret( - provider: publish::DistributionProvider, - env_names: &[&str], -) -> Result> { - if let Some(name) = env_names.iter().find(|name| env::var_os(name).is_some()) { - return env::var(name) - .map(Some) - .with_context(|| format!("environment variable {name} is not valid UTF-8")); - } - let path = vault_record_path(provider)?; - if !path.exists() { - return Ok(None); - } - let bytes = load_provider_secret(provider)?; - String::from_utf8(bytes) - .map(Some) - .context("stored provider credential is not valid UTF-8") -} - fn edit_release_config(project_dir: &Path, tui: bool) -> Result<()> { let path = project_dir.join("fission.toml"); fs::metadata(&path).with_context(|| format!("{} does not exist", path.display()))?; if tui { - return crate::ui::run_ui(crate::ui::UiOptions { + return fission_command_ui::run_ui(fission_command_ui::UiOptions { project_dir: project_dir.to_path_buf(), screenshot: None, exit_after_render: false, @@ -882,7 +862,7 @@ fn edit_release_file( Ok(()) } -fn auth_report(area: &str, provider: Option) -> LifecycleReport { +fn auth_report(area: &str, provider: Option) -> LifecycleReport { let mut report = base_report(area, provider, None); let providers = provider .map(|provider| vec![provider]) @@ -894,7 +874,7 @@ fn auth_report(area: &str, provider: Option) -> L report } -fn auth_setup_report(provider: Option) -> LifecycleReport { +fn auth_setup_report(provider: Option) -> LifecycleReport { let mut report = base_report("auth.setup", provider, None); let providers = provider .map(|provider| vec![provider]) @@ -937,19 +917,19 @@ fn auth_setup_report(provider: Option) -> Lifecyc report } -fn auth_providers() -> Vec { +fn auth_providers() -> Vec { vec![ - publish::DistributionProvider::GithubPages, - publish::DistributionProvider::GithubReleases, - publish::DistributionProvider::CloudflarePages, - publish::DistributionProvider::Netlify, - publish::DistributionProvider::S3, - publish::DistributionProvider::GoogleDrive, - publish::DistributionProvider::OneDrive, - publish::DistributionProvider::Dropbox, - publish::DistributionProvider::PlayStore, - publish::DistributionProvider::AppStore, - publish::DistributionProvider::MicrosoftStore, + DistributionProvider::GithubPages, + DistributionProvider::GithubReleases, + DistributionProvider::CloudflarePages, + DistributionProvider::Netlify, + DistributionProvider::S3, + DistributionProvider::GoogleDrive, + DistributionProvider::OneDrive, + DistributionProvider::Dropbox, + DistributionProvider::PlayStore, + DistributionProvider::AppStore, + DistributionProvider::MicrosoftStore, ] } @@ -960,63 +940,63 @@ struct ProviderAuthSpec { permissions: &'static str, } -fn provider_auth_spec(provider: publish::DistributionProvider) -> ProviderAuthSpec { +fn provider_auth_spec(provider: DistributionProvider) -> ProviderAuthSpec { match provider { - publish::DistributionProvider::GithubPages => ProviderAuthSpec { + DistributionProvider::GithubPages => ProviderAuthSpec { kind: "GitHub token or GitHub App installation token", env: &["GH_TOKEN", "GITHUB_TOKEN"], command: "fission auth import github-pages --from env:GH_TOKEN --yes", permissions: "repository contents/workflows/pages permissions for local API operations; Actions deployment uses repository workflow permissions", }, - publish::DistributionProvider::GithubReleases => ProviderAuthSpec { + DistributionProvider::GithubReleases => ProviderAuthSpec { kind: "Authenticated GitHub CLI session, GitHub token, or GitHub App installation token", env: &["GH_TOKEN", "GITHUB_TOKEN"], command: "gh auth login", permissions: "repository Contents write permission to create/update releases and upload/delete release assets", }, - publish::DistributionProvider::CloudflarePages => ProviderAuthSpec { + DistributionProvider::CloudflarePages => ProviderAuthSpec { kind: "Cloudflare API token plus Wrangler login/config for uploads", env: &["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"], command: "fission auth import cloudflare-pages --from env:CLOUDFLARE_API_TOKEN --yes", permissions: "Pages edit/deploy permission for the target account/project", }, - publish::DistributionProvider::Netlify => ProviderAuthSpec { + DistributionProvider::Netlify => ProviderAuthSpec { kind: "Netlify personal access token", env: &["NETLIFY_AUTH_TOKEN"], command: "fission auth import netlify --from env:NETLIFY_AUTH_TOKEN --yes", permissions: "site read/deploy permissions for the configured site", }, - publish::DistributionProvider::S3 => ProviderAuthSpec { + DistributionProvider::S3 => ProviderAuthSpec { kind: "AWS/S3 profile or access key credentials", env: &["AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], command: "fission auth import s3 --from env:AWS_SECRET_ACCESS_KEY --yes", permissions: "s3:PutObject, s3:ListBucket, and optional s3:PutObjectAcl for public artifacts", }, - publish::DistributionProvider::GoogleDrive => ProviderAuthSpec { + DistributionProvider::GoogleDrive => ProviderAuthSpec { kind: "Google OAuth access token or service-account flow managed outside fission.toml", env: &["GOOGLE_DRIVE_ACCESS_TOKEN"], command: "fission auth import google-drive --from env:GOOGLE_DRIVE_ACCESS_TOKEN --yes", permissions: "Drive file create/update permission for the selected folder", }, - publish::DistributionProvider::OneDrive => ProviderAuthSpec { + DistributionProvider::OneDrive => ProviderAuthSpec { kind: "Microsoft Graph OAuth access token", env: &["ONEDRIVE_ACCESS_TOKEN"], command: "fission auth import onedrive --from env:ONEDRIVE_ACCESS_TOKEN --yes", permissions: "Files.ReadWrite or equivalent delegated/application permission for the target drive", }, - publish::DistributionProvider::Dropbox => ProviderAuthSpec { + DistributionProvider::Dropbox => ProviderAuthSpec { kind: "Dropbox OAuth access token", env: &["DROPBOX_ACCESS_TOKEN"], command: "fission auth import dropbox --from env:DROPBOX_ACCESS_TOKEN --yes", permissions: "files.content.write and files.metadata.read for the destination path", }, - publish::DistributionProvider::PlayStore => ProviderAuthSpec { + DistributionProvider::PlayStore => ProviderAuthSpec { kind: "Google Play Android Publisher service-account JSON or access token", env: &["PLAY_STORE_SERVICE_ACCOUNT_JSON"], command: "fission auth import play-store --from file:play-service-account.json --yes", permissions: "Android Publisher API access to the configured package and release tracks", }, - publish::DistributionProvider::AppStore => ProviderAuthSpec { + DistributionProvider::AppStore => ProviderAuthSpec { kind: "App Store Connect API private key plus issuer/key ids", env: &[ "APP_STORE_CONNECT_API_KEY", @@ -1027,7 +1007,7 @@ fn provider_auth_spec(provider: publish::DistributionProvider) -> ProviderAuthSp command: "fission auth import app-store --from file:AuthKey.p8 --yes", permissions: "App Manager or equivalent App Store Connect API role for metadata, uploads, TestFlight, and submissions", }, - publish::DistributionProvider::MicrosoftStore => ProviderAuthSpec { + DistributionProvider::MicrosoftStore => ProviderAuthSpec { kind: "Partner Center/Entra application secret or access token", env: &["MICROSOFT_STORE_TOKEN", "MICROSOFT_STORE_CLIENT_SECRET"], command: "fission auth import microsoft-store --from env:MICROSOFT_STORE_CLIENT_SECRET --yes", @@ -1036,22 +1016,22 @@ fn provider_auth_spec(provider: publish::DistributionProvider) -> ProviderAuthSp } } -fn provider_env_check(provider: publish::DistributionProvider) -> LifecycleCheck { +fn provider_env_check(provider: DistributionProvider) -> LifecycleCheck { let vars: &[&str] = match provider { - publish::DistributionProvider::GithubPages => &["GH_TOKEN", "GITHUB_TOKEN"], - publish::DistributionProvider::GithubReleases => &["GH_TOKEN", "GITHUB_TOKEN"], - publish::DistributionProvider::CloudflarePages => &["CLOUDFLARE_API_TOKEN"], - publish::DistributionProvider::Netlify => &["NETLIFY_AUTH_TOKEN"], - publish::DistributionProvider::S3 => &["AWS_PROFILE", "AWS_ACCESS_KEY_ID"], - publish::DistributionProvider::GoogleDrive => &["GOOGLE_DRIVE_ACCESS_TOKEN"], - publish::DistributionProvider::OneDrive => &["ONEDRIVE_ACCESS_TOKEN"], - publish::DistributionProvider::Dropbox => &["DROPBOX_ACCESS_TOKEN"], - publish::DistributionProvider::PlayStore => &["PLAY_STORE_SERVICE_ACCOUNT_JSON"], - publish::DistributionProvider::AppStore => &["APP_STORE_CONNECT_API_KEY"], - publish::DistributionProvider::MicrosoftStore => &["MICROSOFT_STORE_TOKEN"], + DistributionProvider::GithubPages => &["GH_TOKEN", "GITHUB_TOKEN"], + DistributionProvider::GithubReleases => &["GH_TOKEN", "GITHUB_TOKEN"], + DistributionProvider::CloudflarePages => &["CLOUDFLARE_API_TOKEN"], + DistributionProvider::Netlify => &["NETLIFY_AUTH_TOKEN"], + DistributionProvider::S3 => &["AWS_PROFILE", "AWS_ACCESS_KEY_ID"], + DistributionProvider::GoogleDrive => &["GOOGLE_DRIVE_ACCESS_TOKEN"], + DistributionProvider::OneDrive => &["ONEDRIVE_ACCESS_TOKEN"], + DistributionProvider::Dropbox => &["DROPBOX_ACCESS_TOKEN"], + DistributionProvider::PlayStore => &["PLAY_STORE_SERVICE_ACCOUNT_JSON"], + DistributionProvider::AppStore => &["APP_STORE_CONNECT_API_KEY"], + DistributionProvider::MicrosoftStore => &["MICROSOFT_STORE_TOKEN"], }; let found = vars.iter().find(|name| env::var_os(name).is_some()); - let vault_path = vault_record_path(provider).ok(); + let vault_path = credentials::vault_record_path(provider).ok(); let vault_present = vault_path.as_ref().is_some_and(|path| path.exists()); LifecycleCheck { id: format!("auth.{}.credentials", provider.as_str().replace('-', "_")), @@ -1073,114 +1053,6 @@ fn provider_env_check(provider: publish::DistributionProvider) -> LifecycleCheck } } -fn read_secret_source(source: &str) -> Result { - if let Some(name) = source.strip_prefix("env:") { - env::var(name).with_context(|| format!("environment variable {name} is not set")) - } else if let Some(path) = source.strip_prefix("file:") { - fs::read_to_string(path).with_context(|| format!("failed to read credential file {path}")) - } else { - bail!("credential source must be env: or file:") - } -} - -fn store_provider_secret(provider: publish::DistributionProvider, secret: &[u8]) -> Result<()> { - let key = vault_key(true)?; - let mut nonce = [0u8; 24]; - getrandom::getrandom(&mut nonce)?; - let cipher = XChaCha20Poly1305::new_from_slice(&key) - .map_err(|error| anyhow::anyhow!("failed to initialize vault cipher: {error}"))?; - let ciphertext = cipher - .encrypt(XNonce::from_slice(&nonce), secret) - .map_err(|error| anyhow::anyhow!("failed to encrypt credential record: {error}"))?; - let record = VaultRecord { - schema_version: 1, - provider: provider.as_str().to_string(), - created_at_unix_seconds: now_unix_seconds(), - nonce: STANDARD_NO_PAD.encode(nonce), - ciphertext: STANDARD_NO_PAD.encode(ciphertext), - }; - let path = vault_record_path(provider)?; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - fs::write(&path, serde_json::to_vec_pretty(&record)?) - .with_context(|| format!("failed to write {}", path.display()))?; - Ok(()) -} - -fn load_provider_secret(provider: publish::DistributionProvider) -> Result> { - let path = vault_record_path(provider)?; - let record: VaultRecord = serde_json::from_slice( - &fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?, - )?; - let nonce = STANDARD_NO_PAD - .decode(record.nonce) - .context("failed to decode vault nonce")?; - let ciphertext = STANDARD_NO_PAD - .decode(record.ciphertext) - .context("failed to decode vault ciphertext")?; - let key = vault_key(false)?; - let cipher = XChaCha20Poly1305::new_from_slice(&key) - .map_err(|error| anyhow::anyhow!("failed to initialize vault cipher: {error}"))?; - cipher - .decrypt(XNonce::from_slice(&nonce), ciphertext.as_ref()) - .map_err(|error| anyhow::anyhow!("failed to decrypt credential record: {error}")) -} - -fn rotate_provider_secret(provider: publish::DistributionProvider) -> Result<()> { - let secret = load_provider_secret(provider)?; - store_provider_secret(provider, &secret) -} - -fn vault_key(create: bool) -> Result<[u8; 32]> { - let entry = keyring::Entry::new("fission", "release-vault") - .context("failed to open OS credential store for the Fission release vault")?; - match entry.get_password() { - Ok(encoded) => decode_vault_key(&encoded), - Err(error) if create => { - let mut key = [0u8; 32]; - getrandom::getrandom(&mut key)?; - entry - .set_password(&STANDARD_NO_PAD.encode(key)) - .with_context(|| { - format!("failed to store Fission vault key in OS credential store: {error}") - })?; - Ok(key) - } - Err(error) => { - Err(error).context("Fission vault key does not exist in the OS credential store") - } - } -} - -fn decode_vault_key(encoded: &str) -> Result<[u8; 32]> { - let bytes = STANDARD_NO_PAD - .decode(encoded) - .context("failed to decode Fission vault key")?; - let key: [u8; 32] = bytes - .try_into() - .map_err(|_| anyhow::anyhow!("Fission vault key has the wrong length"))?; - Ok(key) -} - -fn vault_record_path(provider: publish::DistributionProvider) -> Result { - Ok(vault_dir()?.join(format!("{}.json", provider.as_str()))) -} - -fn vault_dir() -> Result { - let home = env::var_os("HOME") - .or_else(|| env::var_os("USERPROFILE")) - .context("HOME/USERPROFILE is not set")?; - Ok(PathBuf::from(home).join(".fission/vault")) -} - -fn now_unix_seconds() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} - fn set_toml_path(root: &mut toml::Value, path: &str, value: toml::Value) -> Result<()> { let mut current = root; let parts = path.split('.').collect::>(); @@ -1204,7 +1076,7 @@ fn set_toml_path(root: &mut toml::Value, path: &str, value: toml::Value) -> Resu fn base_report( area: &str, - provider: Option, + provider: Option, target: Option, ) -> LifecycleReport { LifecycleReport { @@ -1323,7 +1195,7 @@ mod tests { #[test] fn auth_setup_documents_provider_credentials_without_secrets() { - let report = auth_setup_report(Some(publish::DistributionProvider::CloudflarePages)); + let report = auth_setup_report(Some(DistributionProvider::CloudflarePages)); assert_eq!(report.status, "ready"); assert!(report.checks.iter().any(|check| { check.id == "auth.cloudflare_pages.env" diff --git a/crates/tools/fission-cli/src/release/microsoft_store_ops.rs b/crates/tools/fission-command-release/src/microsoft_store_ops.rs similarity index 99% rename from crates/tools/fission-cli/src/release/microsoft_store_ops.rs rename to crates/tools/fission-command-release/src/microsoft_store_ops.rs index de9ac5f4..2b2d0b89 100644 --- a/crates/tools/fission-cli/src/release/microsoft_store_ops.rs +++ b/crates/tools/fission-command-release/src/microsoft_store_ops.rs @@ -687,7 +687,7 @@ fn microsoft_store_access_token(cfg: &MicrosoftStoreConfig, client: &Client) -> .context("distribution.microsoft_store.client_id or AZURE_CLIENT_ID is required")?; let client_secret = env_value("MICROSOFT_STORE_CLIENT_SECRET") .or_else(|| { - provider_secret(publish::DistributionProvider::MicrosoftStore, &[]) + provider_secret(DistributionProvider::MicrosoftStore, &[]) .ok() .flatten() }) @@ -742,7 +742,7 @@ fn microsoft_store_success(value: &Value, operation: &str) -> Result<()> { fn http_client() -> Result { Client::builder() .timeout(Duration::from_secs(300)) - .user_agent("fission-cli-release/0.1") + .user_agent("cargo-fission-release/0.1") .build() .context("failed to build release HTTP client") } diff --git a/crates/tools/fission-cli/src/release/model.rs b/crates/tools/fission-command-release/src/model.rs similarity index 96% rename from crates/tools/fission-cli/src/release/model.rs rename to crates/tools/fission-command-release/src/model.rs index 4cf80544..e1b7412c 100644 --- a/crates/tools/fission-cli/src/release/model.rs +++ b/crates/tools/fission-command-release/src/model.rs @@ -42,7 +42,7 @@ struct ReleaseEntry { pub(super) fn validate_release_config_model( project_dir: &Path, - provider: Option, + provider: Option, ) -> Result { let mut report = base_report("release-config.validate", provider, None); let path = project_dir.join("fission.toml"); @@ -168,7 +168,7 @@ fn validate_release_entry( root: &ReleaseRoot, release: &ReleaseEntry, index: usize, - provider: Option, + provider: Option, checks: &mut Vec, ) { let id = release @@ -314,14 +314,14 @@ fn validate_release_path( } fn validate_provider_listing( - provider: publish::DistributionProvider, + provider: DistributionProvider, root: &ReleaseRoot, checks: &mut Vec, ) { let key = match provider { - publish::DistributionProvider::PlayStore => "play_store", - publish::DistributionProvider::AppStore => "app_store", - publish::DistributionProvider::MicrosoftStore => "microsoft_store", + DistributionProvider::PlayStore => "play_store", + DistributionProvider::AppStore => "app_store", + DistributionProvider::MicrosoftStore => "microsoft_store", _ => return, }; let listings = root.store_listing.get(key); @@ -371,7 +371,7 @@ fn validate_listing_value( fn validate_tracks_match_provider( release_id: &str, - provider: Option, + provider: Option, tracks: &[String], checks: &mut Vec, ) { @@ -536,8 +536,7 @@ privacy_url = "https://example.com/privacy" let dir = unique_dir("valid"); write_release_project(&dir, "A precise release note.\n"); let report = - validate_release_config_model(&dir, Some(publish::DistributionProvider::PlayStore)) - .unwrap(); + validate_release_config_model(&dir, Some(DistributionProvider::PlayStore)).unwrap(); assert_ne!(report.status, "blocked"); } @@ -546,8 +545,7 @@ privacy_url = "https://example.com/privacy" let dir = unique_dir("placeholder"); write_release_project(&dir, "TODO fill this in.\n"); let report = - validate_release_config_model(&dir, Some(publish::DistributionProvider::PlayStore)) - .unwrap(); + validate_release_config_model(&dir, Some(DistributionProvider::PlayStore)).unwrap(); assert_eq!(report.status, "blocked"); assert!(report .checks diff --git a/crates/tools/fission-cli/src/release/signing_ops.rs b/crates/tools/fission-command-release/src/signing_ops.rs similarity index 100% rename from crates/tools/fission-cli/src/release/signing_ops.rs rename to crates/tools/fission-command-release/src/signing_ops.rs diff --git a/crates/tools/fission-cli/src/release/store_ops.rs b/crates/tools/fission-command-release/src/store_ops.rs similarity index 96% rename from crates/tools/fission-cli/src/release/store_ops.rs rename to crates/tools/fission-command-release/src/store_ops.rs index dbe5296c..b34da6fd 100644 --- a/crates/tools/fission-cli/src/release/store_ops.rs +++ b/crates/tools/fission-command-release/src/store_ops.rs @@ -170,24 +170,20 @@ struct OAuthTokenResponse { } pub(super) fn reviews_list( - provider: publish::DistributionProvider, + provider: DistributionProvider, since: Option, project_dir: &Path, json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { - play_reviews_list(project_dir, since, json_output) - } - publish::DistributionProvider::AppStore => { - app_store_reviews_list(project_dir, since, json_output) - } + DistributionProvider::PlayStore => play_reviews_list(project_dir, since, json_output), + DistributionProvider::AppStore => app_store_reviews_list(project_dir, since, json_output), _ => unsupported_reviews(provider, "list"), } } pub(super) fn reviews_reply( - provider: publish::DistributionProvider, + provider: DistributionProvider, review: &str, message_file: &Path, project_dir: &Path, @@ -195,10 +191,10 @@ pub(super) fn reviews_reply( json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_reviews_reply(project_dir, review, message_file, dry_run, json_output) } - publish::DistributionProvider::AppStore => { + DistributionProvider::AppStore => { app_store_reviews_reply(project_dir, review, message_file, dry_run, json_output) } _ => unsupported_reviews(provider, "reply"), @@ -206,28 +202,26 @@ pub(super) fn reviews_reply( } pub(super) fn beta_groups_list( - provider: publish::DistributionProvider, + provider: DistributionProvider, project_dir: &Path, json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => play_beta_groups_list(project_dir, json_output), - publish::DistributionProvider::AppStore => { - app_store_beta_groups_list(project_dir, json_output) - } + DistributionProvider::PlayStore => play_beta_groups_list(project_dir, json_output), + DistributionProvider::AppStore => app_store_beta_groups_list(project_dir, json_output), _ => unsupported_beta(provider, "groups list"), } } pub(super) fn beta_groups_sync( - provider: publish::DistributionProvider, + provider: DistributionProvider, from: &Path, project_dir: &Path, dry_run: bool, json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_beta_groups_sync(project_dir, from, dry_run, json_output) } _ => unsupported_beta(provider, "groups sync"), @@ -235,7 +229,7 @@ pub(super) fn beta_groups_sync( } pub(super) fn beta_testers_import( - provider: publish::DistributionProvider, + provider: DistributionProvider, group: Option<&str>, track: Option<&str>, csv: &Path, @@ -244,10 +238,10 @@ pub(super) fn beta_testers_import( json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_beta_testers_import(project_dir, track, csv, dry_run, json_output) } - publish::DistributionProvider::AppStore => { + DistributionProvider::AppStore => { app_store_beta_testers_import(project_dir, group, csv, dry_run, json_output) } _ => unsupported_beta(provider, "testers import"), @@ -255,7 +249,7 @@ pub(super) fn beta_testers_import( } pub(super) fn beta_testers_export( - provider: publish::DistributionProvider, + provider: DistributionProvider, group: Option<&str>, track: Option<&str>, output: &Path, @@ -263,10 +257,10 @@ pub(super) fn beta_testers_export( json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_beta_testers_export(project_dir, track, output, json_output) } - publish::DistributionProvider::AppStore => { + DistributionProvider::AppStore => { app_store_beta_testers_export(project_dir, group, output, json_output) } _ => unsupported_beta(provider, "testers export"), @@ -274,44 +268,38 @@ pub(super) fn beta_testers_export( } pub(super) fn release_config_import( - provider: publish::DistributionProvider, + provider: DistributionProvider, locales: Option, yes: bool, project_dir: &Path, json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_release_config_import(project_dir, locales.as_deref(), yes, json_output) } - publish::DistributionProvider::AppStore => { + DistributionProvider::AppStore => { app_store_release_config_import(project_dir, locales.as_deref(), yes, json_output) } - publish::DistributionProvider::MicrosoftStore => { - super::microsoft_store_ops::release_config_import( - project_dir, - locales.as_deref(), - yes, - json_output, - ) - } + DistributionProvider::MicrosoftStore => super::microsoft_store_ops::release_config_import( + project_dir, + locales.as_deref(), + yes, + json_output, + ), _ => unsupported_release_config(provider, "import"), } } pub(super) fn release_config_diff( - provider: publish::DistributionProvider, + provider: DistributionProvider, project_dir: &Path, json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { - play_release_config_diff(project_dir, json_output) - } - publish::DistributionProvider::AppStore => { - app_store_release_config_diff(project_dir, json_output) - } - publish::DistributionProvider::MicrosoftStore => { + DistributionProvider::PlayStore => play_release_config_diff(project_dir, json_output), + DistributionProvider::AppStore => app_store_release_config_diff(project_dir, json_output), + DistributionProvider::MicrosoftStore => { super::microsoft_store_ops::release_config_diff(project_dir, json_output) } _ => unsupported_release_config(provider, "diff"), @@ -319,7 +307,7 @@ pub(super) fn release_config_diff( } pub(super) fn release_config_push( - provider: publish::DistributionProvider, + provider: DistributionProvider, locales: Option, dry_run: bool, yes: bool, @@ -327,25 +315,23 @@ pub(super) fn release_config_push( json_output: bool, ) -> Result<()> { match provider { - publish::DistributionProvider::PlayStore => { + DistributionProvider::PlayStore => { play_release_config_push(project_dir, locales.as_deref(), dry_run, yes, json_output) } - publish::DistributionProvider::AppStore => app_store_release_config_push( + DistributionProvider::AppStore => app_store_release_config_push( + project_dir, + locales.as_deref(), + dry_run, + yes, + json_output, + ), + DistributionProvider::MicrosoftStore => super::microsoft_store_ops::release_config_push( project_dir, locales.as_deref(), dry_run, yes, json_output, ), - publish::DistributionProvider::MicrosoftStore => { - super::microsoft_store_ops::release_config_push( - project_dir, - locales.as_deref(), - dry_run, - yes, - json_output, - ) - } _ => unsupported_release_config(provider, "push"), } } @@ -1956,7 +1942,7 @@ fn parse_locale_list(locales: &str) -> Result> { Ok(values) } -fn unsupported_release_config(provider: publish::DistributionProvider, action: &str) -> Result<()> { +fn unsupported_release_config(provider: DistributionProvider, action: &str) -> Result<()> { bail!( "{} release-config {} is not exposed by the current provider API backend; Google Play, App Store, and Microsoft Store metadata import/diff/push are implemented", provider.as_str(), @@ -1964,7 +1950,7 @@ fn unsupported_release_config(provider: publish::DistributionProvider, action: & ) } -fn unsupported_reviews(provider: publish::DistributionProvider, action: &str) -> Result<()> { +fn unsupported_reviews(provider: DistributionProvider, action: &str) -> Result<()> { bail!( "{} review {} is not exposed by the current provider API backend; Google Play and App Store review list/reply are implemented", provider.as_str(), @@ -1972,7 +1958,7 @@ fn unsupported_reviews(provider: publish::DistributionProvider, action: &str) -> ) } -fn unsupported_beta(provider: publish::DistributionProvider, action: &str) -> Result<()> { +fn unsupported_beta(provider: DistributionProvider, action: &str) -> Result<()> { bail!( "{} beta {} is not exposed by the current provider API backend; Google Play group management and App Store TestFlight group/tester management are implemented", provider.as_str(), @@ -2082,7 +2068,7 @@ fn app_store_access_token(cfg: &AppStoreConfig) -> Result { let key_source = env_value("APP_STORE_CONNECT_API_KEY") .or_else(|| env_value("APP_STORE_CONNECT_API_KEY_PATH")) .or(cfg.api_key_path.clone()) - .or_else(|| provider_secret(publish::DistributionProvider::AppStore, &[]).ok().flatten()) + .or_else(|| provider_secret(DistributionProvider::AppStore, &[]).ok().flatten()) .context("APP_STORE_CONNECT_API_KEY, APP_STORE_CONNECT_API_KEY_PATH, distribution.app_store.api_key_path, or vault credentials are required")?; if looks_like_bearer_token(&key_source) { return Ok(key_source); @@ -2302,7 +2288,7 @@ fn google_play_access_token(cfg: &PlayStoreConfig, client: &Client) -> Result Result { fn http_client() -> Result { Client::builder() .timeout(Duration::from_secs(300)) - .user_agent("fission-cli-release/0.1") + .user_agent("cargo-fission-release/0.1") .build() .context("failed to build release HTTP client") } diff --git a/crates/tools/fission-cli/src/release/workflow_ops.rs b/crates/tools/fission-command-release/src/workflow_ops.rs similarity index 100% rename from crates/tools/fission-cli/src/release/workflow_ops.rs rename to crates/tools/fission-command-release/src/workflow_ops.rs diff --git a/crates/tools/fission-command-run/Cargo.toml b/crates/tools/fission-command-run/Cargo.toml new file mode 100644 index 00000000..79df68f3 --- /dev/null +++ b/crates/tools/fission-command-run/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fission-command-run" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Run, build, test, logs, and doctor workflows for the Fission command" + +[dependencies] +anyhow = "1.0" +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +fission-command-site = { path = "../fission-command-site", version = "0.1.1" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/crates/tools/fission-cli/src/doctor.rs b/crates/tools/fission-command-run/src/doctor.rs similarity index 99% rename from crates/tools/fission-cli/src/doctor.rs rename to crates/tools/fission-command-run/src/doctor.rs index 9fdc7414..abf4cf59 100644 --- a/crates/tools/fission-cli/src/doctor.rs +++ b/crates/tools/fission-command-run/src/doctor.rs @@ -1,5 +1,5 @@ -use crate::Target; use anyhow::{bail, Result}; +use fission_command_core::Target; use std::env; use std::ffi::OsStr; use std::fs; @@ -64,7 +64,7 @@ impl Check { } } -pub(crate) fn run_doctor(project_dir: &Path, targets: &[Target], strict: bool) -> Result<()> { +pub fn run_doctor(project_dir: &Path, targets: &[Target], strict: bool) -> Result<()> { let targets = normalized_targets(targets); let mut checks = Vec::new(); @@ -130,7 +130,7 @@ fn check_project(project_dir: &Path, targets: &[Target]) -> Vec { format!("{} scaffold", target.as_str()), format!("{} does not exist", path.display()), format!( - "run `cargo fission add-target {} --project-dir {}`", + "run `fission add-target {} --project-dir {}`", target.as_str(), project_dir.display() ), diff --git a/crates/tools/fission-cli/src/workflow.rs b/crates/tools/fission-command-run/src/lib.rs similarity index 74% rename from crates/tools/fission-cli/src/workflow.rs rename to crates/tools/fission-command-run/src/lib.rs index 93ddf29d..5735fc4d 100644 --- a/crates/tools/fission-cli/src/workflow.rs +++ b/crates/tools/fission-command-run/src/lib.rs @@ -1,70 +1,70 @@ -use crate::{ios_executable_name, read_project_config, FissionProject, Target}; +pub mod doctor; + use anyhow::{bail, Context, Result}; +use fission_command_core::{ios_executable_name, read_project_config, FissionProject, Target}; use serde::Serialize; use std::env; -use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; -use std::io::{self, BufRead, IsTerminal, Read, Seek, Write}; -use std::net::{TcpListener, TcpStream}; +use std::io::{self, IsTerminal, Read, Seek, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; #[derive(Clone, Debug, Serialize)] -pub(crate) struct Device { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) target: Target, - pub(crate) kind: String, - pub(crate) status: String, - pub(crate) detail: String, - pub(crate) available: bool, +pub struct Device { + pub id: String, + pub name: String, + pub target: Target, + pub kind: String, + pub status: String, + pub detail: String, + pub available: bool, } #[derive(Clone, Debug)] -pub(crate) struct RunOptions { - pub(crate) project_dir: PathBuf, - pub(crate) target: Option, - pub(crate) device: Option, - pub(crate) detach: bool, - pub(crate) release: bool, - pub(crate) host: String, - pub(crate) port: u16, - pub(crate) no_open: bool, - pub(crate) headless: bool, +pub struct RunOptions { + pub project_dir: PathBuf, + pub target: Option, + pub device: Option, + pub detach: bool, + pub release: bool, + pub host: String, + pub port: u16, + pub no_open: bool, + pub headless: bool, } #[derive(Clone, Debug)] -pub(crate) struct BuildOptions { - pub(crate) project_dir: PathBuf, - pub(crate) target: Option, - pub(crate) release: bool, +pub struct BuildOptions { + pub project_dir: PathBuf, + pub target: Option, + pub release: bool, } #[derive(Clone, Debug)] -pub(crate) struct TestOptions { - pub(crate) project_dir: PathBuf, - pub(crate) target: Option, - pub(crate) headless: bool, +pub struct TestOptions { + pub project_dir: PathBuf, + pub target: Option, + pub headless: bool, } #[derive(Clone, Debug)] -pub(crate) struct LogOptions { - pub(crate) project_dir: PathBuf, - pub(crate) target: Option, - pub(crate) device: Option, - pub(crate) follow: bool, +pub struct LogOptions { + pub project_dir: PathBuf, + pub target: Option, + pub device: Option, + pub follow: bool, } #[derive(Clone, Debug)] -pub(crate) struct ServeWebOptions { - pub(crate) project_dir: PathBuf, - pub(crate) host: String, - pub(crate) port: u16, - pub(crate) open: bool, +pub struct ServeWebOptions { + pub project_dir: PathBuf, + pub host: String, + pub port: u16, + pub open: bool, } -pub(crate) fn list_devices(project_dir: &Path, json: bool) -> Result<()> { +pub fn list_devices(project_dir: &Path, json: bool) -> Result<()> { let devices = discover_devices(project_dir); if json { println!("{}", serde_json::to_string_pretty(&devices)?); @@ -73,7 +73,10 @@ pub(crate) fn list_devices(project_dir: &Path, json: bool) -> Result<()> { println!("Fission devices"); if devices.is_empty() { - println!("No runnable devices detected. Run `cargo fission doctor web ios android --project-dir {}`.", project_dir.display()); + println!( + "No runnable devices detected. Run `fission doctor web ios android --project-dir {}`.", + project_dir.display() + ); return Ok(()); } @@ -119,7 +122,7 @@ pub(crate) fn list_devices(project_dir: &Path, json: bool) -> Result<()> { Ok(()) } -pub(crate) fn run_app(options: RunOptions) -> Result<()> { +pub fn run_app(options: RunOptions) -> Result<()> { let project = read_project_config(&options.project_dir)?; let device = select_device( &options.project_dir, @@ -143,7 +146,7 @@ pub(crate) fn run_app(options: RunOptions) -> Result<()> { } } -pub(crate) fn build_app(options: BuildOptions) -> Result<()> { +pub fn build_app(options: BuildOptions) -> Result<()> { let project = read_project_config(&options.project_dir)?; let target = options.target.unwrap_or_else(host_desktop_target); ensure_target_configured(&project, &options.project_dir, target)?; @@ -173,7 +176,7 @@ pub(crate) fn build_app(options: BuildOptions) -> Result<()> { } } -pub(crate) fn test_app(options: TestOptions) -> Result<()> { +pub fn test_app(options: TestOptions) -> Result<()> { let project = read_project_config(&options.project_dir)?; let target = options.target.unwrap_or_else(host_desktop_target); ensure_target_configured(&project, &options.project_dir, target)?; @@ -215,7 +218,7 @@ pub(crate) fn test_app(options: TestOptions) -> Result<()> { } } -pub(crate) fn attach_logs(options: LogOptions) -> Result<()> { +pub fn attach_logs(options: LogOptions) -> Result<()> { let project = read_project_config(&options.project_dir)?; let device = select_device( &options.project_dir, @@ -240,8 +243,8 @@ pub(crate) fn attach_logs(options: LogOptions) -> Result<()> { } } -pub(crate) fn serve_web(options: ServeWebOptions) -> Result<()> { - serve_static( +pub fn serve_web(options: ServeWebOptions) -> Result<()> { + fission_command_site::serve_static( options.project_dir, options.host, options.port, @@ -249,140 +252,29 @@ pub(crate) fn serve_web(options: ServeWebOptions) -> Result<()> { ) } -pub(crate) fn site_build(project_dir: &Path, release: bool) -> Result<()> { - if site_entry_configured(project_dir)? { - return run_site_builder(project_dir, release, "build", &[]); - } - let project = read_project_config(project_dir)?; - let options = site_build_options(project_dir, &project)?; - let report = fission_shell_site::build_content_site(&options)?; - println!( - "Built {} static route(s) into {}", - report.routes.len(), - report.output_dir.display() - ); - for route in report.routes { - println!("{} -> {}", route.path, route.output.display()); - } - Ok(()) +pub fn site_build(project_dir: &Path, release: bool) -> Result<()> { + fission_command_site::build(project_dir, release) } -pub(crate) fn site_check(project_dir: &Path, release: bool) -> Result<()> { - if site_entry_configured(project_dir)? { - return run_site_builder(project_dir, release, "check", &[]); - } - let project = read_project_config(project_dir)?; - let options = site_build_options(project_dir, &project)?; - let report = fission_shell_site::check_content_site(&options)?; - println!( - "Checked {} static route(s); output would be {}", - report.routes.len(), - report.output_dir.display() - ); - Ok(()) +pub fn site_check(project_dir: &Path, release: bool) -> Result<()> { + fission_command_site::check(project_dir, release) } -pub(crate) fn site_routes(project_dir: &Path) -> Result<()> { - if site_entry_configured(project_dir)? { - return run_site_builder(project_dir, false, "routes", &[]); - } - let project = read_project_config(project_dir)?; - let options = site_build_options(project_dir, &project)?; - let routes = fission_shell_site::list_content_routes(&options)?; - for route in routes { - println!( - "{} {} {}", - route.path, - route.title, - route.source.display() - ); - } - Ok(()) +pub fn site_routes(project_dir: &Path) -> Result<()> { + fission_command_site::routes(project_dir) } -pub(crate) fn site_serve( +pub fn site_serve( project_dir: &Path, release: bool, host: String, port: u16, open: bool, ) -> Result<()> { - if site_entry_configured(project_dir)? { - let port = port.to_string(); - let open_flag = if open { "" } else { "--no-open" }; - let mut args = vec!["--host", host.as_str(), "--port", port.as_str()]; - if !open { - args.push(open_flag); - } - return run_site_builder(project_dir, release, "serve", &args); - } - site_build(project_dir, release)?; - let project = read_project_config(project_dir)?; - let options = site_build_options(project_dir, &project)?; - serve_static(options.output_dir, host, port, open) -} - -fn site_build_options( - project_dir: &Path, - project: &FissionProject, -) -> Result { - fission_shell_site::SiteBuildOptions::from_project_dir(project_dir, project.app.name.clone()) - .or_else(|_| { - Ok(fission_shell_site::SiteBuildOptions::for_project( - project_dir, - project.app.name.clone(), - )) - }) -} - -fn site_entry_configured(project_dir: &Path) -> Result { - let path = project_dir.join("fission.toml"); - let data = - fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; - let value: toml::Value = - toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?; - Ok(value - .get("site") - .and_then(|site| site.get("entry")) - .and_then(|entry| entry.as_str()) - .is_some()) + fission_command_site::serve(project_dir, release, host, port, open) } -fn run_site_builder( - project_dir: &Path, - release: bool, - command_name: &str, - extra_args: &[&str], -) -> Result<()> { - let manifest_path = project_dir.join("Cargo.toml"); - if !manifest_path.exists() { - bail!( - "site entry is configured but {} is missing", - manifest_path.display() - ); - } - let mut command = Command::new("cargo"); - command - .arg("run") - .arg("--manifest-path") - .arg(&manifest_path); - if release { - command.arg("--release"); - } - command - .arg("--") - .arg(command_name) - .arg("--project-dir") - .arg(project_dir); - for arg in extra_args { - if !arg.is_empty() { - command.arg(arg); - } - } - run_status(&mut command, "site builder") -} - -pub(crate) fn discover_devices(_project_dir: &Path) -> Vec { +pub fn discover_devices(_project_dir: &Path) -> Vec { let mut devices = Vec::new(); devices.push(Device { id: "desktop".to_string(), @@ -461,7 +353,7 @@ fn select_device( } return match matches.len() { 0 => bail!( - "no device matched `{query}`; run `cargo fission devices --project-dir {}`", + "no device matched `{query}`; run `fission devices --project-dir {}`", project_dir.display() ), 1 => Ok(matches[0].clone()), @@ -478,7 +370,7 @@ fn select_device( if devices.is_empty() { bail!( - "no runnable devices detected; run `cargo fission devices --project-dir {}`", + "no runnable devices detected; run `fission devices --project-dir {}`", project_dir.display() ); } @@ -527,7 +419,7 @@ fn ensure_target_configured( ) -> Result<()> { if !project.targets.contains(&target) { bail!( - "target `{}` is not configured for this app; run `cargo fission add-target {} --project-dir {}`", + "target `{}` is not configured for this app; run `fission add-target {} --project-dir {}`", target.as_str(), target.as_str(), project_dir.display() @@ -536,7 +428,7 @@ fn ensure_target_configured( let scaffold = project_dir.join(target.scaffold_relative_path()); if !scaffold.exists() { bail!( - "target `{}` scaffold is missing at {}; run `cargo fission add-target {} --project-dir {}`", + "target `{}` scaffold is missing at {}; run `fission add-target {} --project-dir {}`", target.as_str(), scaffold.display(), target.as_str(), @@ -585,13 +477,13 @@ fn run_web(options: &RunOptions, _device: &Device) -> Result<()> { println!( "Started web server pid {} at {}. Logs: {}", child.id(), - web_url(&options.host, options.port), + format!("http://{}:{}/platforms/web/", options.host, options.port), log_path.display() ); return Ok(()); } - serve_static( + fission_command_site::serve_static( options.project_dir.clone(), options.host.clone(), options.port, @@ -882,138 +774,6 @@ fn open_log(path: &Path) -> Result { .with_context(|| format!("failed to open log file {}", path.display())) } -fn serve_static(root: PathBuf, host: String, port: u16, open: bool) -> Result<()> { - let listener = TcpListener::bind((host.as_str(), port)) - .with_context(|| format!("failed to bind {}:{}", host, port))?; - let url = if root.join("index.html").exists() { - format!("http://{host}:{port}/") - } else { - web_url(&host, port) - }; - println!("Serving {} at {}", root.display(), url); - println!("Press Ctrl+C to stop."); - if open { - let _ = open_url(&url); - } - for stream in listener.incoming() { - match stream { - Ok(stream) => { - if let Err(error) = handle_http_request(stream, &root) { - eprintln!("request failed: {error}"); - } - } - Err(error) => eprintln!("accept failed: {error}"), - } - } - Ok(()) -} - -fn handle_http_request(mut stream: TcpStream, root: &Path) -> Result<()> { - let mut reader = io::BufReader::new(stream.try_clone()?); - let mut request_line = String::new(); - reader.read_line(&mut request_line)?; - let path = request_line - .split_whitespace() - .nth(1) - .unwrap_or("/") - .split('?') - .next() - .unwrap_or("/"); - let response = static_response(root, path)?; - stream.write_all(&response)?; - Ok(()) -} - -fn static_response(root: &Path, request_path: &str) -> Result> { - let mut relative = request_path.trim_start_matches('/').to_string(); - if relative.is_empty() { - relative = if root.join("index.html").exists() { - "index.html".to_string() - } else { - "platforms/web/".to_string() - }; - } - if relative.ends_with('/') { - relative.push_str("index.html"); - } - if !relative.ends_with(".html") && !relative.contains('.') { - relative.push_str("/index.html"); - } - let path = sanitize_static_path(root, &relative)?; - if !path.exists() || !path.is_file() { - println!("GET {} 404", request_path); - return Ok(http_response(404, "text/plain", b"not found")); - } - let body = fs::read(&path)?; - let content_type = content_type(&path); - println!("GET {} 200", request_path); - Ok(http_response(200, content_type, &body)) -} - -fn sanitize_static_path(root: &Path, relative: &str) -> Result { - let mut path = PathBuf::from(root); - for part in relative.split('/') { - if part.is_empty() || part == "." { - continue; - } - if part == ".." || part.contains('\\') { - bail!("invalid static path `{relative}`"); - } - path.push(part); - } - Ok(path) -} - -fn http_response(status: u16, content_type: &str, body: &[u8]) -> Vec { - let reason = match status { - 200 => "OK", - 404 => "Not Found", - _ => "Error", - }; - let mut response = format!( - "HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\nContent-Type: {content_type}\r\nConnection: close\r\n\r\n", - body.len() - ) - .into_bytes(); - response.extend_from_slice(body); - response -} - -fn content_type(path: &Path) -> &'static str { - match path.extension().and_then(OsStr::to_str).unwrap_or("") { - "html" => "text/html; charset=utf-8", - "js" | "mjs" => "text/javascript; charset=utf-8", - "wasm" => "application/wasm", - "json" => "application/json; charset=utf-8", - "png" => "image/png", - "svg" => "image/svg+xml", - "css" => "text/css; charset=utf-8", - _ => "application/octet-stream", - } -} - -fn web_url(host: &str, port: u16) -> String { - format!("http://{host}:{port}/platforms/web/") -} - -fn open_url(url: &str) -> Result<()> { - let mut command = if cfg!(target_os = "macos") { - let mut cmd = Command::new("open"); - cmd.arg(url); - cmd - } else if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.args(["/C", "start", "", url]); - cmd - } else { - let mut cmd = Command::new("xdg-open"); - cmd.arg(url); - cmd - }; - command.spawn()?; - Ok(()) -} - fn discover_ios_simulators() -> Vec { if !cfg!(target_os = "macos") || find_in_path("xcrun").is_none() { return Vec::new(); @@ -1184,7 +944,7 @@ fn first_android_serial() -> Option { fn adb_path() -> Result { android_tool("platform-tools/adb") - .context("Android adb was not found; run `cargo fission doctor android`") + .context("Android adb was not found; run `fission doctor android`") } fn android_tool(relative: &str) -> Option { diff --git a/crates/tools/fission-command-site/Cargo.toml b/crates/tools/fission-command-site/Cargo.toml new file mode 100644 index 00000000..b3aecb58 --- /dev/null +++ b/crates/tools/fission-command-site/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fission-command-site" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Static site command implementation for the Fission command" + +[dependencies] +anyhow = "1.0" +fission-shell-site = { path = "../../shell/fission-shell-site", version = "0.1.1" } +toml = "0.8" diff --git a/crates/tools/fission-command-site/src/lib.rs b/crates/tools/fission-command-site/src/lib.rs new file mode 100644 index 00000000..9dd2173f --- /dev/null +++ b/crates/tools/fission-command-site/src/lib.rs @@ -0,0 +1,281 @@ +use anyhow::{bail, Context, Result}; +use std::ffi::OsStr; +use std::fs; +use std::io::{self, BufRead, Write}; +use std::net::{TcpListener, TcpStream}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +pub fn build(project_dir: &Path, release: bool) -> Result<()> { + if site_entry_configured(project_dir)? { + return run_site_builder(project_dir, release, "build", &[]); + } + let options = site_build_options(project_dir)?; + let report = fission_shell_site::build_content_site(&options)?; + println!( + "Built {} static route(s) into {}", + report.routes.len(), + report.output_dir.display() + ); + for route in report.routes { + println!("{} -> {}", route.path, route.output.display()); + } + Ok(()) +} + +pub fn check(project_dir: &Path, release: bool) -> Result<()> { + if site_entry_configured(project_dir)? { + return run_site_builder(project_dir, release, "check", &[]); + } + let options = site_build_options(project_dir)?; + let report = fission_shell_site::check_content_site(&options)?; + println!( + "Checked {} static route(s); output would be {}", + report.routes.len(), + report.output_dir.display() + ); + Ok(()) +} + +pub fn routes(project_dir: &Path) -> Result<()> { + if site_entry_configured(project_dir)? { + return run_site_builder(project_dir, false, "routes", &[]); + } + let options = site_build_options(project_dir)?; + let routes = fission_shell_site::list_content_routes(&options)?; + for route in routes { + println!( + "{} {} {}", + route.path, + route.title, + route.source.display() + ); + } + Ok(()) +} + +pub fn serve(project_dir: &Path, release: bool, host: String, port: u16, open: bool) -> Result<()> { + if site_entry_configured(project_dir)? { + let port = port.to_string(); + let open_flag = if open { "" } else { "--no-open" }; + let mut args = vec!["--host", host.as_str(), "--port", port.as_str()]; + if !open { + args.push(open_flag); + } + return run_site_builder(project_dir, release, "serve", &args); + } + build(project_dir, release)?; + let options = site_build_options(project_dir)?; + serve_static(options.output_dir, host, port, open) +} + +pub fn serve_static(root: PathBuf, host: String, port: u16, open: bool) -> Result<()> { + let listener = TcpListener::bind((host.as_str(), port)) + .with_context(|| format!("failed to bind {}:{}", host, port))?; + let url = if root.join("index.html").exists() { + format!("http://{host}:{port}/") + } else { + format!("http://{host}:{port}/platforms/web/") + }; + println!("Serving {} at {}", root.display(), url); + println!("Press Ctrl+C to stop."); + if open { + let _ = open_url(&url); + } + for stream in listener.incoming() { + match stream { + Ok(stream) => { + if let Err(error) = handle_http_request(stream, &root) { + eprintln!("request failed: {error}"); + } + } + Err(error) => eprintln!("accept failed: {error}"), + } + } + Ok(()) +} + +fn site_build_options(project_dir: &Path) -> Result { + let app_name = project_name(project_dir)?; + fission_shell_site::SiteBuildOptions::from_project_dir(project_dir, app_name.clone()).or_else( + |_| { + Ok(fission_shell_site::SiteBuildOptions::for_project( + project_dir, + app_name, + )) + }, + ) +} + +fn project_name(project_dir: &Path) -> Result { + let path = project_dir.join("fission.toml"); + let data = + fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + let value: toml::Value = + toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?; + value + .get("app") + .and_then(|app| app.get("name")) + .and_then(|name| name.as_str()) + .map(ToString::to_string) + .context("fission.toml is missing app.name") +} + +fn site_entry_configured(project_dir: &Path) -> Result { + let path = project_dir.join("fission.toml"); + let data = + fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?; + let value: toml::Value = + toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?; + Ok(value + .get("site") + .and_then(|site| site.get("entry")) + .and_then(|entry| entry.as_str()) + .is_some()) +} + +fn run_site_builder( + project_dir: &Path, + release: bool, + command_name: &str, + extra_args: &[&str], +) -> Result<()> { + let manifest_path = project_dir.join("Cargo.toml"); + if !manifest_path.exists() { + bail!( + "site entry is configured but {} is missing", + manifest_path.display() + ); + } + let mut command = Command::new("cargo"); + command + .arg("run") + .arg("--manifest-path") + .arg(&manifest_path); + if release { + command.arg("--release"); + } + command + .arg("--") + .arg(command_name) + .arg("--project-dir") + .arg(project_dir); + for arg in extra_args { + if !arg.is_empty() { + command.arg(arg); + } + } + run_status(&mut command, "site builder") +} + +fn run_status(command: &mut Command, label: &str) -> Result<()> { + let status = command + .status() + .with_context(|| format!("failed to run {label}"))?; + if !status.success() { + bail!("{label} failed with {status}"); + } + Ok(()) +} + +fn handle_http_request(mut stream: TcpStream, root: &Path) -> Result<()> { + let mut reader = io::BufReader::new(stream.try_clone()?); + let mut request_line = String::new(); + reader.read_line(&mut request_line)?; + let path = request_line + .split_whitespace() + .nth(1) + .unwrap_or("/") + .split('?') + .next() + .unwrap_or("/"); + let response = static_response(root, path)?; + stream.write_all(&response)?; + Ok(()) +} + +fn static_response(root: &Path, request_path: &str) -> Result> { + let mut relative = request_path.trim_start_matches('/').to_string(); + if relative.is_empty() { + relative = if root.join("index.html").exists() { + "index.html".to_string() + } else { + "platforms/web/".to_string() + }; + } + if relative.ends_with('/') { + relative.push_str("index.html"); + } + if !relative.ends_with(".html") && !relative.contains('.') { + relative.push_str("/index.html"); + } + let path = sanitize_static_path(root, &relative)?; + if !path.exists() || !path.is_file() { + println!("GET {} 404", request_path); + return Ok(http_response(404, "text/plain", b"not found")); + } + let body = fs::read(&path)?; + let content_type = content_type(&path); + println!("GET {} 200", request_path); + Ok(http_response(200, content_type, &body)) +} + +fn sanitize_static_path(root: &Path, relative: &str) -> Result { + let mut path = PathBuf::from(root); + for part in relative.split('/') { + if part.is_empty() || part == "." { + continue; + } + if part == ".." || part.contains('\\') { + bail!("invalid static path `{relative}`"); + } + path.push(part); + } + Ok(path) +} + +fn http_response(status: u16, content_type: &str, body: &[u8]) -> Vec { + let reason = match status { + 200 => "OK", + 404 => "Not Found", + _ => "Error", + }; + let mut response = format!( + "HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\nContent-Type: {content_type}\r\nConnection: close\r\n\r\n", + body.len() + ) + .into_bytes(); + response.extend_from_slice(body); + response +} + +fn content_type(path: &Path) -> &'static str { + match path.extension().and_then(OsStr::to_str).unwrap_or("") { + "html" => "text/html; charset=utf-8", + "js" | "mjs" => "text/javascript; charset=utf-8", + "wasm" => "application/wasm", + "json" => "application/json; charset=utf-8", + "png" => "image/png", + "svg" => "image/svg+xml", + "css" => "text/css; charset=utf-8", + _ => "application/octet-stream", + } +} + +fn open_url(url: &str) -> Result<()> { + let mut command = if cfg!(target_os = "macos") { + let mut cmd = Command::new("open"); + cmd.arg(url); + cmd + } else if cfg!(target_os = "windows") { + let mut cmd = Command::new("cmd"); + cmd.args(["/C", "start", "", url]); + cmd + } else { + let mut cmd = Command::new("xdg-open"); + cmd.arg(url); + cmd + }; + command.spawn()?; + Ok(()) +} diff --git a/crates/tools/fission-command-ui/Cargo.toml b/crates/tools/fission-command-ui/Cargo.toml new file mode 100644 index 00000000..94ee4b3e --- /dev/null +++ b/crates/tools/fission-command-ui/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fission-command-ui" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Terminal UI application for the Fission command" + +[dependencies] +anyhow = "1.0" +fission = { path = "../../authoring/fission", version = "0.1.1", default-features = false, features = ["terminal-shell"] } +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +fission-command-run = { path = "../fission-command-run", version = "0.1.1" } +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/tools/fission-cli/src/ui/actions.rs b/crates/tools/fission-command-ui/src/actions.rs similarity index 68% rename from crates/tools/fission-cli/src/ui/actions.rs rename to crates/tools/fission-command-ui/src/actions.rs index 7b9c0d00..4ff1e580 100644 --- a/crates/tools/fission-cli/src/ui/actions.rs +++ b/crates/tools/fission-command-ui/src/actions.rs @@ -1,26 +1,26 @@ use super::commands::{execute_ui_command, UiCommand}; use super::routes::UiRoute; use super::state::{UiDialog, UiState}; -use crate::Target; use fission::prelude::*; +use fission_command_core::Target; #[fission_reducer(Navigate)] -pub(crate) fn navigate(state: &mut UiState, route: UiRoute) { +pub fn navigate(state: &mut UiState, route: UiRoute) { state.route = route; } #[fission_reducer(ToggleTheme)] -pub(crate) fn toggle_theme(state: &mut UiState) { +pub fn toggle_theme(state: &mut UiState) { state.theme_mode = state.theme_mode.toggle(); } #[fission_reducer(ToggleCompactMode)] -pub(crate) fn toggle_compact_mode(state: &mut UiState) { +pub fn toggle_compact_mode(state: &mut UiState) { state.compact_mode = !state.compact_mode; } #[fission_reducer(SelectTarget)] -pub(crate) fn select_target(state: &mut UiState, target: Target) { +pub fn select_target(state: &mut UiState, target: Target) { state.selected_target = Some(target); state.selected_device = state .devices @@ -30,42 +30,42 @@ pub(crate) fn select_target(state: &mut UiState, target: Target) { } #[fission_reducer(SelectDevice)] -pub(crate) fn select_device(state: &mut UiState, id: String) { +pub fn select_device(state: &mut UiState, id: String) { state.selected_device = Some(id); } #[fission_reducer(SelectCommandSession)] -pub(crate) fn select_command_session(state: &mut UiState, id: u64) { +pub fn select_command_session(state: &mut UiState, id: u64) { state.select_command_session(id); } #[fission_reducer(SetInitName)] -pub(crate) fn set_init_name(state: &mut UiState, value: String) { +pub fn set_init_name(state: &mut UiState, value: String) { state.init_name = value; } #[fission_reducer(SetInitAppId)] -pub(crate) fn set_init_app_id(state: &mut UiState, value: String) { +pub fn set_init_app_id(state: &mut UiState, value: String) { state.init_app_id = value; } #[fission_reducer(SetInitLocalPath)] -pub(crate) fn set_init_local_path(state: &mut UiState, value: String) { +pub fn set_init_local_path(state: &mut UiState, value: String) { state.init_local_path = value; } #[fission_reducer(SetHost)] -pub(crate) fn set_host(state: &mut UiState, value: String) { +pub fn set_host(state: &mut UiState, value: String) { state.host = value; } #[fission_reducer(SetPort)] -pub(crate) fn set_port(state: &mut UiState, value: String) { +pub fn set_port(state: &mut UiState, value: String) { state.port = value; } #[fission_reducer(SetScrollbackLimitInput)] -pub(crate) fn set_scrollback_limit_input(state: &mut UiState, value: String) { +pub fn set_scrollback_limit_input(state: &mut UiState, value: String) { state.scrollback_limit_input = value.clone(); if let Some(limit) = parse_scrollback_limit(&value) { state.set_scrollback_limit(limit); @@ -73,47 +73,47 @@ pub(crate) fn set_scrollback_limit_input(state: &mut UiState, value: String) { } #[fission_reducer(SetScrollbackLimit)] -pub(crate) fn set_scrollback_limit(state: &mut UiState, limit: usize) { +pub fn set_scrollback_limit(state: &mut UiState, limit: usize) { state.set_scrollback_limit(limit); } #[fission_reducer(ToggleStrict)] -pub(crate) fn toggle_strict(state: &mut UiState) { +pub fn toggle_strict(state: &mut UiState) { state.strict = !state.strict; } #[fission_reducer(ToggleRelease)] -pub(crate) fn toggle_release(state: &mut UiState) { +pub fn toggle_release(state: &mut UiState) { state.release = !state.release; } #[fission_reducer(ToggleDetach)] -pub(crate) fn toggle_detach(state: &mut UiState) { +pub fn toggle_detach(state: &mut UiState) { state.detach = !state.detach; } #[fission_reducer(ToggleNoOpen)] -pub(crate) fn toggle_no_open(state: &mut UiState) { +pub fn toggle_no_open(state: &mut UiState) { state.no_open = !state.no_open; } #[fission_reducer(ToggleHeadless)] -pub(crate) fn toggle_headless(state: &mut UiState) { +pub fn toggle_headless(state: &mut UiState) { state.headless = !state.headless; } #[fission_reducer(ExecuteCommand)] -pub(crate) fn execute_command(state: &mut UiState, command: UiCommand) { +pub fn execute_command(state: &mut UiState, command: UiCommand) { execute_ui_command(state, command); } #[fission_reducer(RequestCommand)] -pub(crate) fn request_command(state: &mut UiState, command: UiCommand) { +pub fn request_command(state: &mut UiState, command: UiCommand) { state.request_command_confirmation(command); } #[fission_reducer(ConfirmDialog)] -pub(crate) fn confirm_dialog(state: &mut UiState) { +pub fn confirm_dialog(state: &mut UiState) { let Some(dialog) = state.pending_dialog.take() else { return; }; @@ -126,7 +126,7 @@ pub(crate) fn confirm_dialog(state: &mut UiState) { } #[fission_reducer(CancelDialog)] -pub(crate) fn cancel_dialog(state: &mut UiState) { +pub fn cancel_dialog(state: &mut UiState) { state.pending_dialog = None; } diff --git a/crates/tools/fission-cli/src/ui/app.rs b/crates/tools/fission-command-ui/src/app.rs similarity index 95% rename from crates/tools/fission-cli/src/ui/app.rs rename to crates/tools/fission-command-ui/src/app.rs index 186b9f87..45bc8ba2 100644 --- a/crates/tools/fission-cli/src/ui/app.rs +++ b/crates/tools/fission-command-ui/src/app.rs @@ -4,7 +4,7 @@ use super::state::UiState; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct CliUiApp; +pub struct CliUiApp; impl Widget for CliUiApp { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/commands.rs b/crates/tools/fission-command-ui/src/commands.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/commands.rs rename to crates/tools/fission-command-ui/src/commands.rs index e609c308..11262506 100644 --- a/crates/tools/fission-cli/src/ui/commands.rs +++ b/crates/tools/fission-command-ui/src/commands.rs @@ -1,5 +1,5 @@ use super::state::UiState; -use crate::Target; +use fission_command_core::Target; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::io::{BufRead, BufReader}; @@ -9,12 +9,12 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -pub(crate) const DEFAULT_SCROLLBACK_LINES: usize = 100_000; +pub const DEFAULT_SCROLLBACK_LINES: usize = 100_000; const MAX_SCROLLBACK_LINE_CHARS: usize = 4096; -pub(crate) type CommandSessionId = u64; +pub type CommandSessionId = u64; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) enum UiCommand { +pub enum UiCommand { InitProject, AddTarget(Target), DoctorAll, @@ -32,7 +32,7 @@ pub(crate) enum UiCommand { } impl UiCommand { - pub(crate) fn label(&self) -> String { + pub fn label(&self) -> String { match self { Self::InitProject => "initialise this project".to_string(), Self::AddTarget(target) => format!("add the {} target", target.as_str()), @@ -51,7 +51,7 @@ impl UiCommand { } } - pub(crate) fn confirmation_message(&self) -> String { + pub fn confirmation_message(&self) -> String { match self { Self::InitProject => { "This writes missing Fission project files only when they are absent.".to_string() @@ -83,14 +83,14 @@ impl UiCommand { } #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub(crate) struct CommandRecord { - pub(crate) title: String, - pub(crate) status: CommandStatus, - pub(crate) output: ScrollbackBuffer, +pub struct CommandRecord { + pub title: String, + pub status: CommandStatus, + pub output: ScrollbackBuffer, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub(crate) enum CommandStatus { +pub enum CommandStatus { #[default] Ready, Running, @@ -99,7 +99,7 @@ pub(crate) enum CommandStatus { } #[derive(Clone, Debug)] -pub(crate) struct ScrollbackBuffer { +pub struct ScrollbackBuffer { inner: Arc>, } @@ -117,7 +117,7 @@ impl Default for ScrollbackBuffer { } impl ScrollbackBuffer { - pub(crate) fn new(limit: usize) -> Self { + pub fn new(limit: usize) -> Self { Self { inner: Arc::new(Mutex::new(ScrollbackBufferData { limit: limit.max(1), @@ -127,7 +127,7 @@ impl ScrollbackBuffer { } } - pub(crate) fn from_text(limit: usize, text: impl AsRef) -> Self { + pub fn from_text(limit: usize, text: impl AsRef) -> Self { let mut buffer = Self::new(limit); for line in text.as_ref().lines() { buffer.push_line(line); @@ -138,24 +138,24 @@ impl ScrollbackBuffer { buffer } - pub(crate) fn set_limit(&mut self, limit: usize) { + pub fn set_limit(&mut self, limit: usize) { let mut data = self.inner.lock().expect("scrollback lock poisoned"); data.limit = limit.max(1); data.trim_to_limit(); } - pub(crate) fn push_line(&mut self, line: &str) { + pub fn push_line(&mut self, line: &str) { let mut data = self.inner.lock().expect("scrollback lock poisoned"); data.lines.push_back(truncate_line(line)); data.trim_to_limit(); } - pub(crate) fn display_line_count(&self) -> usize { + pub fn display_line_count(&self) -> usize { let data = self.inner.lock().expect("scrollback lock poisoned"); data.lines.len() + usize::from(data.dropped_lines > 0) } - pub(crate) fn visible_lines(&self, start: usize, count: usize) -> Vec { + pub fn visible_lines(&self, start: usize, count: usize) -> Vec { let mut visible = Vec::new(); if count == 0 { return visible; @@ -204,7 +204,7 @@ impl ScrollbackBufferData { } impl CommandStatus { - pub(crate) fn label(self) -> &'static str { + pub fn label(self) -> &'static str { match self { Self::Ready => "Ready", Self::Running => "Running", @@ -215,21 +215,21 @@ impl CommandStatus { } #[derive(Clone, Debug, PartialEq)] -pub(crate) struct CommandSnapshot { - pub(crate) id: CommandSessionId, - pub(crate) revision: u64, - pub(crate) record: CommandRecord, - pub(crate) finished: bool, +pub struct CommandSnapshot { + pub id: CommandSessionId, + pub revision: u64, + pub record: CommandRecord, + pub finished: bool, } #[derive(Clone, Debug, Default, PartialEq)] -pub(crate) struct CommandRuntimeSnapshot { - pub(crate) active_session_id: Option, - pub(crate) sessions: Vec, +pub struct CommandRuntimeSnapshot { + pub active_session_id: Option, + pub sessions: Vec, } #[derive(Clone, Default)] -pub(crate) struct CommandRuntime { +pub struct CommandRuntime { inner: Arc>, } @@ -281,7 +281,7 @@ impl CommandRuntime { snapshot.revision = snapshot.revision.saturating_add(1); } - pub(crate) fn snapshot(&self) -> CommandRuntimeSnapshot { + pub fn snapshot(&self) -> CommandRuntimeSnapshot { let state = self.inner.lock().expect("command runtime lock poisoned"); CommandRuntimeSnapshot { active_session_id: state.active_session_id, @@ -289,14 +289,14 @@ impl CommandRuntime { } } - pub(crate) fn set_active(&self, session_id: CommandSessionId) { + pub fn set_active(&self, session_id: CommandSessionId) { let mut state = self.inner.lock().expect("command runtime lock poisoned"); if state.sessions.iter().any(|item| item.id == session_id) { state.active_session_id = Some(session_id); } } - pub(crate) fn record_completed( + pub fn record_completed( &self, mut record: CommandRecord, limit: usize, @@ -316,7 +316,7 @@ impl CommandRuntime { session_id } - pub(crate) fn set_limit(&self, limit: usize) { + pub fn set_limit(&self, limit: usize) { let mut state = self.inner.lock().expect("command runtime lock poisoned"); for snapshot in &mut state.sessions { snapshot.record.output.set_limit(limit); @@ -336,7 +336,7 @@ enum CommandMode { Capture, } -pub(crate) fn execute_ui_command(state: &mut UiState, command: UiCommand) { +pub fn execute_ui_command(state: &mut UiState, command: UiCommand) { if matches!(command, UiCommand::Refresh) { state.refresh(); let record = CommandRecord { diff --git a/crates/tools/fission-cli/src/ui/components/chrome.rs b/crates/tools/fission-command-ui/src/components/chrome.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/components/chrome.rs rename to crates/tools/fission-command-ui/src/components/chrome.rs index eafe30e2..c7e74003 100644 --- a/crates/tools/fission-cli/src/ui/components/chrome.rs +++ b/crates/tools/fission-command-ui/src/components/chrome.rs @@ -1,9 +1,9 @@ use super::{ActionButton, ButtonTone, OutputPanel}; -use crate::ui::actions::{navigate, toggle_theme, Navigate, ToggleTheme}; -use crate::ui::density::UiDensity; -use crate::ui::routes::UiRoute; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::actions::{navigate, toggle_theme, Navigate, ToggleTheme}; +use crate::density::UiDensity; +use crate::routes::UiRoute; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::ir::op::{AlignItems, JustifyContent}; use fission::ir::NodeId; use fission::prelude::*; @@ -11,8 +11,8 @@ use fission::prelude::*; const NAV_SCROLL_NODE_ID: &str = "cli_ui_nav_scroll"; #[derive(Clone)] -pub(crate) struct AppShell { - pub(crate) content: Node, +pub struct AppShell { + pub content: Node, } impl Widget for AppShell { @@ -73,7 +73,7 @@ impl Widget for AppShell { } #[derive(Clone)] -pub(crate) struct AppHeader; +pub struct AppHeader; impl Widget for AppHeader { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { @@ -88,7 +88,7 @@ impl Widget for AppHeader { Column { gap: Some(0.0), children: vec![ - Text::new("Fission CLI") + Text::new("Fission command") .color(palette.accent_text) .into_node(), Text::new(format!( @@ -132,9 +132,9 @@ impl Widget for AppHeader { } #[derive(Clone)] -pub(crate) struct Sidebar { - pub(crate) width: f32, - pub(crate) height: f32, +pub struct Sidebar { + pub width: f32, + pub height: f32, } impl Widget for Sidebar { diff --git a/crates/tools/fission-cli/src/ui/components/controls.rs b/crates/tools/fission-command-ui/src/components/controls.rs similarity index 79% rename from crates/tools/fission-cli/src/ui/components/controls.rs rename to crates/tools/fission-command-ui/src/components/controls.rs index fdc857ba..483e3cb1 100644 --- a/crates/tools/fission-cli/src/ui/components/controls.rs +++ b/crates/tools/fission-command-ui/src/components/controls.rs @@ -1,11 +1,11 @@ -use crate::ui::density::UiDensity; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::density::UiDensity; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::ir::op::Fill; use fission::prelude::*; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum ButtonTone { +pub enum ButtonTone { Primary, Neutral, Success, @@ -13,15 +13,15 @@ pub(crate) enum ButtonTone { } #[derive(Clone)] -pub(crate) struct ActionButton { - pub(crate) label: String, - pub(crate) action: ActionEnvelope, - pub(crate) tone: ButtonTone, - pub(crate) width: Option, +pub struct ActionButton { + pub label: String, + pub action: ActionEnvelope, + pub tone: ButtonTone, + pub width: Option, } impl ActionButton { - pub(crate) fn new(label: impl Into, action: ActionEnvelope) -> Self { + pub fn new(label: impl Into, action: ActionEnvelope) -> Self { Self { label: label.into(), action, @@ -30,12 +30,12 @@ impl ActionButton { } } - pub(crate) fn tone(mut self, tone: ButtonTone) -> Self { + pub fn tone(mut self, tone: ButtonTone) -> Self { self.tone = tone; self } - pub(crate) fn width(mut self, width: f32) -> Self { + pub fn width(mut self, width: f32) -> Self { self.width = Some(width); self } @@ -73,14 +73,14 @@ impl Widget for ActionButton { } #[derive(Clone)] -pub(crate) struct TogglePill { - pub(crate) label: String, - pub(crate) enabled: bool, - pub(crate) action: ActionEnvelope, +pub struct TogglePill { + pub label: String, + pub enabled: bool, + pub action: ActionEnvelope, } impl TogglePill { - pub(crate) fn new(label: impl Into, enabled: bool, action: ActionEnvelope) -> Self { + pub fn new(label: impl Into, enabled: bool, action: ActionEnvelope) -> Self { Self { label: label.into(), enabled, @@ -108,17 +108,17 @@ impl Widget for TogglePill { } #[derive(Clone)] -pub(crate) struct FormTextField { - pub(crate) id: &'static str, - pub(crate) label: String, - pub(crate) value: String, - pub(crate) placeholder: String, - pub(crate) action: ActionEnvelope, - pub(crate) width: f32, +pub struct FormTextField { + pub id: &'static str, + pub label: String, + pub value: String, + pub placeholder: String, + pub action: ActionEnvelope, + pub width: f32, } impl FormTextField { - pub(crate) fn new( + pub fn new( id: &'static str, label: impl Into, value: impl Into, @@ -135,7 +135,7 @@ impl FormTextField { } } - pub(crate) fn width(mut self, width: f32) -> Self { + pub fn width(mut self, width: f32) -> Self { self.width = width; self } diff --git a/crates/tools/fission-cli/src/ui/components/data.rs b/crates/tools/fission-command-ui/src/components/data.rs similarity index 89% rename from crates/tools/fission-cli/src/ui/components/data.rs rename to crates/tools/fission-command-ui/src/components/data.rs index e9e5ec90..39223c68 100644 --- a/crates/tools/fission-cli/src/ui/components/data.rs +++ b/crates/tools/fission-command-ui/src/components/data.rs @@ -1,20 +1,20 @@ use super::{ActionButton, ButtonTone}; -use crate::ui::actions::{select_device, select_target, SelectDevice, SelectTarget}; -use crate::ui::density::UiDensity; -use crate::ui::state::{all_targets, target_label, UiDevice, UiState}; -use crate::ui::theme::UiPalette; -use crate::Target; +use crate::actions::{select_device, select_target, SelectDevice, SelectTarget}; +use crate::density::UiDensity; +use crate::state::{all_targets, target_label, UiDevice, UiState}; +use crate::theme::UiPalette; use fission::ir::op::AlignItems; use fission::prelude::*; +use fission_command_core::Target; #[derive(Clone)] -pub(crate) struct KeyValueRow { - pub(crate) label: String, - pub(crate) value: String, +pub struct KeyValueRow { + pub label: String, + pub value: String, } impl KeyValueRow { - pub(crate) fn new(label: impl Into, value: impl Into) -> Self { + pub fn new(label: impl Into, value: impl Into) -> Self { Self { label: label.into(), value: value.into(), @@ -44,8 +44,8 @@ impl Widget for KeyValueRow { } #[derive(Clone)] -pub(crate) struct TargetPicker { - pub(crate) configured_only: bool, +pub struct TargetPicker { + pub configured_only: bool, } impl Widget for TargetPicker { @@ -84,10 +84,10 @@ fn target_button(target: Target, ctx: &mut BuildCtx, view: &View, - pub(crate) selectable: bool, - pub(crate) max_rows: usize, +pub struct DeviceTable { + pub devices: Vec, + pub selectable: bool, + pub max_rows: usize, } impl Widget for DeviceTable { diff --git a/crates/tools/fission-cli/src/ui/components/dialog.rs b/crates/tools/fission-command-ui/src/components/dialog.rs similarity index 92% rename from crates/tools/fission-cli/src/ui/components/dialog.rs rename to crates/tools/fission-command-ui/src/components/dialog.rs index 4bfbd93a..b50674b2 100644 --- a/crates/tools/fission-cli/src/ui/components/dialog.rs +++ b/crates/tools/fission-command-ui/src/components/dialog.rs @@ -1,12 +1,12 @@ -use crate::ui::actions::{cancel_dialog, confirm_dialog, CancelDialog, ConfirmDialog}; -use crate::ui::components::{ActionButton, ButtonTone}; -use crate::ui::state::{UiDialog, UiState}; -use crate::ui::theme::UiPalette; +use crate::actions::{cancel_dialog, confirm_dialog, CancelDialog, ConfirmDialog}; +use crate::components::{ActionButton, ButtonTone}; +use crate::state::{UiDialog, UiState}; +use crate::theme::UiPalette; use fission::ir::op::{AlignItems, JustifyContent}; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct ConfirmationDialog; +pub struct ConfirmationDialog; impl Widget for ConfirmationDialog { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-command-ui/src/components/mod.rs b/crates/tools/fission-command-ui/src/components/mod.rs new file mode 100644 index 00000000..da686dbf --- /dev/null +++ b/crates/tools/fission-command-ui/src/components/mod.rs @@ -0,0 +1,11 @@ +mod chrome; +mod controls; +mod data; +mod dialog; +mod output; + +pub use chrome::AppShell; +pub use controls::{ActionButton, ButtonTone, FormTextField, TogglePill}; +pub use data::{DeviceTable, KeyValueRow, TargetPicker}; +pub use dialog::ConfirmationDialog; +pub use output::OutputPanel; diff --git a/crates/tools/fission-cli/src/ui/components/output.rs b/crates/tools/fission-command-ui/src/components/output.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/components/output.rs rename to crates/tools/fission-command-ui/src/components/output.rs index 17df2876..43988c5a 100644 --- a/crates/tools/fission-cli/src/ui/components/output.rs +++ b/crates/tools/fission-command-ui/src/components/output.rs @@ -1,15 +1,15 @@ -use crate::ui::actions::{select_command_session, SelectCommandSession}; -use crate::ui::commands::CommandStatus; -use crate::ui::commands::{CommandSessionId, CommandSnapshot}; -use crate::ui::density::UiDensity; -use crate::ui::state::{log_scroll_node_id, UiState}; -use crate::ui::theme::UiPalette; +use crate::actions::{select_command_session, SelectCommandSession}; +use crate::commands::CommandStatus; +use crate::commands::{CommandSessionId, CommandSnapshot}; +use crate::density::UiDensity; +use crate::state::{log_scroll_node_id, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct OutputPanel { - pub(crate) width: f32, - pub(crate) height: f32, +pub struct OutputPanel { + pub width: f32, + pub height: f32, } impl Widget for OutputPanel { diff --git a/crates/tools/fission-cli/src/ui/density.rs b/crates/tools/fission-command-ui/src/density.rs similarity index 69% rename from crates/tools/fission-cli/src/ui/density.rs rename to crates/tools/fission-command-ui/src/density.rs index 22b9cba6..9ac68b8d 100644 --- a/crates/tools/fission-cli/src/ui/density.rs +++ b/crates/tools/fission-command-ui/src/density.rs @@ -1,14 +1,14 @@ #[derive(Clone, Copy, Debug)] -pub(crate) struct UiDensity { +pub struct UiDensity { compact: bool, } impl UiDensity { - pub(crate) fn new(compact: bool) -> Self { + pub fn new(compact: bool) -> Self { Self { compact } } - pub(crate) fn header_height(self) -> f32 { + pub fn header_height(self) -> f32 { if self.compact { 2.0 } else { @@ -16,7 +16,7 @@ impl UiDensity { } } - pub(crate) fn shell_gap(self) -> f32 { + pub fn shell_gap(self) -> f32 { if self.compact { 0.0 } else { @@ -24,7 +24,7 @@ impl UiDensity { } } - pub(crate) fn body_gap(self) -> f32 { + pub fn body_gap(self) -> f32 { if self.compact { 1.0 } else { @@ -32,7 +32,7 @@ impl UiDensity { } } - pub(crate) fn outer_padding(self) -> [f32; 4] { + pub fn outer_padding(self) -> [f32; 4] { if self.compact { [1.0, 1.0, 0.0, 0.0] } else { @@ -40,7 +40,7 @@ impl UiDensity { } } - pub(crate) fn content_padding(self) -> [f32; 4] { + pub fn content_padding(self) -> [f32; 4] { if self.compact { [1.0, 1.0, 0.0, 0.0] } else { @@ -48,7 +48,7 @@ impl UiDensity { } } - pub(crate) fn sidebar_padding(self) -> [f32; 4] { + pub fn sidebar_padding(self) -> [f32; 4] { if self.compact { [1.0, 1.0, 0.0, 0.0] } else { @@ -56,7 +56,7 @@ impl UiDensity { } } - pub(crate) fn sidebar_width(self) -> f32 { + pub fn sidebar_width(self) -> f32 { if self.compact { 20.0 } else { @@ -64,7 +64,7 @@ impl UiDensity { } } - pub(crate) fn nav_route_height(self) -> f32 { + pub fn nav_route_height(self) -> f32 { if self.compact { 1.0 } else { @@ -72,7 +72,7 @@ impl UiDensity { } } - pub(crate) fn nav_gap(self) -> f32 { + pub fn nav_gap(self) -> f32 { if self.compact { 0.0 } else { @@ -80,7 +80,7 @@ impl UiDensity { } } - pub(crate) fn control_height(self) -> f32 { + pub fn control_height(self) -> f32 { if self.compact { 1.0 } else { @@ -88,7 +88,7 @@ impl UiDensity { } } - pub(crate) fn control_padding(self) -> [f32; 4] { + pub fn control_padding(self) -> [f32; 4] { if self.compact { [0.0, 0.0, 0.0, 0.0] } else { @@ -96,7 +96,7 @@ impl UiDensity { } } - pub(crate) fn text_input_height(self) -> f32 { + pub fn text_input_height(self) -> f32 { if self.compact { 3.0 } else { @@ -104,7 +104,7 @@ impl UiDensity { } } - pub(crate) fn text_input_padding(self) -> [f32; 4] { + pub fn text_input_padding(self) -> [f32; 4] { if self.compact { [0.0, 0.0, 0.0, 0.0] } else { @@ -112,12 +112,12 @@ impl UiDensity { } } - pub(crate) fn output_log_height(self, panel_height: f32) -> f32 { + pub fn output_log_height(self, panel_height: f32) -> f32 { let reserved = if self.compact { 2.0 } else { 3.0 }; (panel_height - reserved).max(1.0) } - pub(crate) fn shell_metrics(self, height: f32) -> ShellMetrics { + pub fn shell_metrics(self, height: f32) -> ShellMetrics { let header_h = self.header_height(); let padding = self.outer_padding(); let gap_h = self.shell_gap() * 2.0; @@ -132,7 +132,7 @@ impl UiDensity { } #[derive(Clone, Copy, Debug)] -pub(crate) struct ShellMetrics { - pub(crate) body_h: f32, - pub(crate) footer_h: f32, +pub struct ShellMetrics { + pub body_h: f32, + pub footer_h: f32, } diff --git a/crates/tools/fission-cli/src/ui/mod.rs b/crates/tools/fission-command-ui/src/lib.rs similarity index 86% rename from crates/tools/fission-cli/src/ui/mod.rs rename to crates/tools/fission-command-ui/src/lib.rs index 4d870594..5bd88167 100644 --- a/crates/tools/fission-cli/src/ui/mod.rs +++ b/crates/tools/fission-command-ui/src/lib.rs @@ -13,19 +13,19 @@ use fission::terminal::TerminalRunOptions; use std::path::PathBuf; use theme::UiThemeMode; -pub(crate) use app::CliUiApp; -pub(crate) use state::UiState; +pub use app::CliUiApp; +pub use state::UiState; #[derive(Clone, Debug)] -pub(crate) struct UiOptions { - pub(crate) project_dir: PathBuf, - pub(crate) screenshot: Option, - pub(crate) exit_after_render: bool, - pub(crate) width: Option, - pub(crate) height: Option, +pub struct UiOptions { + pub project_dir: PathBuf, + pub screenshot: Option, + pub exit_after_render: bool, + pub width: Option, + pub height: Option, } -pub(crate) fn run_ui(options: UiOptions) -> Result<()> { +pub fn run_ui(options: UiOptions) -> Result<()> { let state = UiState::load(options.project_dir.clone()); let run_options = TerminalRunOptions { width: options.width, @@ -35,7 +35,7 @@ pub(crate) fn run_ui(options: UiOptions) -> Result<()> { ..TerminalRunOptions::default() }; fission::terminal::TerminalApp::with_state(CliUiApp, state) - .with_title("Fission CLI") + .with_title("Fission command") .with_env(|env| { env.theme = fission::theme::Theme::dark(); }) @@ -57,9 +57,9 @@ pub(crate) fn run_ui(options: UiOptions) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::ui::routes::UiRoute; - use crate::ui::state::all_targets; - use crate::Target; + use crate::routes::UiRoute; + use crate::state::all_targets; + use fission_command_core::Target; use std::path::PathBuf; #[test] @@ -78,7 +78,7 @@ mod tests { theme_mode: UiThemeMode::Dark, ..Default::default() }; - state.devices = vec![crate::ui::state::UiDevice { + state.devices = vec![crate::state::UiDevice { id: "chrome".to_string(), name: "Chrome/Chromium".to_string(), target: Target::Web, @@ -113,7 +113,7 @@ mod tests { theme_mode: UiThemeMode::Dark, ..Default::default() }; - state.request_command_confirmation(crate::ui::commands::UiCommand::RunSelected); + state.request_command_confirmation(crate::commands::UiCommand::RunSelected); let mut app = fission::terminal::TerminalApp::with_state(CliUiApp, state).with_sync_env( |state, env| { diff --git a/crates/tools/fission-cli/src/ui/routes.rs b/crates/tools/fission-command-ui/src/routes.rs similarity index 87% rename from crates/tools/fission-cli/src/ui/routes.rs rename to crates/tools/fission-command-ui/src/routes.rs index 650af9fa..e39eda44 100644 --- a/crates/tools/fission-cli/src/ui/routes.rs +++ b/crates/tools/fission-command-ui/src/routes.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) enum UiRoute { +pub enum UiRoute { #[default] Dashboard, Project, @@ -18,7 +18,7 @@ pub(crate) enum UiRoute { impl UiRoute { #[cfg(test)] - pub(crate) const ALL: [Self; 11] = [ + pub const ALL: [Self; 11] = [ Self::Dashboard, Self::Project, Self::Doctor, @@ -32,7 +32,7 @@ impl UiRoute { Self::Help, ]; - pub(crate) const SIDEBAR: [Self; 7] = [ + pub const SIDEBAR: [Self; 7] = [ Self::Dashboard, Self::Project, Self::Run, @@ -42,7 +42,7 @@ impl UiRoute { Self::Help, ]; - pub(crate) fn title(self) -> &'static str { + pub fn title(self) -> &'static str { match self { Self::Dashboard => "Dashboard", Self::Project => "Project", diff --git a/crates/tools/fission-cli/src/ui/screens/dashboard.rs b/crates/tools/fission-command-ui/src/screens/dashboard.rs similarity index 90% rename from crates/tools/fission-cli/src/ui/screens/dashboard.rs rename to crates/tools/fission-command-ui/src/screens/dashboard.rs index b7dbea6f..54f60563 100644 --- a/crates/tools/fission-cli/src/ui/screens/dashboard.rs +++ b/crates/tools/fission-command-ui/src/screens/dashboard.rs @@ -1,14 +1,14 @@ use super::title_block; -use crate::ui::actions::{navigate, request_command, Navigate, RequestCommand}; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow}; -use crate::ui::routes::UiRoute; -use crate::ui::state::{target_label, UiState}; -use crate::ui::theme::UiPalette; +use crate::actions::{navigate, request_command, Navigate, RequestCommand}; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow}; +use crate::routes::UiRoute; +use crate::state::{target_label, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct DashboardScreen; +pub struct DashboardScreen; impl Widget for DashboardScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/devices.rs b/crates/tools/fission-command-ui/src/screens/devices.rs similarity index 85% rename from crates/tools/fission-cli/src/ui/screens/devices.rs rename to crates/tools/fission-command-ui/src/screens/devices.rs index b3e3105d..1c0b9aab 100644 --- a/crates/tools/fission-cli/src/ui/screens/devices.rs +++ b/crates/tools/fission-command-ui/src/screens/devices.rs @@ -1,13 +1,13 @@ use super::title_block; -use crate::ui::actions::{request_command, RequestCommand}; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow}; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::actions::{request_command, RequestCommand}; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow}; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct DevicesScreen; +pub struct DevicesScreen; impl Widget for DevicesScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/doctor.rs b/crates/tools/fission-command-ui/src/screens/doctor.rs similarity index 86% rename from crates/tools/fission-cli/src/ui/screens/doctor.rs rename to crates/tools/fission-command-ui/src/screens/doctor.rs index da5d43ab..5ee69b39 100644 --- a/crates/tools/fission-cli/src/ui/screens/doctor.rs +++ b/crates/tools/fission-command-ui/src/screens/doctor.rs @@ -1,13 +1,13 @@ use super::title_block; -use crate::ui::actions::{request_command, toggle_strict, RequestCommand, ToggleStrict}; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, KeyValueRow, TargetPicker, TogglePill}; -use crate::ui::state::{target_label, UiState}; -use crate::ui::theme::UiPalette; +use crate::actions::{request_command, toggle_strict, RequestCommand, ToggleStrict}; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, KeyValueRow, TargetPicker, TogglePill}; +use crate::state::{target_label, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct DoctorScreen; +pub struct DoctorScreen; impl Widget for DoctorScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/help.rs b/crates/tools/fission-command-ui/src/screens/help.rs similarity index 92% rename from crates/tools/fission-cli/src/ui/screens/help.rs rename to crates/tools/fission-command-ui/src/screens/help.rs index c3cd1278..f8cb9349 100644 --- a/crates/tools/fission-cli/src/ui/screens/help.rs +++ b/crates/tools/fission-command-ui/src/screens/help.rs @@ -1,11 +1,11 @@ use super::title_block; -use crate::ui::components::KeyValueRow; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::components::KeyValueRow; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct HelpScreen; +pub struct HelpScreen; impl Widget for HelpScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/logs.rs b/crates/tools/fission-command-ui/src/screens/logs.rs similarity index 89% rename from crates/tools/fission-cli/src/ui/screens/logs.rs rename to crates/tools/fission-command-ui/src/screens/logs.rs index 358bbb63..94fd5266 100644 --- a/crates/tools/fission-cli/src/ui/screens/logs.rs +++ b/crates/tools/fission-command-ui/src/screens/logs.rs @@ -1,13 +1,13 @@ use super::title_block; -use crate::ui::actions::{request_command, RequestCommand}; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow, TargetPicker}; -use crate::ui::state::{UiDevice, UiState}; -use crate::ui::theme::UiPalette; +use crate::actions::{request_command, RequestCommand}; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, DeviceTable, KeyValueRow, TargetPicker}; +use crate::state::{UiDevice, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct LogsScreen; +pub struct LogsScreen; impl Widget for LogsScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/mod.rs b/crates/tools/fission-command-ui/src/screens/mod.rs similarity index 74% rename from crates/tools/fission-cli/src/ui/screens/mod.rs rename to crates/tools/fission-command-ui/src/screens/mod.rs index eeab3b25..c6634c84 100644 --- a/crates/tools/fission-cli/src/ui/screens/mod.rs +++ b/crates/tools/fission-command-ui/src/screens/mod.rs @@ -8,22 +8,22 @@ mod run_build_test; mod settings; mod site; -use crate::ui::routes::UiRoute; -use crate::ui::state::UiState; +use crate::routes::UiRoute; +use crate::state::UiState; use fission::prelude::*; -pub(crate) use dashboard::DashboardScreen; -pub(crate) use devices::DevicesScreen; -pub(crate) use doctor::DoctorScreen; -pub(crate) use help::HelpScreen; -pub(crate) use logs::LogsScreen; -pub(crate) use project::ProjectScreen; -pub(crate) use run_build_test::{BuildScreen, RunScreen, TestScreen}; -pub(crate) use settings::SettingsScreen; -pub(crate) use site::SiteScreen; +pub use dashboard::DashboardScreen; +pub use devices::DevicesScreen; +pub use doctor::DoctorScreen; +pub use help::HelpScreen; +pub use logs::LogsScreen; +pub use project::ProjectScreen; +pub use run_build_test::{BuildScreen, RunScreen, TestScreen}; +pub use settings::SettingsScreen; +pub use site::SiteScreen; #[derive(Clone)] -pub(crate) struct ActiveScreen; +pub struct ActiveScreen; impl Widget for ActiveScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { @@ -43,7 +43,7 @@ impl Widget for ActiveScreen { } } -pub(crate) fn title_block( +pub fn title_block( title: &str, description: &str, title_color: fission::ir::op::Color, diff --git a/crates/tools/fission-cli/src/ui/screens/project.rs b/crates/tools/fission-command-ui/src/screens/project.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/screens/project.rs rename to crates/tools/fission-command-ui/src/screens/project.rs index 56731410..7c901076 100644 --- a/crates/tools/fission-cli/src/ui/screens/project.rs +++ b/crates/tools/fission-command-ui/src/screens/project.rs @@ -1,16 +1,16 @@ use super::title_block; -use crate::ui::actions::{ +use crate::actions::{ request_command, set_init_app_id, set_init_local_path, set_init_name, RequestCommand, SetInitAppId, SetInitLocalPath, SetInitName, }; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow}; -use crate::ui::state::{all_targets, target_label, UiState}; -use crate::ui::theme::UiPalette; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow}; +use crate::state::{all_targets, target_label, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct ProjectScreen; +pub struct ProjectScreen; impl Widget for ProjectScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/run_build_test.rs b/crates/tools/fission-command-ui/src/screens/run_build_test.rs similarity index 96% rename from crates/tools/fission-cli/src/ui/screens/run_build_test.rs rename to crates/tools/fission-command-ui/src/screens/run_build_test.rs index b6c5aa57..df11767c 100644 --- a/crates/tools/fission-cli/src/ui/screens/run_build_test.rs +++ b/crates/tools/fission-command-ui/src/screens/run_build_test.rs @@ -1,25 +1,25 @@ use super::title_block; -use crate::ui::actions::{ +use crate::actions::{ request_command, set_host, set_port, toggle_detach, toggle_headless, toggle_no_open, toggle_release, RequestCommand, SetHost, SetPort, ToggleDetach, ToggleHeadless, ToggleNoOpen, ToggleRelease, }; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ +use crate::commands::UiCommand; +use crate::components::{ ActionButton, ButtonTone, DeviceTable, FormTextField, KeyValueRow, TargetPicker, TogglePill, }; -use crate::ui::state::{UiDevice, UiState}; -use crate::ui::theme::UiPalette; +use crate::state::{UiDevice, UiState}; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct RunScreen; +pub struct RunScreen; #[derive(Clone)] -pub(crate) struct BuildScreen; +pub struct BuildScreen; #[derive(Clone)] -pub(crate) struct TestScreen; +pub struct TestScreen; impl Widget for RunScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/settings.rs b/crates/tools/fission-command-ui/src/screens/settings.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/screens/settings.rs rename to crates/tools/fission-command-ui/src/screens/settings.rs index 78fd7a8e..90df1fe0 100644 --- a/crates/tools/fission-cli/src/ui/screens/settings.rs +++ b/crates/tools/fission-command-ui/src/screens/settings.rs @@ -1,15 +1,15 @@ use super::title_block; -use crate::ui::actions::{ +use crate::actions::{ set_scrollback_limit, set_scrollback_limit_input, toggle_compact_mode, SetScrollbackLimit, SetScrollbackLimitInput, ToggleCompactMode, }; -use crate::ui::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow}; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow}; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct SettingsScreen; +pub struct SettingsScreen; impl Widget for SettingsScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/screens/site.rs b/crates/tools/fission-command-ui/src/screens/site.rs similarity index 94% rename from crates/tools/fission-cli/src/ui/screens/site.rs rename to crates/tools/fission-command-ui/src/screens/site.rs index a3eb2eb4..ab3c66c5 100644 --- a/crates/tools/fission-cli/src/ui/screens/site.rs +++ b/crates/tools/fission-command-ui/src/screens/site.rs @@ -1,16 +1,16 @@ use super::title_block; -use crate::ui::actions::{ +use crate::actions::{ request_command, set_host, set_port, toggle_no_open, toggle_release, RequestCommand, SetHost, SetPort, ToggleNoOpen, ToggleRelease, }; -use crate::ui::commands::UiCommand; -use crate::ui::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow, TogglePill}; -use crate::ui::state::UiState; -use crate::ui::theme::UiPalette; +use crate::commands::UiCommand; +use crate::components::{ActionButton, ButtonTone, FormTextField, KeyValueRow, TogglePill}; +use crate::state::UiState; +use crate::theme::UiPalette; use fission::prelude::*; #[derive(Clone)] -pub(crate) struct SiteScreen; +pub struct SiteScreen; impl Widget for SiteScreen { fn build(&self, ctx: &mut BuildCtx, view: &View) -> Node { diff --git a/crates/tools/fission-cli/src/ui/state.rs b/crates/tools/fission-command-ui/src/state.rs similarity index 80% rename from crates/tools/fission-cli/src/ui/state.rs rename to crates/tools/fission-command-ui/src/state.rs index 4a2344fb..a8177d92 100644 --- a/crates/tools/fission-cli/src/ui/state.rs +++ b/crates/tools/fission-command-ui/src/state.rs @@ -4,46 +4,47 @@ use super::commands::{ use super::density::UiDensity; use super::routes::UiRoute; use super::theme::UiThemeMode; -use crate::{read_project_config, workflow, Target}; use fission::core::{Env, RuntimeState}; use fission::ir::NodeId; use fission::prelude::AppState; +use fission_command_core::{read_project_config, Target}; +use fission_command_run as workflow; use std::path::PathBuf; const LOG_SCROLL_NODE_ID_PREFIX: &str = "cli_ui_log_scrollback"; #[derive(Clone, Debug, PartialEq)] -pub(crate) struct UiState { - pub(crate) project_dir: PathBuf, - pub(crate) project_name: String, - pub(crate) app_id: String, - pub(crate) project_status: String, - pub(crate) targets: Vec, - pub(crate) devices: Vec, - pub(crate) route: UiRoute, - pub(crate) theme_mode: UiThemeMode, - pub(crate) compact_mode: bool, - pub(crate) selected_target: Option, - pub(crate) selected_device: Option, - pub(crate) init_name: String, - pub(crate) init_app_id: String, - pub(crate) init_local_path: String, - pub(crate) host: String, - pub(crate) port: String, - pub(crate) strict: bool, - pub(crate) release: bool, - pub(crate) detach: bool, - pub(crate) no_open: bool, - pub(crate) headless: bool, - pub(crate) command_runtime: CommandRuntime, - pub(crate) command_sessions: Vec, - pub(crate) active_command_session_id: Option, - pub(crate) last_active_log_line_count: usize, - pub(crate) refreshed_finished_sessions: Vec, - pub(crate) scrollback_limit: usize, - pub(crate) scrollback_limit_input: String, - pub(crate) pending_dialog: Option, - pub(crate) exit_confirmed: bool, +pub struct UiState { + pub project_dir: PathBuf, + pub project_name: String, + pub app_id: String, + pub project_status: String, + pub targets: Vec, + pub devices: Vec, + pub route: UiRoute, + pub theme_mode: UiThemeMode, + pub compact_mode: bool, + pub selected_target: Option, + pub selected_device: Option, + pub init_name: String, + pub init_app_id: String, + pub init_local_path: String, + pub host: String, + pub port: String, + pub strict: bool, + pub release: bool, + pub detach: bool, + pub no_open: bool, + pub headless: bool, + pub command_runtime: CommandRuntime, + pub command_sessions: Vec, + pub active_command_session_id: Option, + pub last_active_log_line_count: usize, + pub refreshed_finished_sessions: Vec, + pub scrollback_limit: usize, + pub scrollback_limit_input: String, + pub pending_dialog: Option, + pub exit_confirmed: bool, } impl AppState for UiState {} @@ -86,7 +87,7 @@ impl Default for UiState { } impl UiState { - pub(crate) fn load(project_dir: PathBuf) -> Self { + pub fn load(project_dir: PathBuf) -> Self { let mut state = Self { project_dir, route: UiRoute::Dashboard, @@ -102,7 +103,7 @@ impl UiState { state } - pub(crate) fn refresh(&mut self) { + pub fn refresh(&mut self) { match read_project_config(&self.project_dir) { Ok(project) => { self.project_name = project.app.name; @@ -154,21 +155,21 @@ impl UiState { } } - pub(crate) fn selected_target_label(&self) -> String { + pub fn selected_target_label(&self) -> String { self.selected_target .map(Target::as_str) .unwrap_or("none") .to_string() } - pub(crate) fn selected_device_label(&self) -> String { + pub fn selected_device_label(&self) -> String { self.selected_device .as_deref() .unwrap_or("auto") .to_string() } - pub(crate) fn target_devices(&self) -> Vec<&UiDevice> { + pub fn target_devices(&self) -> Vec<&UiDevice> { self.devices .iter() .filter(|device| { @@ -179,7 +180,7 @@ impl UiState { .collect() } - pub(crate) fn poll_command_status(&mut self, runtime: &mut RuntimeState, env: &Env) -> bool { + pub fn poll_command_status(&mut self, runtime: &mut RuntimeState, env: &Env) -> bool { let snapshot = self.command_runtime.snapshot(); let mut changed = false; @@ -224,7 +225,7 @@ impl UiState { changed } - pub(crate) fn sync_command_sessions(&mut self) { + pub fn sync_command_sessions(&mut self) { let snapshot = self.command_runtime.snapshot(); self.active_command_session_id = snapshot.active_session_id; self.last_active_log_line_count = snapshot @@ -235,18 +236,18 @@ impl UiState { self.command_sessions = snapshot.sessions; } - pub(crate) fn active_command_session(&self) -> Option<&CommandSnapshot> { + pub fn active_command_session(&self) -> Option<&CommandSnapshot> { self.active_command_session_id .and_then(|id| self.command_sessions.iter().find(|item| item.id == id)) .or_else(|| self.command_sessions.last()) } - pub(crate) fn select_command_session(&mut self, session_id: CommandSessionId) { + pub fn select_command_session(&mut self, session_id: CommandSessionId) { self.command_runtime.set_active(session_id); self.sync_command_sessions(); } - pub(crate) fn request_command_confirmation(&mut self, command: UiCommand) { + pub fn request_command_confirmation(&mut self, command: UiCommand) { let label = command.label(); let message = command.confirmation_message(); self.pending_dialog = Some(UiDialog::Command { @@ -256,17 +257,17 @@ impl UiState { }); } - pub(crate) fn request_exit_confirmation(&mut self) { + pub fn request_exit_confirmation(&mut self) { if self.exit_confirmed { return; } self.pending_dialog = Some(UiDialog::Exit { - title: "Exit Fission CLI?".to_string(), + title: "Exit Fission command?".to_string(), message: "Running commands are not stopped automatically. You can cancel and inspect their output before leaving.".to_string(), }); } - pub(crate) fn set_scrollback_limit(&mut self, limit: usize) { + pub fn set_scrollback_limit(&mut self, limit: usize) { let limit = limit.max(1); self.scrollback_limit = limit; self.scrollback_limit_input = limit.to_string(); @@ -276,7 +277,7 @@ impl UiState { } #[derive(Clone, Debug, PartialEq)] -pub(crate) enum UiDialog { +pub enum UiDialog { Command { command: UiCommand, title: String, @@ -288,11 +289,11 @@ pub(crate) enum UiDialog { }, } -pub(crate) fn log_scroll_node_id(session_id: CommandSessionId) -> NodeId { +pub fn log_scroll_node_id(session_id: CommandSessionId) -> NodeId { NodeId::explicit(&format!("{LOG_SCROLL_NODE_ID_PREFIX}_{session_id}")) } -pub(crate) fn log_visible_rows_for_height(height: f32, compact: bool) -> usize { +pub fn log_visible_rows_for_height(height: f32, compact: bool) -> usize { let density = UiDensity::new(compact); let metrics = density.shell_metrics(height); density.output_log_height(metrics.footer_h).floor().max(1.0) as usize @@ -340,14 +341,14 @@ fn should_follow_log_output( } #[derive(Clone, Debug, PartialEq)] -pub(crate) struct UiDevice { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) target: Target, - pub(crate) kind: String, - pub(crate) status: String, - pub(crate) detail: String, - pub(crate) available: bool, +pub struct UiDevice { + pub id: String, + pub name: String, + pub target: Target, + pub kind: String, + pub status: String, + pub detail: String, + pub available: bool, } impl From for UiDevice { @@ -364,7 +365,7 @@ impl From for UiDevice { } } -pub(crate) fn target_label(target: Target) -> &'static str { +pub fn target_label(target: Target) -> &'static str { match target { Target::Android => "Android", Target::Ios => "iOS", @@ -376,7 +377,7 @@ pub(crate) fn target_label(target: Target) -> &'static str { } } -pub(crate) fn all_targets() -> [Target; 7] { +pub fn all_targets() -> [Target; 7] { [ Target::Android, Target::Ios, diff --git a/crates/tools/fission-cli/src/ui/theme.rs b/crates/tools/fission-command-ui/src/theme.rs similarity index 73% rename from crates/tools/fission-cli/src/ui/theme.rs rename to crates/tools/fission-command-ui/src/theme.rs index 67799802..115914ae 100644 --- a/crates/tools/fission-cli/src/ui/theme.rs +++ b/crates/tools/fission-command-ui/src/theme.rs @@ -2,21 +2,21 @@ use fission::ir::op::Color; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) enum UiThemeMode { +pub enum UiThemeMode { Light, #[default] Dark, } impl UiThemeMode { - pub(crate) fn toggle(self) -> Self { + pub fn toggle(self) -> Self { match self { Self::Light => Self::Dark, Self::Dark => Self::Light, } } - pub(crate) fn label(self) -> &'static str { + pub fn label(self) -> &'static str { match self { Self::Light => "Light", Self::Dark => "Dark", @@ -25,23 +25,23 @@ impl UiThemeMode { } #[derive(Clone, Copy, Debug)] -pub(crate) struct UiPalette { - pub(crate) background: Color, - pub(crate) surface: Color, - pub(crate) raised: Color, - pub(crate) subtle: Color, - pub(crate) border: Color, - pub(crate) text: Color, - pub(crate) muted: Color, - pub(crate) accent: Color, - pub(crate) accent_text: Color, - pub(crate) success: Color, - pub(crate) warning: Color, - pub(crate) error: Color, +pub struct UiPalette { + pub background: Color, + pub surface: Color, + pub raised: Color, + pub subtle: Color, + pub border: Color, + pub text: Color, + pub muted: Color, + pub accent: Color, + pub accent_text: Color, + pub success: Color, + pub warning: Color, + pub error: Color, } impl UiPalette { - pub(crate) fn for_mode(mode: UiThemeMode) -> Self { + pub fn for_mode(mode: UiThemeMode) -> Self { match mode { UiThemeMode::Dark => Self { background: rgb(11, 18, 32), @@ -75,6 +75,6 @@ impl UiPalette { } } -pub(crate) const fn rgb(r: u8, g: u8, b: u8) -> Color { +pub const fn rgb(r: u8, g: u8, b: u8) -> Color { Color { r, g, b, a: 255 } } diff --git a/crates/tools/fission-credentials/Cargo.toml b/crates/tools/fission-credentials/Cargo.toml new file mode 100644 index 00000000..273e3012 --- /dev/null +++ b/crates/tools/fission-credentials/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fission-credentials" +version = "0.1.1" +edition = "2021" +license = "MIT" +repository = "https://github.com/worka-ai/fission" +description = "Credential vault helpers for the Fission command" + +[dependencies] +anyhow = "1.0" +base64 = "0.22" +chacha20poly1305 = "0.10" +fission-command-core = { path = "../fission-command-core", version = "0.1.1" } +getrandom = { version = "0.2", features = ["std"] } +keyring = { version = "3", default-features = false, features = ["apple-native", "windows-native", "linux-native", "crypto-rust"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/crates/tools/fission-credentials/src/lib.rs b/crates/tools/fission-credentials/src/lib.rs new file mode 100644 index 00000000..a3527ee9 --- /dev/null +++ b/crates/tools/fission-credentials/src/lib.rs @@ -0,0 +1,148 @@ +use anyhow::{bail, Context, Result}; +use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + XChaCha20Poly1305, XNonce, +}; +use fission_command_core::DistributionProvider; +use serde::{Deserialize, Serialize}; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Serialize, Deserialize)] +struct VaultRecord { + schema_version: u32, + provider: String, + created_at_unix_seconds: u64, + nonce: String, + ciphertext: String, +} + +pub fn provider_secret( + provider: DistributionProvider, + env_names: &[&str], +) -> Result> { + if let Some(name) = env_names.iter().find(|name| env::var_os(name).is_some()) { + return env::var(name) + .map(Some) + .with_context(|| format!("environment variable {name} is not valid UTF-8")); + } + let path = vault_record_path(provider)?; + if !path.exists() { + return Ok(None); + } + let bytes = load_provider_secret(provider)?; + String::from_utf8(bytes) + .map(Some) + .context("stored provider credential is not valid UTF-8") +} + +pub fn read_secret_source(source: &str) -> Result { + if let Some(name) = source.strip_prefix("env:") { + env::var(name).with_context(|| format!("environment variable {name} is not set")) + } else if let Some(path) = source.strip_prefix("file:") { + fs::read_to_string(path).with_context(|| format!("failed to read credential file {path}")) + } else { + bail!("credential source must be env: or file:") + } +} + +pub fn store_provider_secret(provider: DistributionProvider, secret: &[u8]) -> Result<()> { + let key = vault_key(true)?; + let mut nonce = [0u8; 24]; + getrandom::getrandom(&mut nonce)?; + let cipher = XChaCha20Poly1305::new_from_slice(&key) + .map_err(|error| anyhow::anyhow!("failed to initialize vault cipher: {error}"))?; + let ciphertext = cipher + .encrypt(XNonce::from_slice(&nonce), secret) + .map_err(|error| anyhow::anyhow!("failed to encrypt credential record: {error}"))?; + let record = VaultRecord { + schema_version: 1, + provider: provider.as_str().to_string(), + created_at_unix_seconds: now_unix_seconds(), + nonce: STANDARD_NO_PAD.encode(nonce), + ciphertext: STANDARD_NO_PAD.encode(ciphertext), + }; + let path = vault_record_path(provider)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&path, serde_json::to_vec_pretty(&record)?) + .with_context(|| format!("failed to write {}", path.display()))?; + Ok(()) +} + +pub fn load_provider_secret(provider: DistributionProvider) -> Result> { + let path = vault_record_path(provider)?; + let record: VaultRecord = serde_json::from_slice( + &fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?, + )?; + let nonce = STANDARD_NO_PAD + .decode(record.nonce) + .context("failed to decode vault nonce")?; + let ciphertext = STANDARD_NO_PAD + .decode(record.ciphertext) + .context("failed to decode vault ciphertext")?; + let key = vault_key(false)?; + let cipher = XChaCha20Poly1305::new_from_slice(&key) + .map_err(|error| anyhow::anyhow!("failed to initialize vault cipher: {error}"))?; + cipher + .decrypt(XNonce::from_slice(&nonce), ciphertext.as_ref()) + .map_err(|error| anyhow::anyhow!("failed to decrypt credential record: {error}")) +} + +pub fn rotate_provider_secret(provider: DistributionProvider) -> Result<()> { + let secret = load_provider_secret(provider)?; + store_provider_secret(provider, &secret) +} + +pub fn vault_record_path(provider: DistributionProvider) -> Result { + Ok(vault_dir()?.join(format!("{}.json", provider.as_str()))) +} + +fn vault_key(create: bool) -> Result<[u8; 32]> { + let entry = keyring::Entry::new("fission", "release-vault") + .context("failed to open OS credential store for the Fission release vault")?; + match entry.get_password() { + Ok(encoded) => decode_vault_key(&encoded), + Err(error) if create => { + let mut key = [0u8; 32]; + getrandom::getrandom(&mut key)?; + entry + .set_password(&STANDARD_NO_PAD.encode(key)) + .with_context(|| { + format!("failed to store Fission vault key in OS credential store: {error}") + })?; + Ok(key) + } + Err(error) => { + Err(error).context("Fission vault key does not exist in the OS credential store") + } + } +} + +fn decode_vault_key(encoded: &str) -> Result<[u8; 32]> { + let bytes = STANDARD_NO_PAD + .decode(encoded) + .context("failed to decode Fission vault key")?; + let key: [u8; 32] = bytes + .try_into() + .map_err(|_| anyhow::anyhow!("Fission vault key has the wrong length"))?; + Ok(key) +} + +fn vault_dir() -> Result { + let home = env::var_os("HOME") + .or_else(|| env::var_os("USERPROFILE")) + .context("HOME/USERPROFILE is not set")?; + Ok(PathBuf::from(home).join(".fission/vault")) +} + +fn now_unix_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} diff --git a/docs/cli-and-targets.md b/docs/cli-and-targets.md index ff76ce78..f6495983 100644 --- a/docs/cli-and-targets.md +++ b/docs/cli-and-targets.md @@ -1,4 +1,4 @@ -# Fission CLI and target status +# Fission command and target status ## Commands @@ -25,59 +25,59 @@ fission init my-app --local-path /path/to/fission Add more platform targets: ```sh -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` Diagnose local SDKs, emulators, browsers, and Rust targets: ```sh -cargo fission doctor web ios android --project-dir my-app +fission doctor web ios android --project-dir my-app ``` List the devices and runtime targets the CLI can launch: ```sh -cargo fission devices --project-dir my-app -cargo fission devices --project-dir my-app --json +fission devices --project-dir my-app +fission devices --project-dir my-app --json ``` Run an app on the selected target. The command attaches by default, so desktop stdout/stderr, web server requests, iOS simulator logs, or Android `logcat` output stay in the terminal until you stop them: ```sh -cargo fission run --project-dir my-app -cargo fission run --project-dir my-app --target web -cargo fission run --project-dir my-app --target ios --device -cargo fission run --project-dir my-app --target android --device emulator-5554 +fission run --project-dir my-app +fission run --project-dir my-app --target web +fission run --project-dir my-app --target ios --device +fission run --project-dir my-app --target android --device emulator-5554 ``` Start without attaching when you want the app to keep running in the background: ```sh -cargo fission run --project-dir my-app --target web --detach -cargo fission logs --project-dir my-app --target web --follow +fission run --project-dir my-app --target web --detach +fission logs --project-dir my-app --target web --follow ``` Build or smoke-test a configured target: ```sh -cargo fission build --project-dir my-app --target web --release -cargo fission test --project-dir my-app --target web -cargo fission test --project-dir my-app --target ios --headless -cargo fission test --project-dir my-app --target android --headless +fission build --project-dir my-app --target web --release +fission test --project-dir my-app --target web +fission test --project-dir my-app --target ios --headless +fission test --project-dir my-app --target android --headless ``` Package, check, and publish release artifacts: ```sh -cargo fission package --project-dir my-app --target site --format static --release -cargo fission package --project-dir my-app --target linux --format run --release -cargo fission package --project-dir my-app --target macos --format app --release -cargo fission package --project-dir my-app --target android --format apk --release -cargo fission readiness release --project-dir my-app --target site --format static --provider github-pages --site production -cargo fission readiness distribute --project-dir my-app --provider github-pages --site production --artifact my-app/target/fission/release/site/static/artifact-manifest.json -cargo fission distribute setup --project-dir my-app --provider github-pages --site production -cargo fission distribute --project-dir my-app --provider github-pages --site production --artifact my-app/target/fission/release/site/static/artifact-manifest.json -cargo fission distribute --project-dir my-app --provider play-store --track internal --artifact my-app/target/fission/release/android/aab/artifact-manifest.json +fission package --project-dir my-app --target site --format static --release +fission package --project-dir my-app --target linux --format run --release +fission package --project-dir my-app --target macos --format app --release +fission package --project-dir my-app --target android --format apk --release +fission readiness release --project-dir my-app --target site --format static --provider github-pages --site production +fission readiness distribute --project-dir my-app --provider github-pages --site production --artifact my-app/target/fission/release/site/static/artifact-manifest.json +fission distribute setup --project-dir my-app --provider github-pages --site production +fission distribute --project-dir my-app --provider github-pages --site production --artifact my-app/target/fission/release/site/static/artifact-manifest.json +fission distribute --project-dir my-app --provider play-store --track internal --artifact my-app/target/fission/release/android/aab/artifact-manifest.json ``` Every package command stages output under `target/fission///` and writes `artifact-manifest.json` with file hashes and MIME types. Static site/web publishing supports GitHub Pages, Cloudflare Pages, Netlify, direct S3-compatible object storage uploads through the Rust AWS SDK, and direct OAuth-backed uploads to Google Drive, OneDrive, and Dropbox. Store providers are represented in the lifecycle command surface so release metadata, beta groups, signing checks, review operations, and authentication can be validated from the same project root before provider-specific store APIs mutate remote state. @@ -85,13 +85,13 @@ Every package command stages output under `target/fission/// --device --project-dir .` while developing. Omit `--device` for the interactive selector when more than one runnable device exists. -4. `cargo fission run --target --detach --project-dir .` when you want the launched app/server to keep running without owning the terminal. -5. `cargo fission logs --target --device --project-dir . --follow` to attach logs later. -6. `cargo fission build --target --project-dir . --release` before producing artifacts for a tester. -7. `cargo fission test --target --project-dir .` to run the generated platform smoke test. +1. `fission doctor --project-dir .` before starting platform work, especially on a new machine or CI runner. +2. `fission devices --project-dir .` to see the local desktop target, Chrome/Chromium, Android devices/emulators, and iOS simulators. +3. `fission run --target --device --project-dir .` while developing. Omit `--device` for the interactive selector when more than one runnable device exists. +4. `fission run --target --detach --project-dir .` when you want the launched app/server to keep running without owning the terminal. +5. `fission logs --target --device --project-dir . --follow` to attach logs later. +6. `fission build --target --project-dir . --release` before producing artifacts for a tester. +7. `fission test --target --project-dir .` to run the generated platform smoke test. Device ids are stable enough for scripts: Android uses the `adb` serial, iOS uses the simulator UDID, web uses `chrome`, and desktop uses `desktop`. @@ -177,7 +177,7 @@ rustup target add aarch64-apple-ios aarch64-apple-ios-sim aarch64-linux-android Run doctor before platform work: ```sh -cargo fission doctor web ios android --project-dir . +fission doctor web ios android --project-dir . ``` ### iOS @@ -195,11 +195,11 @@ Commands: ./examples/mobile-smoke/platforms/ios/test-sim.sh ``` -Generated app command after `cargo fission add-target ios`: +Generated app command after `fission add-target ios`: ```sh -cargo fission run --target ios --project-dir . -cargo fission test --target ios --project-dir . +fission run --target ios --project-dir . +fission test --target ios --project-dir . ``` The generated iOS script opens the Simulator app by default. Set `IOS_SIM_HEADLESS=1` for CI or background-only runs. @@ -234,11 +234,11 @@ Commands: ./examples/mobile-smoke/platforms/android/test-emulator.sh ``` -Generated app command after `cargo fission add-target android`: +Generated app command after `fission add-target android`: ```sh -cargo fission run --target android --project-dir . -cargo fission test --target android --project-dir . +fission run --target android --project-dir . +fission test --target android --project-dir . ``` ### Web / WASM @@ -249,7 +249,7 @@ Required tools: rustup target add wasm32-unknown-unknown cargo install wasm-pack node --version # Node 22+ is required by the CDP smoke test -cargo fission doctor web --project-dir . +fission doctor web --project-dir . ``` The browser test script uses Node.js plus Chrome/Chromium's DevTools Protocol endpoint. It starts a transient server, fails on browser runtime or console errors, and waits for a non-empty canvas. Set `FISSION_CHROME=/path/to/chrome` if the browser cannot be auto-detected. @@ -261,16 +261,17 @@ Commands: ./examples/web-smoke/platforms/web/test-browser.sh ``` -Generated app command after `cargo fission add-target web`: +Generated app command after `fission add-target web`: ```sh -cargo fission run --target web --project-dir . -cargo fission test --target web --project-dir . +fission run --target web --project-dir . +fission test --target web --project-dir . ``` Relevant paths: -- CLI: `crates/tools/fission-cli/` +- command package: `crates/tools/cargo-fission/` +- command implementation crates: `crates/tools/fission-command-*` - mobile shell: `crates/shell/fission-shell-mobile/` - web shell: `crates/shell/fission-shell-web/` - mobile smoke example: `examples/mobile-smoke/` diff --git a/docs/platform-smoke-tests.md b/docs/platform-smoke-tests.md index d7c5df92..45922964 100644 --- a/docs/platform-smoke-tests.md +++ b/docs/platform-smoke-tests.md @@ -22,13 +22,13 @@ git submodule update --init --recursive Use the CLI doctor before platform work: ```sh -cargo run -p fission-cli --bin fission -- doctor web ios android --project-dir examples/mobile-smoke +fission doctor web ios android --project-dir examples/mobile-smoke ``` For a generated app, run: ```sh -cargo fission doctor web ios android --project-dir . +fission doctor web ios android --project-dir . ``` Doctor checks Rust targets, wasm-pack, Node.js CDP support, Chrome/Chromium, Xcode/simctl, Android SDK tools, installed Android platforms, build-tools, NDK, and the NDK clang linker for the selected Android minimum API. @@ -164,8 +164,8 @@ A newly scaffolded app uses the same scripts: ```sh fission init /tmp/demo-app --local-path "$PWD" -cargo fission add-target ios android web --project-dir /tmp/demo-app -cargo fission doctor web ios android --project-dir /tmp/demo-app +fission add-target ios android web --project-dir /tmp/demo-app +fission doctor web ios android --project-dir /tmp/demo-app cd /tmp/demo-app ./platforms/ios/run-sim.sh ./platforms/ios/test-sim.sh diff --git a/docs/post-build-lifecycle.md b/docs/post-build-lifecycle.md index 7c2d1f07..a062d91c 100644 --- a/docs/post-build-lifecycle.md +++ b/docs/post-build-lifecycle.md @@ -1,12 +1,12 @@ # Post-build lifecycle: packaging, signing, distribution, and release automation Status: proposal/specification -Audience: Fission CLI, platform shell, tooling, and release engineering implementers +Audience: Fission command, platform shell, tooling, and release engineering implementers Scope: everything after `fission build` produces a release binary or web bundle ## 1. Purpose -Fission should manage the whole lifecycle of an application, not just compile and run it during development. A production team should be able to use the Fission CLI to answer four questions for every supported platform: +Fission should manage the whole lifecycle of an application, not just compile and run it during development. A production team should be able to use the Fission command to answer four questions for every supported platform: 1. Can this project be packaged for this platform on this machine? 2. Can this project be signed or notarized where the platform requires it? @@ -127,6 +127,37 @@ The TUI MUST only write `fission.toml`, asset files, or generated receipts that fission readiness release --target android --format aab --provider play-store --json ``` +### 4.1 Single public command, modular implementation + +Fission MUST present one command to developers: `fission`. A developer installs the command once and then uses that same executable for initialization, target management, running, testing, site generation, packaging, signing, release metadata, credentials, distribution, and lifecycle checks. The implementation MAY keep a Cargo-compatible helper binary for compatibility with Cargo subcommand discovery, but public documentation, examples, generated project files, and remediation messages MUST use `fission` and MUST NOT describe the product using implementation crate names. + +The Cargo package that distributes the command MAY be named `cargo-fission`, but that package name is an installation detail. After installation the product surface is the `fission` command. + +The single public command does not imply a monolithic Rust crate. The implementation SHOULD be split into internal crates with clear dependency boundaries: + +| Internal crate | Responsibility | Dependency rule | +|---|---|---| +| `cargo-fission` | Binary entrypoint, argument parsing, dispatch, compatibility shim, and command composition | Thin crate; should not own provider implementations or platform-specific packaging logic directly | +| `fission-command-core` | Shared command models, manifest helpers, report schemas, target identifiers, diagnostics, and reusable process utilities | No store SDKs, no desktop shell, no mobile SDK bindings, no credential backend | +| `fission-command-site` | Static-site route listing, check, build, serve, static artifact helpers, and site-only validation | No credential vault, no store provider SDK, no app-store/cloud upload clients | +| `fission-command-run` | Device discovery, doctor checks, build/run/test/log workflows for desktop, web, Android, iOS, terminal, and future targets | Platform-specific detection must stay isolated behind this crate's public API | +| `fission-command-package` | Artifact creation, package manifest generation, icon outputs, checksums, receipts, and target-format validation | May call platform vendor tools, but must not own provider upload logic | +| `fission-command-release` | Release metadata, release-content capture/render/validate, beta groups/testers, reviews, signing metadata, and workflow orchestration | Uses provider traits; should not hard-wire a provider implementation into generic release logic | +| `fission-command-ui` | Terminal UI app over the same command model | UI actions must delegate to the same command execution path as non-interactive commands | +| `fission-credentials` | Vault records, OS credential-store integration, credential import/rotation/audit, and CI/env credential lookup | Credential backends are isolated here so unrelated commands do not learn provider-secret details | +| Provider crates | GitHub Pages/Releases, S3-compatible storage, Cloudflare Pages, Netlify, Google Drive, OneDrive, Dropbox, Play Store, App Store, Microsoft Store | Each provider owns only its API/client/CLI orchestration and implements shared provider traits | + +This crate split is an implementation boundary, not a developer-facing product boundary. It is acceptable for `cargo install cargo-fission` to compile the crates needed by the default distribution, but command implementations must be organized so dependency leakage is visible and testable. For example, static-site route checks must not depend on DBus, desktop windowing, mobile SDKs, AWS, or store API clients. Release publishing may depend on provider clients and credential backends, but those dependencies must not be introduced through site, doctor, or plain run/build code paths. + +CI MUST include dependency-boundary checks for the important command families. At minimum: + +- the static-site command path must not contain DBus, desktop shell, mobile SDK, AWS, store-provider, or credential-vault dependencies; +- the run/build/test path must not contain cloud or store-provider clients unless a platform target explicitly requires them; +- provider crates must not be pulled into unrelated command-family tests; +- the public command help and generated project text must consistently show `fission ...`. + +These checks do not replace normal Rust features. The preferred implementation is still normal Rust crates, explicit features, and target-specific dependencies. The key constraint is developer experience: no matter how many internal crates exist, developers use one `fission` command. + The JSON schema MUST include: ```json @@ -583,7 +614,7 @@ Readiness SHOULD warn when: The CLI implementation MUST keep all icon handling in a dedicated Rust module: ```text -crates/tools/fission-cli/src/icons/ +crates/tools/cargo-fission/src/icons/ mod.rs config.rs model.rs @@ -822,7 +853,7 @@ The release tooling should prefer Rust for Fission-owned control flow and data h | Area | Fission/Rust responsibility | Provider/platform tool | Notes | | --- | --- | --- | --- | -| App icon generation | Dedicated `crates/tools/fission-cli/src/icons` module using `usvg`, `resvg`, `tiny-skia`, `image`, `png`, `ico`, `icns`, `plist`, `serde_json`, `quick-xml`, `sha2`, and optional `oxipng` | platform tools only for final platform-owned bundle/package signing or validation | Fission owns deterministic icon generation from `[package.icons]` into target-specific outputs. Platform packagers consume the generated icon manifest and must not implement their own icon parsing or generation [R73][R74][R75][R76][R77][R78][R79][R80][R81][R82][R83][R84]. | +| App icon generation | Dedicated `crates/tools/fission-command-package/src/icons` module using `usvg`, `resvg`, `tiny-skia`, `image`, `png`, `ico`, `icns`, `plist`, `serde_json`, `quick-xml`, `sha2`, and optional `oxipng` | platform tools only for final platform-owned bundle/package signing or validation | Fission owns deterministic icon generation from `[package.icons]` into target-specific outputs. Platform packagers consume the generated icon manifest and must not implement their own icon parsing or generation [R73][R74][R75][R76][R77][R78][R79][R80][R81][R82][R83][R84]. | | Desktop bundle generation | `cargo-packager` integration where it fits, plus Fission manifest and asset staging | platform-specific packager | `cargo-packager` supports macOS `.app`, Linux AppImage/deb, Windows NSIS/WiX, but not every Fission-required format such as Linux `.run`, macOS `.pkg`, MSIX, AAB, or IPA [R25]. | | macOS `.app` basics | bundle metadata, assets, `Info.plist` inputs, and readiness checks | Xcode command-line tools for signing/notarization | `.app` bundle structure and `Info.plist` requirements are Apple platform rules [R8][R9]. | | macOS `.pkg` | configuration, staging, receipts, and readiness checks | `pkgbuild`, `productbuild`, `productsign` | Apple's package tools create installer component packages and product archives [R13]. | @@ -1975,7 +2006,7 @@ fission readiness package --target windows --format msix --builder windows-ci Remote builder contracts MUST include: -- same Fission CLI version or compatible protocol; +- same Fission command version or compatible protocol; - clean checkout or artifact input; - credential source policy; - artifact return path; @@ -2073,7 +2104,7 @@ These are implementation milestones, not partial product definitions. The final 2. Add credential vault and `fission auth` commands. 3. Add release-config root schema in `fission.toml`, `[[releases]]` file references, referenced release-file schemas, TUI editing, non-interactive edit/import/diff/validate/push commands, screenshot scenario model, and content manifest. 4. Add readiness engine with JSON output and stable error IDs. -5. Add `[package.icons]`, the dedicated `crates/tools/fission-cli/src/icons` module, icon readiness checks, deterministic platform icon generation, manual/provided icon validation, and icon manifests before platform packagers consume icons. +5. Add `[package.icons]`, the dedicated `crates/tools/fission-command-package/src/icons` module, icon readiness checks, deterministic platform icon generation, manual/provided icon validation, and icon manifests before platform packagers consume icons. 6. Implement Linux `.run` packager and smoke install/uninstall checks. 7. Implement macOS `.app`, `.pkg`, signing, notarization, and readiness checks. 8. Implement Windows `.exe`, `.msi`, `.msix`, signing/provider distinctions, and readiness checks. diff --git a/docs/rfc-terminal-shell.md b/docs/rfc-terminal-shell.md index 26a12117..69470be8 100644 --- a/docs/rfc-terminal-shell.md +++ b/docs/rfc-terminal-shell.md @@ -14,7 +14,7 @@ Unsupported widgets or render operations must fail during the terminal target bu ## 2. Goals -- Add a terminal platform target that can be selected with normal Fission CLI commands. +- Add a terminal platform target that can be selected with normal Fission command commands. - Render Fission UI into a deterministic terminal cell buffer. - Support keyboard-first interaction, focus traversal, selection, text input, scroll, dialogs, menus, buttons, forms, tables, lists, and progress indicators. - Fail the terminal build for unsupported lowered operations in declared routes/screens/states. diff --git a/documentation/content/blog/2026-05-16-welcome.mdx b/documentation/content/blog/2026-05-16-welcome.mdx index b2d1840a..7a53bbb0 100644 --- a/documentation/content/blog/2026-05-16-welcome.mdx +++ b/documentation/content/blog/2026-05-16-welcome.mdx @@ -21,4 +21,4 @@ Use: - `/docs/charts` for chart guides and the implemented chart catalog, - `/docs/cookbook` for practical application walkthroughs. -The project lives in `documentation/`, and you can build it locally with `cargo fission site build --project-dir documentation` or serve it with `cargo fission site serve --project-dir documentation`. +The project lives in `documentation/`, and you can build it locally with `fission site build --project-dir documentation` or serve it with `fission site serve --project-dir documentation`. diff --git a/documentation/content/docs/cookbook/add-platform-targets.mdx b/documentation/content/docs/cookbook/add-platform-targets.mdx index 9ac5b474..7ca6fbcc 100644 --- a/documentation/content/docs/cookbook/add-platform-targets.mdx +++ b/documentation/content/docs/cookbook/add-platform-targets.mdx @@ -17,7 +17,7 @@ You have a Fission app and you want to reach another platform: the browser, Andr You do not want to fork the app into separate codebases. You want one shared runtime model and multiple real hosts around it. -That is what `cargo fission add-target` is for. +That is what `fission add-target` is for. ## Step 1: choose the first target by product need @@ -50,7 +50,7 @@ That is why target generation matters: it gives you the platform wrapper without From the project root, run: ```bash -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` The command-line interface, or command-line interface, is the tool that generates these host folders. @@ -60,12 +60,12 @@ If you are already standing inside the generated project directory, `my-app` is For example, if you only need the browser host right now, this is also valid: ```bash -cargo fission add-target web --project-dir my-app +fission add-target web --project-dir my-app ``` A beginner-friendly way to read the command is: -- `cargo fission` means "run the Fission command-line interface through Cargo" +- `fission` means "run the Fission command-line interface through Cargo" - `add-target` means "generate more host wrappers for this app" - `web ios android` are the host targets you want - `--project-dir my-app` tells the command-line interface which existing app to update diff --git a/documentation/content/docs/develop/workflow.mdx b/documentation/content/docs/develop/workflow.mdx index fcc9f8e6..477a67a7 100644 --- a/documentation/content/docs/develop/workflow.mdx +++ b/documentation/content/docs/develop/workflow.mdx @@ -1,6 +1,6 @@ --- title: Develop workflow -description: Use the Fission CLI, targets, shells, devices, logs, and project configuration during day-to-day app development. +description: Use the Fission command, targets, shells, devices, logs, and project configuration during day-to-day app development. --- # Develop workflow @@ -18,7 +18,7 @@ The Fission development workflow starts with one project and grows by adding tar ```bash fission init my-app -cargo fission add-target web android ios --project-dir my-app +fission add-target web android ios --project-dir my-app fission devices --project-dir my-app fission run --project-dir my-app ``` diff --git a/documentation/content/docs/guides/platform-shells-cli-and-testing.mdx b/documentation/content/docs/guides/platform-shells-cli-and-testing.mdx index 10337a04..293de1e0 100644 --- a/documentation/content/docs/guides/platform-shells-cli-and-testing.mdx +++ b/documentation/content/docs/guides/platform-shells-cli-and-testing.mdx @@ -87,14 +87,14 @@ you get the shared app files, the initial desktop entrypoint, the `fission.toml` When you later run: ```bash -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` you are not cloning your product into separate codebases. You are generating additional host wrappers for the same shared app. That is the right way to think about the command-line interface. It creates and extends the host boundary. It does not change the core architecture of your app. -A generated host project is simply the platform-specific wrapper around the shared runtime. The command-line interface is the normal front door for that wrapper: `cargo fission devices` shows where the app can run, `cargo fission run --target ` builds and launches it, `cargo fission logs` reconnects to supported log streams, and `cargo fission test --target ` runs the generated smoke test. The lower-level scripts remain checked in under `platforms//` for continuous integration and advanced debugging, but day-to-day development should not require memorizing platform-specific script names. +A generated host project is simply the platform-specific wrapper around the shared runtime. The command-line interface is the normal front door for that wrapper: `fission devices` shows where the app can run, `fission run --target ` builds and launches it, `fission logs` reconnects to supported log streams, and `fission test --target ` runs the generated smoke test. The lower-level scripts remain checked in under `platforms//` for continuous integration and advanced debugging, but day-to-day development should not require memorizing platform-specific script names. On the web, that means building the WebAssembly output, serving it locally, and opening the browser when requested. On Android and iOS, that means packaging, installing, launching, and attaching to the device or simulator logs by default. diff --git a/documentation/content/docs/guides/static-sites.mdx b/documentation/content/docs/guides/static-sites.mdx index 421fd015..434628cc 100644 --- a/documentation/content/docs/guides/static-sites.mdx +++ b/documentation/content/docs/guides/static-sites.mdx @@ -70,7 +70,7 @@ generate_sitemap = true generate_robots = true ``` -The `entry` value tells the command-line interface that this project has a Rust site app. When it is present, `cargo fission site build` runs the project's own site builder so custom routes, custom footers, theme choices, and content transforms are included. When it is absent, Fission can still build a content-only site from the configured content routes. +The `entry` value tells the command-line interface that this project has a Rust site app. When it is present, `fission site build` runs the project's own site builder so custom routes, custom footers, theme choices, and content transforms are included. When it is absent, Fission can still build a content-only site from the configured content routes. A practical directory layout looks like this: @@ -241,10 +241,10 @@ This documentation site enables those features because it is intended to be inde Use these commands from the repository or from a generated site project. ```sh -cargo fission site routes --project-dir documentation -cargo fission site check --project-dir documentation -cargo fission site build --project-dir documentation -cargo fission site serve --project-dir documentation --host 127.0.0.1 --port 8123 +fission site routes --project-dir documentation +fission site check --project-dir documentation +fission site build --project-dir documentation +fission site serve --project-dir documentation --host 127.0.0.1 --port 8123 ``` `routes` lists custom and content routes without writing the site. `check` renders every route and validates generated links without producing a deployable directory. `build` writes the configured output directory. `serve` builds first, then starts a local static file server and opens the browser unless `--no-open` is provided. @@ -252,8 +252,8 @@ cargo fission site serve --project-dir documentation --host 127.0.0.1 --port 812 For release verification, use the same commands with `--release` where supported: ```sh -cargo fission site check --project-dir documentation --release -cargo fission site build --project-dir documentation --release +fission site check --project-dir documentation --release +fission site build --project-dir documentation --release ``` ## Publishing @@ -263,7 +263,7 @@ A production publishing job should build the site from the checked-in Fission pr That is the recommended model for hosted static sites: 1. keep content, custom route widgets, sidebars, assets, and site config in source control; -2. run `cargo fission site check` or `cargo fission site build` in continuous integration; +2. run `fission site check` or `fission site build` in continuous integration; 3. verify key outputs such as `index.html`, `site.css`, `sitemap.xml`, search files, and copied assets; 4. upload the generated output directory to the host. diff --git a/documentation/content/docs/guides/terminal-user-interfaces.mdx b/documentation/content/docs/guides/terminal-user-interfaces.mdx index 940938ee..bac02b92 100644 --- a/documentation/content/docs/guides/terminal-user-interfaces.mdx +++ b/documentation/content/docs/guides/terminal-user-interfaces.mdx @@ -79,9 +79,9 @@ fn main() -> anyhow::Result<()> { Real applications should not keep everything in one file. Follow the same structure you would use for a production Fission app: keep state in a state module, reducers in an actions module, screens in a screens module, reusable widgets in a components module, and route selection in a routes module. -## Use the Fission CLI as the reference example +## Use the Fission command as the reference example -The interactive Fission CLI is a terminal Fission app. Running `fission ui --project-dir .` opens an app built from normal Fission widgets and hosted by `TerminalApp`. +The interactive Fission command is a terminal Fission app. Running `fission ui --project-dir .` opens an app built from normal Fission widgets and hosted by `TerminalApp`. The CLI UI is organised around the same patterns recommended for product applications: diff --git a/documentation/content/docs/learn/examples-and-targets.mdx b/documentation/content/docs/learn/examples-and-targets.mdx index 42d144d5..aabc52d7 100644 --- a/documentation/content/docs/learn/examples-and-targets.mdx +++ b/documentation/content/docs/learn/examples-and-targets.mdx @@ -75,7 +75,7 @@ This repository already contains concrete, runnable host proofs. `mobile-smoke` is the checked-in mobile path. It includes an Android emulator launcher under `examples/mobile-smoke/platforms/android/run-emulator.sh` and an iOS simulator launcher under `examples/mobile-smoke/platforms/ios/run-sim.sh`. Those examples matter because they keep the platform claims grounded in runnable hosts, not just abstract diagrams. -The command-line interface can generate equivalent host folders for your own app with `cargo fission add-target ...`, so you are not limited to the checked-in examples when you move into your own project. +The command-line interface can generate equivalent host folders for your own app with `fission add-target ...`, so you are not limited to the checked-in examples when you move into your own project. ## What to keep in mind as you grow diff --git a/documentation/content/docs/learn/quickstart.mdx b/documentation/content/docs/learn/quickstart.mdx index a5fb5d48..f063c927 100644 --- a/documentation/content/docs/learn/quickstart.mdx +++ b/documentation/content/docs/learn/quickstart.mdx @@ -24,19 +24,19 @@ cargo --version If both commands print version numbers, you are ready for the next step. -## 2. Install the Fission command-line interface +## 2. Install the Fission command -Fission includes a first-party command-line interface, usually shortened to command-line interface. A command-line interface is just a tool you run from the terminal. +Fission includes a first-party command-line tool. It is the `fission` program you run from the terminal. -In Fission, the command-line interface does two important jobs for beginners. First, it creates the starting project structure for you so you do not have to assemble the files by hand. Second, later on, it can generate the platform-specific wrappers for web, Android, and iOS. +For beginners, the command does two important jobs. First, it creates the starting project structure for you so you do not have to assemble the files by hand. Second, later on, it can generate the platform-specific wrappers for web, Android, and iOS. Install it once with: ```bash -cargo install fission-cli +cargo install cargo-fission ``` -This command asks `cargo` to download, build, and install the Fission command-line interface on your machine. After it completes, you will have the `fission` command for creating projects, and the `cargo fission` command for adding more targets later. +This command asks `cargo` to download, build, and install Fission's command-line tool on your machine. After it completes, use the single `fission` command for creating projects, adding targets, running apps, checking setup, packaging, and publishing. ## 3. Create your first app @@ -103,7 +103,7 @@ When you add one of these targets, the command-line interface creates platform f Web is usually the easiest next step after desktop. ```bash -cargo fission add-target web --project-dir my-app +fission add-target web --project-dir my-app ``` This updates `fission.toml`, creates `platforms/web/`, and adds files such as a browser host page plus the `platforms/web/run-browser.sh` host script. That script builds the WebAssembly package and serves it locally. Before running it, read `platforms/web/README.md` for the exact prerequisites, including `wasm-pack`. @@ -111,7 +111,7 @@ This updates `fission.toml`, creates `platforms/web/`, and adds files such as a After the target is added, the normal command is: ```bash -cargo fission run --target web --project-dir my-app +fission run --target web --project-dir my-app ``` That builds the browser package, serves it locally, and keeps the terminal attached to the local server so you can see what is happening. @@ -121,7 +121,7 @@ That builds the browser package, serves it locally, and keeps the terminal attac Android needs the Android Software Development Kit and Native Development Kit installed first, so it is usually a second-day step rather than a first-day step. ```bash -cargo fission add-target android --project-dir my-app +fission add-target android --project-dir my-app ``` This creates `platforms/android/`, including files such as the Android manifest and `platforms/android/run-emulator.sh`. That host script builds the app, packages it, installs it into an emulator, and launches it. Read `platforms/android/README.md` first so you know which environment variables and Android tools are required on your machine. @@ -129,8 +129,8 @@ This creates `platforms/android/`, including files such as the Android manifest Once your Android setup is healthy, ask Fission what devices it can see and then run the app: ```bash -cargo fission devices --project-dir my-app -cargo fission run --target android --project-dir my-app +fission devices --project-dir my-app +fission run --target android --project-dir my-app ``` If more than one Android device or emulator is available, pass `--device ` using the id printed by the devices command. @@ -140,7 +140,7 @@ If more than one Android device or emulator is available, pass `--device ` u iOS also has extra prerequisites, including Xcode and simulator tooling. ```bash -cargo fission add-target ios --project-dir my-app +fission add-target ios --project-dir my-app ``` This creates `platforms/ios/`, including bundle files and `platforms/ios/run-sim.sh`. That host script builds the app and launches it in an iPhone simulator. Read `platforms/ios/README.md` before you rely on it, because the generated notes are the right place to check the current prerequisites and status for the simulator path. @@ -148,8 +148,8 @@ This creates `platforms/ios/`, including bundle files and `platforms/ios/run-sim When the simulator tools are installed, use the same device-and-run flow: ```bash -cargo fission devices --project-dir my-app -cargo fission run --target ios --project-dir my-app +fission devices --project-dir my-app +fission run --target ios --project-dir my-app ``` The run command attaches to simulator logs by default. That makes it the main development entrypoint instead of something you only use after a separate launch step. diff --git a/documentation/content/reference/cli/overview.mdx b/documentation/content/reference/cli/overview.mdx index c14ccd9f..3c478a26 100644 --- a/documentation/content/reference/cli/overview.mdx +++ b/documentation/content/reference/cli/overview.mdx @@ -22,7 +22,7 @@ Most teams meet the command-line interface at two moments. The first moment is the start of a project. You run `fission init` when you want a fresh app with the expected file layout, a starter application, an initial desktop host, and a `fission.toml` manifest that records project metadata and target choices. -The second moment is when your shared app is ready to run in another host. At that point, you use `cargo fission add-target` to generate the extra platform folder for web, Android, or iOS, or to add a specific desktop target if needed. The important idea is that you are not rewriting your app for each platform. You are generating the host wrapper around the same shared app model. +The second moment is when your shared app is ready to run in another host. At that point, you use `fission add-target` to generate the extra platform folder for web, Android, or iOS, or to add a specific desktop target if needed. The important idea is that you are not rewriting your app for each platform. You are generating the host wrapper around the same shared app model. After that, the command-line interface becomes part of the normal development loop. You can ask it which devices are available, choose a device by id, run the app, attach to logs, build without launching, or run the generated smoke test. That keeps the workflow close to the app instead of forcing every developer on the team to memorize different shell scripts for each platform. @@ -35,19 +35,19 @@ Fission exposes these public command-line interface entry points: | Command | What it does | When you usually reach for it | | --- | --- | --- | | `fission init ` | Creates a new Fission project scaffold | When you are starting a new app | -| `cargo fission add-target ` | Adds generated host folders for more platforms | When your existing app needs another host | -| `cargo fission doctor [targets...]` | Checks local SDKs, browsers, emulators, and Rust targets | When setting up a machine or diagnosing a broken platform loop | -| `cargo fission devices` | Lists runnable desktop, browser, simulator, emulator, and device targets | Before choosing where to run the app | -| `cargo fission run` | Builds and launches the app, attaching output or device logs by default | During normal development | -| `cargo fission build` | Builds a target without launching it | Before sharing an artifact with someone else | -| `cargo fission test` | Runs the generated platform smoke test | Before merging target-specific work | -| `cargo fission logs` | Attaches to logs for a running or detached target where supported | When you launched with `--detach` or need to reconnect | -| `cargo fission site routes` | Lists generated custom and content site routes | Before checking navigation or publishing a static site | -| `cargo fission site check` | Renders all static site routes and validates links without writing the final site | In pull-request validation for site content | -| `cargo fission site build` | Builds the static site into the configured output directory | Before publishing documentation or marketing pages | -| `cargo fission site serve` | Builds and serves the generated static site locally | When reviewing site pages in a browser | - -You may also see `cargo-fission` in the repository. That binary exists so the command can be used naturally through Cargo as `cargo fission ...`. +| `fission add-target ` | Adds generated host folders for more platforms | When your existing app needs another host | +| `fission doctor [targets...]` | Checks local SDKs, browsers, emulators, and Rust targets | When setting up a machine or diagnosing a broken platform loop | +| `fission devices` | Lists runnable desktop, browser, simulator, emulator, and device targets | Before choosing where to run the app | +| `fission run` | Builds and launches the app, attaching output or device logs by default | During normal development | +| `fission build` | Builds a target without launching it | Before sharing an artifact with someone else | +| `fission test` | Runs the generated platform smoke test | Before merging target-specific work | +| `fission logs` | Attaches to logs for a running or detached target where supported | When you launched with `--detach` or need to reconnect | +| `fission site routes` | Lists generated custom and content site routes | Before checking navigation or publishing a static site | +| `fission site check` | Renders all static site routes and validates links without writing the final site | In pull-request validation for site content | +| `fission site build` | Builds the static site into the configured output directory | Before publishing documentation or marketing pages | +| `fission site serve` | Builds and serves the generated static site locally | When reviewing site pages in a browser | + +Repository developers may see the package name `cargo-fission` in workspace commands. That is the package that builds and installs the developer-facing `fission` command; product documentation and generated project files should use `fission ...`. ## Starting a new app with `fission init` @@ -90,14 +90,14 @@ These are the files most people open first: The key idea is that only some of those files describe your app. Files under `src/` are the shared product logic. Files under `platforms/` are host-specific scaffolding. The command-line interface keeps those concerns separate because Fission itself keeps the shared app model separate from the platform shell that runs it. -On an existing project, treat the table as "created if missing." Existing files are preserved. The main required output is `fission.toml`, because commands such as `cargo fission run --project-dir . --target web` use it to know which targets belong to the app. +On an existing project, treat the table as "created if missing." Existing files are preserved. The main required output is `fission.toml`, because commands such as `fission run --project-dir . --target web` use it to know which targets belong to the app. -## Adding more hosts with `cargo fission add-target` +## Adding more hosts with `fission add-target` -Once you have an existing app, use `cargo fission add-target` to generate another host around it. +Once you have an existing app, use `fission add-target` to generate another host around it. ```sh -cargo fission add-target web ios android --project-dir my-app +fission add-target web ios android --project-dir my-app ``` This command does not duplicate your shared widgets or reducers. Instead, it updates `fission.toml` and writes the generated files needed for the requested hosts under `platforms/`. @@ -125,35 +125,35 @@ The current target values are: In practice, many projects start with the default desktop scaffold and then add `web`, `android`, or `ios` when that host becomes relevant to the product. The important thing is not the order. The important thing is that the same shared app can be validated through each real host. -## Checking your machine with `cargo fission doctor` +## Checking your machine with `fission doctor` Run doctor when a platform does not build, when you are setting up a new workstation, or when you are preparing a continuous integration runner. ```sh -cargo fission doctor web ios android --project-dir my-app +fission doctor web ios android --project-dir my-app ``` Doctor checks the tools that are visible from your current environment. For web, that includes the WebAssembly Rust target, `wasm-pack`, Node.js, and Chrome or Chromium. For Android, it checks the Android SDK, Native Development Kit, emulator tooling, and Rust Android target. For iOS, it checks Xcode simulator tooling and the Rust iOS simulator target. Use `--strict` when you want the command to fail the build if a required tool is missing. -## Finding devices with `cargo fission devices` +## Finding devices with `fission devices` Run devices before you run an app on a machine with more than one possible host. ```sh -cargo fission devices --project-dir my-app +fission devices --project-dir my-app ``` -The output includes stable ids that you can pass back into `cargo fission run --device `. Desktop uses `desktop`, web uses `chrome`, Android uses the `adb` serial for connected devices and running emulators, and iOS uses the simulator unique device identifier. If you are scripting the workflow, add `--json` and parse the same data without relying on table formatting. +The output includes stable ids that you can pass back into `fission run --device `. Desktop uses `desktop`, web uses `chrome`, Android uses the `adb` serial for connected devices and running emulators, and iOS uses the simulator unique device identifier. If you are scripting the workflow, add `--json` and parse the same data without relying on table formatting. -## Running and attaching with `cargo fission run` +## Running and attaching with `fission run` -`cargo fission run` is the default development command once a target exists. +`fission run` is the default development command once a target exists. ```sh -cargo fission run --project-dir my-app -cargo fission run --target web --project-dir my-app -cargo fission run --target android --device emulator-5554 --project-dir my-app -cargo fission run --target ios --device --project-dir my-app +fission run --project-dir my-app +fission run --target web --project-dir my-app +fission run --target android --device emulator-5554 --project-dir my-app +fission run --target ios --device --project-dir my-app ``` If only one runnable device matches the target, the command uses it. If several devices match and the terminal is interactive, the command asks you to choose one. In a script or continuous integration job, pass `--device` so the choice is explicit. @@ -163,8 +163,8 @@ The command attaches by default because that is the most useful development beha Use `--detach` when you want the app or local server to keep running without owning the terminal. ```sh -cargo fission run --target web --project-dir my-app --detach -cargo fission logs --target web --project-dir my-app --follow +fission run --target web --project-dir my-app --detach +fission logs --target web --project-dir my-app --follow ``` Web also accepts `--host`, `--port`, and `--no-open`. Mobile targets accept `--headless` for simulator or emulator runs where the host supports a background launch. @@ -174,16 +174,16 @@ Web also accepts `--host`, `--port`, and `--no-open`. Mobile targets accept `--h Use `build` when you want the target artifact but do not want to launch it. ```sh -cargo fission build --target web --project-dir my-app --release -cargo fission build --target android --project-dir my-app --release +fission build --target web --project-dir my-app --release +fission build --target android --project-dir my-app --release ``` Use `test` when you want the generated platform smoke test. ```sh -cargo fission test --target web --project-dir my-app -cargo fission test --target ios --project-dir my-app --headless -cargo fission test --target android --project-dir my-app --headless +fission test --target web --project-dir my-app +fission test --target ios --project-dir my-app --headless +fission test --target android --project-dir my-app --headless ``` The smoke tests are intentionally not a replacement for your application test suite. They prove that the generated platform host can build, launch, and expose the basic health path for that target. Your app should still have reducer tests, selector tests, widget tests, and product-specific integration tests. @@ -205,7 +205,7 @@ Second, they contain scripts or host files. These are the concrete launch paths | `platforms/android/AndroidManifest.xml` | Android manifest | Declares the generated Android app package | Written by the command-line interface as part of the host scaffold | | `platforms/ios/run-sim.sh` | shell script | Builds, installs, and launches the app on an iOS simulator | Uses the generated simulator host bundle | -If you are a beginner, the practical rule is simple: edit your shared app in `src/`, then use `cargo fission run --target ` when you want to validate a specific host. The generated scripts remain in `platforms//` for direct CI use and for advanced debugging, but the command-line interface is the normal front door. +If you are a beginner, the practical rule is simple: edit your shared app in `src/`, then use `fission run --target ` when you want to validate a specific host. The generated scripts remain in `platforms//` for direct CI use and for advanced debugging, but the command-line interface is the normal front door. ## A practical way to use the command-line interface without overthinking it @@ -215,11 +215,11 @@ One common flow looks like this: 1. Run `fission init my-app`. 2. Open `src/app.rs` and start shaping your real app state, actions, reducers, and widgets. -3. Run `cargo fission doctor --project-dir my-app` on a new machine. -4. Run `cargo fission devices --project-dir my-app` to see where the app can run. -5. Run the host that is most convenient for your current work with `cargo fission run --target --project-dir my-app`. -6. When you need another platform host, run `cargo fission add-target ...`. -7. Before merging target work, run `cargo fission test --target --project-dir my-app`. +3. Run `fission doctor --project-dir my-app` on a new machine. +4. Run `fission devices --project-dir my-app` to see where the app can run. +5. Run the host that is most convenient for your current work with `fission run --target --project-dir my-app`. +6. When you need another platform host, run `fission add-target ...`. +7. Before merging target work, run `fission test --target --project-dir my-app`. That flow stays aligned with Fission's larger design. The shared app model comes first. The command-line interface then helps each platform shell host run that model in a concrete environment. @@ -235,109 +235,109 @@ fission init [--name ] [--app-id ] [--local-path ] Creates a new Fission project scaffold in the given directory. -### `cargo fission add-target` +### `fission add-target` ```sh -cargo fission add-target [--project-dir ] +fission add-target [--project-dir ] ``` Adds one or more generated host targets to an existing Fission project. -### `cargo fission doctor` +### `fission doctor` ```sh -cargo fission doctor [targets...] [--project-dir ] [--strict] +fission doctor [targets...] [--project-dir ] [--strict] ``` Checks the local platform toolchains needed for the selected targets. -### `cargo fission devices` +### `fission devices` ```sh -cargo fission devices [--project-dir ] [--json] +fission devices [--project-dir ] [--json] ``` Lists launchable desktop, web, Android, and iOS devices known to the command-line interface. -### `cargo fission run` +### `fission run` ```sh -cargo fission run [--target ] [--device ] [--project-dir ] [--release] [--detach] +fission run [--target ] [--device ] [--project-dir ] [--release] [--detach] ``` Builds, launches, and attaches to the selected target. Web also accepts `--host`, `--port`, and `--no-open`. Mobile targets also accept `--headless`. -### `cargo fission build` +### `fission build` ```sh -cargo fission build [--target ] [--project-dir ] [--release] +fission build [--target ] [--project-dir ] [--release] ``` Builds the selected target without launching it. -### `cargo fission test` +### `fission test` ```sh -cargo fission test [--target ] [--project-dir ] [--headless] +fission test [--target ] [--project-dir ] [--headless] ``` Runs the generated platform smoke test for the selected target. -### `cargo fission logs` +### `fission logs` ```sh -cargo fission logs [--target ] [--device ] [--project-dir ] [--follow] +fission logs [--target ] [--device ] [--project-dir ] [--follow] ``` Attaches to logs for a selected running or detached target where the platform exposes a log stream. -### `cargo fission site routes` +### `fission site routes` ```sh -cargo fission site routes [--project-dir ] +fission site routes [--project-dir ] ``` Lists the custom and content routes known to the static site target. Use it when you add Markdown files, change sidebars, or register custom widget routes and want to confirm the generated URL set before building the site. -### `cargo fission site check` +### `fission site check` ```sh -cargo fission site check [--project-dir ] [--release] +fission site check [--project-dir ] [--release] ``` Renders every static site route and validates generated internal links without producing a deployable output directory. This is the fastest command to use in pull-request checks for documentation and reference changes. -### `cargo fission site build` +### `fission site build` ```sh -cargo fission site build [--project-dir ] [--release] +fission site build [--project-dir ] [--release] ``` Builds the static site into the output directory configured by `[site].out_dir` in `fission.toml`. For the Fission documentation site, that means `documentation/dist/site`. The build writes HTML, `site.css`, copied assets, optional search files, optional code-highlighting hooks, `sitemap.xml`, and `robots.txt` when those features are enabled. -### `cargo fission site serve` +### `fission site serve` ```sh -cargo fission site serve [--project-dir ] [--host ] [--port ] [--release] [--no-open] +fission site serve [--project-dir ] [--host ] [--port ] [--release] [--no-open] ``` Builds the static site and serves the generated output with a local static file server. By default it binds to `127.0.0.1:8123` and opens the browser. Use `--no-open` for scripts or remote development environments. ## Static site workflow -The `site` commands are for documentation, marketing, reference, blog, and other mostly-static routes. They are separate from `cargo fission run --target web`, which launches an interactive web application. A site build starts from the same Fission widget model but resolves it ahead of time into crawlable HTML and CSS. +The `site` commands are for documentation, marketing, reference, blog, and other mostly-static routes. They are separate from `fission run --target web`, which launches an interactive web application. A site build starts from the same Fission widget model but resolves it ahead of time into crawlable HTML and CSS. The Fission documentation site in `documentation/` is the production example in this repository. It uses a custom home page widget, Markdown content routes, sidebars, a generated table of contents, light and dark themes, a custom footer, copied static assets, favicon support, generated search, optional code highlighting, sitemap and robots output, JSON-LD structured data, and link validation. You can inspect the route set with: ```sh -cargo fission site routes --project-dir documentation +fission site routes --project-dir documentation ``` Then check or build it with: ```sh -cargo fission site check --project-dir documentation -cargo fission site build --project-dir documentation +fission site check --project-dir documentation +fission site build --project-dir documentation ``` The generated output directory is build output, not source. Keep the site project, content, sidebars, assets, and Rust widgets in source control; publish the generated directory from continuous integration. diff --git a/documentation/content/reference/platform/targets.mdx b/documentation/content/reference/platform/targets.mdx index 6c3fe611..169b01fa 100644 --- a/documentation/content/reference/platform/targets.mdx +++ b/documentation/content/reference/platform/targets.mdx @@ -30,7 +30,7 @@ The current command-line interface target set includes: - `ios` - `android` -A fresh `fission init` project starts with the desktop family scaffolded. You add other targets with `cargo fission add-target ...` when the product or host you care about requires them. +A fresh `fission init` project starts with the desktop family scaffolded. You add other targets with `fission add-target ...` when the product or host you care about requires them. ## Choosing a target in practice diff --git a/documentation/content/reference/widgets/widget-pages/code.mdx b/documentation/content/reference/widgets/widget-pages/code.mdx index e76e851c..7a6092aa 100644 --- a/documentation/content/reference/widgets/widget-pages/code.mdx +++ b/documentation/content/reference/widgets/widget-pages/code.mdx @@ -15,7 +15,7 @@ Use it for command names, short snippets, key values, or other compact inline te use fission::prelude::*; let node = Code { - text: "cargo fission run".into(), + text: "fission run".into(), } .build(ctx, view); ``` diff --git a/documentation/platforms/site/README.md b/documentation/platforms/site/README.md index faf70fa3..856459ff 100644 --- a/documentation/platforms/site/README.md +++ b/documentation/platforms/site/README.md @@ -6,9 +6,9 @@ The site is a real Fission app: custom pages are Rust widgets, Markdown and MDX Useful commands: -- `cargo fission site routes --project-dir documentation` -- list generated custom and content routes -- `cargo fission site check --project-dir documentation` -- render all routes and validate internal links -- `cargo fission site build --project-dir documentation` -- write the configured output directory -- `cargo fission site serve --project-dir documentation` -- build and serve locally on `127.0.0.1:8123` +- `fission site routes --project-dir documentation` -- list generated custom and content routes +- `fission site check --project-dir documentation` -- render all routes and validate internal links +- `fission site build --project-dir documentation` -- write the configured output directory +- `fission site serve --project-dir documentation` -- build and serve locally on `127.0.0.1:8123` The generated output under `dist/site` is build output and should not be committed. diff --git a/documentation/src/components/home_sections.rs b/documentation/src/components/home_sections.rs index 2c0867c2..ff821e78 100644 --- a/documentation/src/components/home_sections.rs +++ b/documentation/src/components/home_sections.rs @@ -518,7 +518,7 @@ impl Widget for ExamplesSection { vec![ ExampleCard::new("Starter", "Counter", "cargo run -p counter", "The smallest complete Fission app loop: plain state, two reducers, a widget tree, and buttons bound with the public prelude macros.", "typed actions and reducers", "single-file starter app", "/docs/cookbook/build-a-counter/", "/reference/core/state-system/").build(ctx, view), ExampleCard::new("Site", "Documentation", "fission site build --project-dir documentation", "This website is a Fission static site: custom homepage widgets, Markdown content routes, generated search, metadata, sidebars, and GitHub Pages output.", "static HTML shell", "content routes and custom widgets", "/docs/guides/static-sites/", "/product/static-sites/").build(ctx, view), - ExampleCard::new("Terminal", "Fission CLI UI", "fission ui --project-dir .", "The CLI includes a terminal Fission app with screens, routes, reducers, dialogs, command sessions, logs, settings, density, and theme switching.", "terminal shell", "non-blocking command workflow", "/docs/guides/terminal-user-interfaces/", "/product/terminal-apps/").build(ctx, view), + ExampleCard::new("Terminal", "Fission command UI", "fission ui --project-dir .", "The CLI includes a terminal Fission app with screens, routes, reducers, dialogs, command sessions, logs, settings, density, and theme switching.", "terminal shell", "non-blocking command workflow", "/docs/guides/terminal-user-interfaces/", "/product/terminal-apps/").build(ctx, view), ], ) .build(ctx, view) diff --git a/documentation/src/components/marketing.rs b/documentation/src/components/marketing.rs index dfb54713..19a6b8c7 100644 --- a/documentation/src/components/marketing.rs +++ b/documentation/src/components/marketing.rs @@ -238,7 +238,7 @@ impl MarketingPageKind { secondary_label: "Try fission ui", secondary_href: "/reference/cli/overview/", proof_label: "Built into the CLI", - proof_body: "The Fission CLI UI is implemented as a Fission terminal app with routes, screens, reducers, dialogs, settings, and command sessions.", + proof_body: "The Fission command UI is implemented as a Fission terminal app with routes, screens, reducers, dialogs, settings, and command sessions.", features: TERMINAL_FEATURES, workflow: TERMINAL_STEPS, }, diff --git a/examples/mobile-smoke/README.md b/examples/mobile-smoke/README.md index 044e3448..86d649c9 100644 --- a/examples/mobile-smoke/README.md +++ b/examples/mobile-smoke/README.md @@ -51,7 +51,7 @@ export ANDROID_MIN_API_LEVEL=24 ./examples/mobile-smoke/platforms/android/test-emulator.sh ``` -The package script auto-detects the newest installed NDK, the matching NDK LLVM host toolchain, the latest installed Android platform, and build-tools. Use `cargo fission doctor android --project-dir examples/mobile-smoke` when your SDK layout needs explicit environment variables. +The package script auto-detects the newest installed NDK, the matching NDK LLVM host toolchain, the latest installed Android platform, and build-tools. Use `fission doctor android --project-dir examples/mobile-smoke` when your SDK layout needs explicit environment variables. Android emulator controls: diff --git a/examples/mobile-smoke/platforms/android/run-emulator.sh b/examples/mobile-smoke/platforms/android/run-emulator.sh index 83d776d1..1376fcd5 100755 --- a/examples/mobile-smoke/platforms/android/run-emulator.sh +++ b/examples/mobile-smoke/platforms/android/run-emulator.sh @@ -34,7 +34,7 @@ RESTART_EMULATOR="${ANDROID_EMULATOR_RESTART:-0}" for tool in "$ADB" "$EMULATOR_BIN" "$AVDMANAGER"; do if [[ ! -x "$tool" ]]; then - printf 'Required Android tool is missing or not executable: %s\nRun `cargo fission doctor android --project-dir examples/mobile-smoke` for setup help.\n' "$tool" >&2 + printf 'Required Android tool is missing or not executable: %s\nRun `fission doctor android --project-dir examples/mobile-smoke` for setup help.\n' "$tool" >&2 exit 1 fi done diff --git a/examples/web-smoke/README.md b/examples/web-smoke/README.md index 2754217b..0066cfc3 100644 --- a/examples/web-smoke/README.md +++ b/examples/web-smoke/README.md @@ -13,17 +13,17 @@ Generated by `fission init`. ## Commands -- `cargo fission doctor --project-dir .` -- check local SDKs, browsers, emulators, and Rust targets -- `cargo fission devices --project-dir .` -- list runnable desktop, browser, simulator, emulator, and device targets -- `cargo fission run --project-dir .` -- launch the desktop app and attach to output -- `cargo fission run --target web --project-dir .` -- launch the web app and attach to the local server -- `cargo fission run --target ios --project-dir .` -- build, install, launch, and attach to simulator logs -- `cargo fission run --target android --project-dir .` -- build, install, launch, and attach to Android logs -- `cargo fission run --target --device --detach --project-dir .` -- launch without attaching -- `cargo fission logs --target --device --project-dir . --follow` -- attach later where supported -- `cargo fission build --target --project-dir . --release` -- build a target without launching it -- `cargo fission test --target --project-dir .` -- run the generated platform smoke test -- `cargo fission add-target web ios android --project-dir .` -- scaffold more targets +- `fission doctor --project-dir .` -- check local SDKs, browsers, emulators, and Rust targets +- `fission devices --project-dir .` -- list runnable desktop, browser, simulator, emulator, and device targets +- `fission run --project-dir .` -- launch the desktop app and attach to output +- `fission run --target web --project-dir .` -- launch the web app and attach to the local server +- `fission run --target ios --project-dir .` -- build, install, launch, and attach to simulator logs +- `fission run --target android --project-dir .` -- build, install, launch, and attach to Android logs +- `fission run --target --device --detach --project-dir .` -- launch without attaching +- `fission logs --target --device --project-dir . --follow` -- attach later where supported +- `fission build --target --project-dir . --release` -- build a target without launching it +- `fission test --target --project-dir .` -- run the generated platform smoke test +- `fission add-target web ios android --project-dir .` -- scaffold more targets - `cat platforms//README.md` -- inspect target-specific prerequisites and environment variables ## Assets @@ -32,4 +32,4 @@ Generated by `fission init`. ## Status -Desktop, web, iOS simulator, and Android emulator workflows are runnable through `cargo fission run`. The platform scripts remain checked in so CI and advanced users can call the lower-level build, run, and smoke-test steps directly when needed. +Desktop, web, iOS simulator, and Android emulator workflows are runnable through `fission run`. The platform scripts remain checked in so CI and advanced users can call the lower-level build, run, and smoke-test steps directly when needed. diff --git a/examples/web-smoke/platforms/android/README.md b/examples/web-smoke/platforms/android/README.md index 89d30a90..383d3417 100644 --- a/examples/web-smoke/platforms/android/README.md +++ b/examples/web-smoke/platforms/android/README.md @@ -3,11 +3,11 @@ Runnable emulator target. The CLI generates a NativeActivity manifest plus shell scripts that build, install, and launch the Fission app on an Android emulator. - Install the Rust target: `rustup target add aarch64-linux-android`. -- Run `cargo fission doctor android --project-dir .` to check SDK, NDK, emulator, and Rust target setup. -- Run `cargo fission devices --project-dir .` to list connected Android devices and configured emulators. -- Run `cargo fission run --target android --project-dir .` to build, install, launch, and attach to logs. -- Run `cargo fission run --target android --device --project-dir .` to launch on a specific device. -- Run `cargo fission test --target android --project-dir .` for an emulator launch plus test-control health check. +- Run `fission doctor android --project-dir .` to check SDK, NDK, emulator, and Rust target setup. +- Run `fission devices --project-dir .` to list connected Android devices and configured emulators. +- Run `fission run --target android --project-dir .` to build, install, launch, and attach to logs. +- Run `fission run --target android --device --project-dir .` to launch on a specific device. +- Run `fission test --target android --project-dir .` for an emulator launch plus test-control health check. - Run `./platforms/android/run-emulator.sh` from the project root to build, package, install, and launch the app on the configured emulator. - Override `ANDROID_HOME`, `ANDROID_NDK`, `ANDROID_MIN_API_LEVEL`, `ANDROID_TARGET_API_LEVEL`, `ANDROID_AVD_NAME`, or `ANDROID_SYSTEM_IMAGE` if your local SDK setup differs. - Set `ANDROID_EMULATOR_HEADLESS=1` for background/CI runs, or `ANDROID_EMULATOR_RESTART=1` to relaunch a hidden emulator visibly. diff --git a/examples/web-smoke/platforms/android/run-emulator.sh b/examples/web-smoke/platforms/android/run-emulator.sh index 7d32d0da..c43f6a30 100755 --- a/examples/web-smoke/platforms/android/run-emulator.sh +++ b/examples/web-smoke/platforms/android/run-emulator.sh @@ -34,7 +34,7 @@ RESTART_EMULATOR="${ANDROID_EMULATOR_RESTART:-0}" for tool in "$ADB" "$EMULATOR_BIN" "$AVDMANAGER"; do if [[ ! -x "$tool" ]]; then - printf 'Required Android tool is missing or not executable: %s\nRun `cargo fission doctor android --project-dir .` for setup help.\n' "$tool" >&2 + printf 'Required Android tool is missing or not executable: %s\nRun `fission doctor android --project-dir .` for setup help.\n' "$tool" >&2 exit 1 fi done diff --git a/examples/web-smoke/platforms/ios/README.md b/examples/web-smoke/platforms/ios/README.md index 3061efbe..7adea480 100644 --- a/examples/web-smoke/platforms/ios/README.md +++ b/examples/web-smoke/platforms/ios/README.md @@ -3,12 +3,12 @@ Simulator target. The CLI generates a simulator app bundle template plus shell scripts that build, install, launch, and smoke-test the Fission app with `simctl`. - Install the Rust targets: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim`. -- Run `cargo fission doctor ios --project-dir .` to check Xcode, simulator, and Rust target setup. +- Run `fission doctor ios --project-dir .` to check Xcode, simulator, and Rust target setup. - Confirm the simulator SDK path with `xcrun --sdk iphonesimulator --show-sdk-path`. -- Run `cargo fission devices --project-dir .` to list available iOS simulators. -- Run `cargo fission run --target ios --project-dir .` to build, install, launch, and attach to simulator logs. -- Run `cargo fission run --target ios --device --project-dir .` to launch on a specific simulator. -- Run `cargo fission test --target ios --project-dir .` for a simulator launch plus test-control health check. +- Run `fission devices --project-dir .` to list available iOS simulators. +- Run `fission run --target ios --project-dir .` to build, install, launch, and attach to simulator logs. +- Run `fission run --target ios --device --project-dir .` to launch on a specific simulator. +- Run `fission test --target ios --project-dir .` for a simulator launch plus test-control health check. - Run `./platforms/ios/run-sim.sh` from the project root to build, install, and launch the app on the first available iPhone simulator. - The generated bundle uses `assets/app-icon.png` as its default app icon. - Set `FISSION_TEST_CONTROL_PORT=` before `run-sim.sh` to expose the in-app test control server on the host. diff --git a/examples/web-smoke/platforms/linux/README.md b/examples/web-smoke/platforms/linux/README.md index 502801f0..dc6c7a7a 100644 --- a/examples/web-smoke/platforms/linux/README.md +++ b/examples/web-smoke/platforms/linux/README.md @@ -2,7 +2,7 @@ Runnable target. Desktop platforms share the default `src/main.rs` entrypoint through `DesktopApp`. -- Run `cargo fission run --project-dir .` from the project root to launch the desktop app and attach output. -- Run `cargo fission build --project-dir . --release` for a release desktop build. -- Run `cargo fission test --project-dir .` for the app crate's Rust tests. +- Run `fission run --project-dir .` from the project root to launch the desktop app and attach output. +- Run `fission build --project-dir . --release` for a release desktop build. +- Run `fission test --project-dir .` for the app crate's Rust tests. - This target uses the default Vello desktop shell path. diff --git a/examples/web-smoke/platforms/macos/README.md b/examples/web-smoke/platforms/macos/README.md index 0c508d06..c1c7ec3e 100644 --- a/examples/web-smoke/platforms/macos/README.md +++ b/examples/web-smoke/platforms/macos/README.md @@ -2,7 +2,7 @@ Runnable target. Desktop platforms share the default `src/main.rs` entrypoint through `DesktopApp`. -- Run `cargo fission run --project-dir .` from the project root to launch the desktop app and attach output. -- Run `cargo fission build --project-dir . --release` for a release desktop build. -- Run `cargo fission test --project-dir .` for the app crate's Rust tests. +- Run `fission run --project-dir .` from the project root to launch the desktop app and attach output. +- Run `fission build --project-dir . --release` for a release desktop build. +- Run `fission test --project-dir .` for the app crate's Rust tests. - This target uses the default Vello desktop shell path. diff --git a/examples/web-smoke/platforms/web/README.md b/examples/web-smoke/platforms/web/README.md index c80e6e2f..c4524a8b 100644 --- a/examples/web-smoke/platforms/web/README.md +++ b/examples/web-smoke/platforms/web/README.md @@ -5,11 +5,11 @@ Runnable browser target. The CLI generates a WASM host page plus helper scripts - Install the Rust target: `rustup target add wasm32-unknown-unknown`. - Install `wasm-pack` once: `cargo install wasm-pack`. - Install Node.js 22+ so the smoke test can inspect Chrome/Chromium CDP runtime and console output. -- Run `cargo fission doctor web --project-dir .` to check wasm-pack, Node.js, Chrome/Chromium, and Rust target setup. -- Run `cargo fission devices --project-dir .` to confirm Chrome/Chromium detection. -- Run `cargo fission run --target web --project-dir .` to build, serve, open, and attach to the local server. -- Run `cargo fission run --target web --detach --project-dir .` to keep the local server running in the background. -- Run `cargo fission test --target web --project-dir .` for a headless Chrome/Chromium CDP smoke test. +- Run `fission doctor web --project-dir .` to check wasm-pack, Node.js, Chrome/Chromium, and Rust target setup. +- Run `fission devices --project-dir .` to confirm Chrome/Chromium detection. +- Run `fission run --target web --project-dir .` to build, serve, open, and attach to the local server. +- Run `fission run --target web --detach --project-dir .` to keep the local server running in the background. +- Run `fission test --target web --project-dir .` for a headless Chrome/Chromium CDP smoke test. - Run `./platforms/web/run-browser.sh` from the project root to build the wasm package and serve the app locally. - Set `FISSION_WEB_PORT=` or `FISSION_WEB_HOST=` if the default `127.0.0.1:8123` does not suit your machine. - Set `FISSION_WEB_OPEN=1` if you want the helper script to open a browser tab automatically. diff --git a/examples/web-smoke/platforms/web/test-browser.sh b/examples/web-smoke/platforms/web/test-browser.sh index 0c564603..dd0c0724 100755 --- a/examples/web-smoke/platforms/web/test-browser.sh +++ b/examples/web-smoke/platforms/web/test-browser.sh @@ -83,7 +83,7 @@ raise SystemExit(f"web server did not serve {url}: {last_error}") PY CHROME=$(detect_chrome) || { - printf 'Chrome/Chromium was not found. Set FISSION_CHROME=/path/to/chrome or run `cargo fission doctor web --project-dir .`.\n' >&2 + printf 'Chrome/Chromium was not found. Set FISSION_CHROME=/path/to/chrome or run `fission doctor web --project-dir .`.\n' >&2 exit 1 } diff --git a/examples/web-smoke/platforms/windows/README.md b/examples/web-smoke/platforms/windows/README.md index 6e5f78a2..3fa09a80 100644 --- a/examples/web-smoke/platforms/windows/README.md +++ b/examples/web-smoke/platforms/windows/README.md @@ -2,7 +2,7 @@ Runnable target. Desktop platforms share the default `src/main.rs` entrypoint through `DesktopApp`. -- Run `cargo fission run --project-dir .` from the project root to launch the desktop app and attach output. -- Run `cargo fission build --project-dir . --release` for a release desktop build. -- Run `cargo fission test --project-dir .` for the app crate's Rust tests. +- Run `fission run --project-dir .` from the project root to launch the desktop app and attach output. +- Run `fission build --project-dir . --release` for a release desktop build. +- Run `fission test --project-dir .` for the app crate's Rust tests. - This target uses the default Vello desktop shell path.