From 8a533f0158a48fab386f8f38624cea8a6b32d0f8 Mon Sep 17 00:00:00 2001 From: Sausage agent Date: Fri, 16 Oct 2026 20:56:59 +0000 Subject: [PATCH] Run without a clone: bare `sausage` creates ./world if needed and runs it - cli: the subcommand is optional; without one, sausage runs ./world and runs init first when ./world is not a world yet (test: cli/tests/cli.rs). - flake: .#loop without a folder runs bare sausage. - README: nix run git+https://gitea.surma.technology/surma/sausage(#loop), clone only for working on Sausage. --- README.md | 28 ++++++++++++++++++++-------- cli/src/main.rs | 16 +++++++++++++--- cli/tests/cli.rs | 41 +++++++++++++++++++++++++++++++++++++++++ flake.nix | 6 ++++-- 4 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 cli/tests/cli.rs diff --git a/README.md b/README.md index 786ea24..eac2984 100644 --- a/README.md +++ b/README.md @@ -11,31 +11,43 @@ change its own code. ## Start You need [Nix](https://nixos.org/download) with flakes, and an API token for -an LLM (OpenAI Responses or Anthropic Messages API). +an LLM (OpenAI Responses or Anthropic Messages API). No clone needed: ```sh -git clone https://gitea.surma.technology/surma/sausage && cd sausage -nix run -- init # creates ./world and builds the agent (a few minutes) -nix run -- run # starts the world and a console +nix run git+https://gitea.surma.technology/surma/sausage +``` + +This creates a world in `./world` the first time (it builds the agent, which +takes a few minutes) and then starts it with a console. Later runs start the +existing world. The two steps also exist on their own: + +```sh +nix run git+https://gitea.surma.technology/surma/sausage -- init # create ./world +nix run git+https://gitea.surma.technology/surma/sausage -- run # start it ``` `init` asks for the API shape (`openai-responses` or `anthropic-messages`), the base URL, the model, and the token. It reads `SAUSAGE_API`, `SAUSAGE_BASE_URL`, `SAUSAGE_MODEL`, and `SAUSAGE_API_TOKEN` first. The -values go to `world/actor/agent/data/config.json`. Both commands take another -folder as an argument, for example `nix run -- run my-world`. +values go to `world/actor/agent/data/config.json`. `init` and `run` take +another folder as an argument, for example `-- run my-world`. To keep a world on the newest version, run it in a loop: ```sh -nix run .#loop -- my-world +nix run git+https://gitea.surma.technology/surma/sausage#loop # ./world +nix run git+https://gitea.surma.technology/surma/sausage#loop -- my-world # an existing world ``` -The loop runs `nix run --refresh git+https://gitea.surma.technology/surma/sausage -- run my-world`. +The loop runs the newest `sausage` from that URL (`nix run --refresh`). When the host crashes or an actor sends `@broker {"type":"exit"}`, the loop fetches the newest commit and starts it again. `/quit` ends the loop. Set `SAUSAGE_FLAKE` to run another flake URL instead, for example your fork. +To work on Sausage itself, clone it +(`git clone https://gitea.surma.technology/surma/sausage`) and use `nix run`, +`nix run .#loop` and `nix develop` in the checkout. + In the console: ```text diff --git a/cli/src/main.rs b/cli/src/main.rs index 989e147..48e8cbd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,11 +12,13 @@ use sausage::{Host, cargo_actor, console}; /// The source of the agent actor. `init` builds it like any other actor. static AGENT_SOURCE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../agent"); +// Without a command, `sausage` runs the world in `./world` and creates it +// first (like `init`) if it does not exist yet. #[derive(Parser)] #[command(version, about)] struct Cli { #[command(subcommand)] - command: Cmd, + command: Option, } #[derive(Subcommand)] @@ -112,7 +114,15 @@ async fn run(dir: PathBuf) -> Result<()> { #[tokio::main] async fn main() -> Result<()> { match Cli::parse().command { - Cmd::Init { dir } => init(dir).await, - Cmd::Run { dir } => run(dir).await, + Some(Cmd::Init { dir }) => init(dir).await, + Some(Cmd::Run { dir }) => run(dir).await, + None => { + let dir = PathBuf::from("world"); + if !dir.join("actor").is_dir() { + println!("No world in ./world yet, creating it (same as `sausage init`)."); + init(dir.clone()).await?; + } + run(dir).await + } } } diff --git a/cli/tests/cli.rs b/cli/tests/cli.rs new file mode 100644 index 0000000..1d93cc2 --- /dev/null +++ b/cli/tests/cli.rs @@ -0,0 +1,41 @@ +//! Tests of the `sausage` command line. + +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn empty_dir(label: &str) -> PathBuf { + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("target/test-cli") + .join(format!("{label}-{}-{nonce}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +/// Bare `sausage` (as in `nix run `) without a world starts `init` +/// for ./world instead of printing a usage error. +#[test] +fn bare_command_without_a_world_starts_init() { + let dir = empty_dir("bare"); + let output = Command::new(env!("CARGO_BIN_EXE_sausage")) + .current_dir(&dir) + .env_remove("SAUSAGE_API") + .env_remove("SAUSAGE_BASE_URL") + .env_remove("SAUSAGE_MODEL") + .env_remove("SAUSAGE_API_TOKEN") + .stdin(Stdio::null()) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + // With no answers on stdin, init stops at the first required value. + assert!(!output.status.success(), "stdout:\n{stdout}\nstderr:\n{stderr}"); + assert!(stdout.contains("No world in ./world yet"), "stdout:\n{stdout}\nstderr:\n{stderr}"); + assert!(stderr.contains("SAUSAGE_BASE_URL"), "stdout:\n{stdout}\nstderr:\n{stderr}"); + assert!(!stderr.contains("Usage:"), "stderr:\n{stderr}"); + // It asks before it creates anything. + assert!(!dir.join("world").exists()); +} diff --git a/flake.nix b/flake.nix index 6fa8678..fe3a160 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,7 @@ meta.mainProgram = "sausage"; }; # `nix run .#loop -- ` runs the newest `sausage run ` + # (without a folder: bare `sausage`, i.e. ./world, created if needed) # from the public repo, again and again: after a crash or a broker # `exit` request it refreshes the flake and starts the host again, # so the world updates itself. `/quit` (exit code 0) ends the loop. @@ -44,9 +45,10 @@ repoUrl = "git+https://gitea.surma.technology/surma/sausage"; loop = pkgs.writeShellScriptBin "sausage-loop" '' url="''${SAUSAGE_FLAKE:-${repoUrl}}" - if [ $# -eq 0 ]; then set -- world; fi + # No folder: bare `sausage` (creates ./world first if needed). + if [ $# -gt 0 ]; then set -- run "$@"; fi while true; do - nix run --refresh "$url#default" -- run "$@" + nix run --refresh "$url#default" -- "$@" code=$? if [ "$code" -eq 0 ]; then exit 0; fi echo "sausage-loop: sausage exited with $code, starting the newest version in 2 s …" >&2