{"id":49355047,"url":"https://github.com/thatsme/tomato","last_synced_at":"2026-04-27T13:02:08.366Z","repository":{"id":350588899,"uuid":"1207505225","full_name":"thatsme/Tomato","owner":"thatsme","description":"Hierarchical DAG engine for composable NixOS configuration management — visual graph editor, OODN variables, template library, SSH deploy with nixos-rebuild","archived":false,"fork":false,"pushed_at":"2026-04-11T03:00:39.000Z","size":140,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-11T05:08:59.355Z","etag":null,"topics":["configuration-management","dag","elixir","graph-editor","infrastructure-as-code","liveview","nix","nixos","nixos-rebuild","phoenix"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thatsme.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":"CLA.md"}},"created_at":"2026-04-11T02:53:32.000Z","updated_at":"2026-04-11T03:03:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thatsme/Tomato","commit_stats":null,"previous_names":["thatsme/tomato"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/thatsme/Tomato","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thatsme%2FTomato","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thatsme%2FTomato/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thatsme%2FTomato/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thatsme%2FTomato/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thatsme","download_url":"https://codeload.github.com/thatsme/Tomato/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thatsme%2FTomato/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32337274,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"online","status_checked_at":"2026-04-27T02:00:06.769Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["configuration-management","dag","elixir","graph-editor","infrastructure-as-code","liveview","nix","nixos","nixos-rebuild","phoenix"],"created_at":"2026-04-27T13:02:04.741Z","updated_at":"2026-04-27T13:02:08.335Z","avatar_url":"https://github.com/thatsme.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tomato\n\nA hierarchical DAG engine for composable NixOS configuration management.\n\nTomato models system configurations as directed acyclic graphs organized in floors (levels). Each leaf node holds a NixOS configuration fragment. Gateway nodes point to subgraphs on the floor below. Walking the graph top-down in topological order composes a valid `configuration.nix` or `flake.nix` — which can be deployed to a NixOS machine via SSH with a single click.\n\n**OODNs** (Out-Of-DAG Nodes) are global key-value pairs — hostnames, ports, flake inputs — that leaf nodes reference via `${key}` placeholders. The walker interpolates them at generation time, so changing one value updates every node that references it.\n\n![Tomato Graph Editor — multi-machine flake](docs/screenshots/tomato_flake.png)\n\n## Quick Start\n\n```bash\nmix setup\nmix phx.server\n```\n\nOpen [localhost:4001](http://localhost:4001). Three demo graphs load automatically — switch between them via the Graph Manager (click the filename in the sidebar).\n\n### Example 1 — `default.json` (simple traditional NixOS)\n\nA single-machine traditional `configuration.nix` example. Good first walkthrough.\n\n- **Root floor**: Networking → System → Services (gateway), with Firewall in parallel\n- **Services subgraph**: PostgreSQL + Nginx\n- **OODNs**: `hostname`, `timezone`, `locale`, `keymap`, `nginx_port`, `pg_port`\n- **Backend**: Traditional (`configuration.nix`)\n\n### Example 2 — `multi-machine.json` (flake with multiple servers)\n\nA flake-based multi-machine setup showing per-machine OODN override and shared config.\n\n- **Root floor**: shared Firewall + 3 machines\n- **webserver** (NixOS, x86_64-linux): Nginx\n- **dbserver** (NixOS, aarch64-linux): PostgreSQL\n- **laptop** (Home Manager, aarch64-darwin): Git + Zsh\n- **OODNs**: `input_nixpkgs`, `input_home-manager`, `input_home-manager_follows`\n- **Backend**: Flake (`flake.nix` with mixed `nixosConfigurations` + `homeConfigurations`)\n\n### Example 3 — `home-manager.json` (pure Home Manager dotfiles)\n\nA developer dotfiles example — no NixOS server, just user-level configuration.\n\n- **One Home Manager machine**: laptop (aarch64-darwin, user \"alex\")\n- **Inside**: Git, Zsh + Starship, Neovim, Tmux, Alacritty, User Packages\n- **OODNs**: `username`, `git_name`, `git_email`, flake inputs\n- **Backend**: Flake (`flake.nix` with `homeConfigurations.\"alex@laptop\"`)\n\n## What's New in v0.3\n\nv0.3 is an internal-quality release — refactoring, bug fixes, and small correctness features. **No breaking changes**: every v0.2 graph file loads and renders unchanged, all public APIs are preserved spec-for-spec.\n\n| Change | Description |\n|---|---|\n| **Deploy split** | `Tomato.Deploy` shrunk from 320 to ~140 lines; logic moved into `Tomato.Deploy.SSH`, `SFTP`, `Rebuild`, `Diff`, `Config`. Public API preserved; `TomatoWeb.GraphLive` needed no changes. |\n| **SSH key authentication** | Credentials resolve in order: explicit `:identity_file` → `TOMATO_DEPLOY_IDENTITY_FILE` env var → `~/.ssh/id_ed25519` → `~/.ssh/id_rsa` → password fallback. Key auth uses Erlang `:ssh.connect/3` with `user_dir`. |\n| **Store split** | `Tomato.Store` shrunk from 716 to 182 lines — thin GenServer facade over `Store.State`, `Mutations`, `OODN`, `Machine`, `Persistence`, `GraphFiles`. Pure mutation modules are testable without a running GenServer. |\n| **Seeder fix** | Demo graphs seed independently. If any of `default.json`, `multi-machine.json`, `home-manager.json` is missing on startup, the seeder creates just that one — previously a populated `default.json` blocked all seeding. |\n| **Leaf `target` field** | Each leaf declares `target :: :nixos \\| :home_manager \\| :all` (default `:nixos`). The walker filters shared root-level fragments by each machine's type, so `networking.firewall.*` is never spliced into a Home Manager module. |\n| **Per-machine OODN overlay** | Each machine gateway carries an optional `oodn_overrides` map that shadows the global OODN registry inside that machine's subtree. Two machines can now hold different `nginx_port` values without naming hacks. |\n| **Canvas components split** | SVG render components (`graph_node`, `edge_line`, `oodn_node`) and their style helpers moved from `TomatoWeb.GraphLive` into `TomatoWeb.GraphLive.CanvasComponents`. First phase of the LiveView god-module refactor. |\n| **Test coverage** | 69 tests → 114 tests (+45) across walker target filtering, mutations, persistence roundtrips, deploy config resolution, and OODN overlay scenarios. |\n\n\u003e ⚠️ **UI gap — `target` and `oodn_overrides` are data-layer only in v0.3.** Both features work end-to-end in the walker, persist to JSON correctly, and have full test coverage, but there's no visual editor yet. New leaves default to `target: :nixos` and new machines default to `oodn_overrides: %{}`, which is correct for every existing graph — but if you need to mark a leaf as `:home_manager` or attach per-machine overrides, you currently have to edit `priv/graphs/*.json` by hand or use `iex -S mix phx.server` (see the [OODN Variables](#oodn-variables) section for an example). **Sidebar editors for both are queued as the next v0.3 PR.**\n\n## What's New in v0.2\n\n| Feature | Description |\n|---|---|\n| **Flake backend** | Toggle between traditional `configuration.nix` and `flake.nix` output. OODN entries prefixed with `input_` become flake inputs |\n| **Multi-machine** | Each machine is a gateway with metadata. Generate one flake with multiple `nixosConfigurations` entries |\n| **Home Manager** | Machines can be `:nixos` or `:home_manager`. Generates `homeConfigurations` alongside NixOS configs |\n| **Deploy modes** | Switch / Test / Dry Run / Diff / Rollback — all from the UI |\n| **Content preview** | Leaf nodes show the first lines of their Nix content directly on the canvas |\n| **Node search** | Find nodes by name or content across all subgraphs and floors |\n| **Undo / Redo** | 50-snapshot mutation history with sidebar buttons |\n\n## How It Works\n\n### The Graph\n\n```\nFloor 0 (root)\n  Input → Networking → System → Services (gateway) → Output\n          Firewall  ↗\n\nFloor 1 (inside Services)\n  Input → PostgreSQL → Output\n          Nginx      ↗\n```\n\n- **Leaf nodes** hold Nix config fragments (e.g. `services.nginx.enable = true;`). Each leaf declares a `target` field — `:nixos` (default), `:home_manager`, or `:all` — which tells the walker whether to include it when generating each backend-specific machine config\n- **Gateway nodes** contain a subgraph on the floor below — composing complex configs from smaller pieces\n- **Machine nodes** are gateways with metadata (`hostname`, `system`, `state_version`, `type`, `oodn_overrides`). The walker overlays the machine's hardcoded keys and any user-supplied `oodn_overrides` on top of the global OODN when interpolating that machine's subtree\n- **OODN node** (Out-Of-DAG Node) is a canvas singleton holding global key-value pairs (`${hostname}`, `${timezone}`, `input_nixpkgs`, etc.) referenced by leaf nodes via `${key}` placeholders. Per-machine `oodn_overrides` shadow the global OODN for that machine's subtree only\n- **Edges** define dependency order — the walker traverses nodes in topological order\n\n### Generate \u0026 Deploy\n\n1. **Generate** — walks the graph, interpolates OODN variables, wraps fragments in either a NixOS module skeleton (traditional) or a `flake.nix` skeleton with `nixosConfigurations`/`homeConfigurations` → writes `.nix` file to `priv/generated/`\n2. **Deploy modes** — pick from the generated output modal:\n   - **Switch** — `nixos-rebuild switch` (apply + boot menu)\n   - **Test** — `nixos-rebuild test` (apply without boot menu)\n   - **Dry Run** — `nixos-rebuild dry-activate` (show what would change)\n   - **Diff** — fetch current remote config and show line-by-line diff\n   - **Rollback** — revert to the previous NixOS generation\n\nReal services start, stop, and reconfigure on a real NixOS machine. Change `${nginx_port}` from `80` to `8080` in the OODN node → both the firewall rules and Nginx config update in one rebuild.\n\n\u003e **Note on Nix syntax errors.** The walker treats leaf content as opaque strings — it does not parse Nix. A syntax error in a fragment passes through generation silently and only surfaces at `nixos-rebuild` time on the remote machine, after a full SSH+SFTP roundtrip. Local validation (`nix-instantiate --parse` on each fragment before write) is on the v0.3 list.\n\n### Backend Toggle\n\nClick **Traditional / Flake** in the sidebar header to switch output format:\n\n- **Traditional** generates `configuration.nix` with imports, deployed via `nixos-rebuild switch`\n- **Flake** generates `flake.nix` with inputs from `input_*` OODNs, multiple `nixosConfigurations` for multi-machine setups, deployed via `nixos-rebuild switch --flake .#hostname`\n\nFlake inputs and `follows` declarations come from OODNs:\n\n```\ninput_nixpkgs              = github:nixos/nixpkgs?ref=nixos-unstable\ninput_home-manager         = github:nix-community/home-manager\ninput_home-manager_follows = nixpkgs\n```\n\n### Multi-Machine\n\nEach machine is a root-level gateway with metadata. The walker generates one `nixosConfigurations` (or `homeConfigurations`) entry per machine, with per-machine `${hostname}`, `${system_arch}`, `${state_version}`, and `${username}` overrides automatically applied during OODN interpolation.\n\n**Shared root-level fragments**: leaf nodes placed at the root (not inside any machine gateway) get included in machines' configs — useful for firewall rules, common packages, or base hardening applied to every server. Each leaf's `target` field controls which machines receive it:\n\n- `target: :nixos` (default) — included in `nixosConfigurations` entries, excluded from `homeConfigurations`\n- `target: :home_manager` — included in `homeConfigurations`, excluded from NixOS\n- `target: :all` — included in both\n\nSo a `networking.firewall.*` leaf ships to the two NixOS servers but not the Home Manager laptop, while a `programs.direnv.enable` leaf would go to the laptop only.\n\n**Per-machine OODN overrides**: each machine gateway can carry an `oodn_overrides` map that shadows the global OODN registry inside that machine's subtree. Two machines with the same service can have different values (e.g. different `nginx_port` per machine) without naming hacks. The global OODN remains the singleton fallback layer for anything the machine doesn't override.\n\n\u003e ⚠️ **v0.3 data-layer only**: the `target` field and `oodn_overrides` map are fully functional in the walker and persisted to JSON, but there is no sidebar editor for either yet. See [OODN Variables](#oodn-variables) below for how to set non-default values via `iex` or direct JSON editing in the current release.\n\n### Template Library\n\nClick **+ Add Node** to pick from predefined templates:\n\n| Category | Templates |\n|---|---|\n| **Stacks** | Prometheus Stack (5 nodes), Grafana + Prometheus, Web Server Stack |\n| **System** | System Base, Networking, Firewall, Admin User, Console |\n| **Web** | Nginx, Nginx Reverse Proxy, Caddy |\n| **Database** | PostgreSQL, MySQL, Redis |\n| **Services** | OpenSSH, Docker, Tailscale, Fail2ban, Cron Jobs |\n| **Monitoring** | Prometheus, Grafana |\n| **Home Manager** | Git, Zsh, Neovim, Tmux, Starship, Direnv, Alacritty, User Packages |\n| **Packages** | Dev Tools |\n\nStack templates create a **gateway with pre-wired child nodes** — e.g. Prometheus Stack creates Prometheus Base + Node Exporter + Scrape configs + Alert Rules, all connected and ready to deploy.\n\nNixOS merges list and attribute set options automatically — `scrapeConfigs` from multiple nodes get concatenated into one `prometheus.yml`.\n\n### OODN Variables\n\nThe OODN node is a singleton on the canvas holding global key-value pairs:\n\n```\nhostname    = tomato-node\ntimezone    = Europe/Rome\nlocale      = it_IT.UTF-8\nkeymap      = it\nnginx_port  = 80\npg_port     = 5432\ninput_nixpkgs              = github:nixos/nixpkgs?ref=nixos-unstable\ninput_home-manager         = github:nix-community/home-manager\ninput_home-manager_follows = nixpkgs\n```\n\nLeaf nodes reference these with `${key}` syntax. The walker interpolates them at generation time. Change a value once, every referencing node updates. The visible OODN node caps at 6 entries with a `+N more` indicator — double-click to open the full editor.\n\n#### Per-machine OODN overrides (v0.3)\n\nEach machine gateway can carry an `oodn_overrides` map that takes precedence over the global OODN registry when the walker interpolates that machine's subtree. Precedence layering, highest to lowest:\n\n1. `machine.oodn_overrides` — user-supplied, wins over everything\n2. Hardcoded machine keys — `hostname`, `system_arch`, `state_version`, `username`\n3. Global OODN registry — the canvas singleton, everything else falls through to here\n\n\u003e ⚠️ **The global OODN panel on the canvas remains the only visual editor in v0.3.** Per-machine overrides are fully wired through the walker and persisted to JSON, but there's no sidebar editor for them yet. A scoped OODN panel that appears inside each machine's subgraph is queued as the next v0.3 PR.\n\nUntil the UI lands, the two ways to set per-machine overrides are:\n\n**(a) Edit the graph file directly** — open `priv/graphs/\u003cyourgraph\u003e.json`, find the machine gateway node, and add an `oodn_overrides` object to its `machine` map:\n\n```json\n{\n  \"id\": \"node-abc\",\n  \"type\": \"gateway\",\n  \"machine\": {\n    \"hostname\": \"webserver-a\",\n    \"system\": \"x86_64-linux\",\n    \"state_version\": \"24.11\",\n    \"type\": \"nixos\",\n    \"oodn_overrides\": {\n      \"nginx_port\": \"8080\",\n      \"max_clients\": \"200\"\n    }\n  }\n}\n```\n\nRestart the server to reload (or call `Store.load_graph/1` from iex).\n\n**(b) Create the machine from `iex -S mix phx.server`**:\n\n```elixir\ngraph = Tomato.Store.get_graph()\nroot = Tomato.Graph.root_subgraph(graph)\n\nTomato.Store.add_machine(root.id,\n  hostname: \"webserver-a\",\n  system: \"x86_64-linux\",\n  state_version: \"24.11\",\n  type: :nixos,\n  oodn_overrides: %{\"nginx_port\" =\u003e \"8080\"}\n)\n```\n\nEither way, the walker picks them up on the next Generate. Leaves inside the machine that reference `${nginx_port}` resolve against the override; shared root-level leaves still see the global `nginx_port`.\n\n## Canvas Interactions\n\n| Action | Effect |\n|---|---|\n| **Click** | Select node |\n| **Drag** | Move node |\n| **Double-click gateway** | Enter subgraph |\n| **Double-click leaf** | Edit content |\n| **Cmd+click leaf** | Edit content |\n| **Long-press / right-click** | Context menu |\n| **Scroll / two-finger** | Pan canvas |\n| **Pinch / Ctrl+scroll** | Zoom |\n\nContext menu actions: Connect from/to, Duplicate, Rename, Disconnect all, Delete, Reverse edge, Fit to view, Reset zoom.\n\nThe sidebar provides node search, undo/redo, graph manager, backend toggle, generate, and node properties.\n\n## Deploy Configuration\n\nTomato supports both **SSH public-key authentication** (recommended) and legacy password authentication as a fallback. Credentials are resolved in this order, first match wins:\n\n1. Explicit `:identity_file` key in `config/deploy.secret.exs`\n2. `TOMATO_DEPLOY_IDENTITY_FILE` environment variable\n3. Auto-discovered `~/.ssh/id_ed25519`\n4. Auto-discovered `~/.ssh/id_rsa`\n5. Password fallback — `:password` / `TOMATO_DEPLOY_PASSWORD` (logs a warning on every connect)\n\n\u003e **Security notice.** For anything beyond a throw-away lab host, **use SSH key authentication**. The password path logs a Logger warning on every deploy and passes credentials in plaintext to Erlang `:ssh.connect/3`. If you must use password auth, use a dedicated low-privilege deploy user, restrict the target host to your LAN/VPN, and rotate the password on every shared machine.\n\nTo deploy generated configs to a NixOS machine, set your target via environment variables or `config/deploy.secret.exs`:\n\n```bash\nexport TOMATO_DEPLOY_HOST=your-nixos-host\nexport TOMATO_DEPLOY_PORT=22\nexport TOMATO_DEPLOY_USER=root\nexport TOMATO_DEPLOY_IDENTITY_FILE=~/.ssh/id_ed25519   # recommended\n# or — legacy password auth:\n# export TOMATO_DEPLOY_PASSWORD=your-password\n```\n\nOr copy the example file:\n\n```bash\ncp config/deploy.secret.exs.example config/deploy.secret.exs\n```\n\nSee `config/deploy.secret.exs.example` for the format. This file is gitignored.\n\n## Architecture\n\n```\nlib/tomato/\n  node.ex              # Node struct — :input/:output/:leaf/:gateway + target + machine meta\n  edge.ex              # Directed edge between nodes on same floor\n  subgraph.ex          # Self-contained DAG on a floor\n  graph.ex             # Top-level container with subgraphs, OODN registry, backend\n  oodn.ex              # Out-of-DAG key-value pair\n  constraint.ex        # DAG validation — cycles, structure, edges\n  walker.ex            # Topological traversal + OODN interpolation + per-machine overlay + target filter\n  template_library.ex  # Predefined NixOS + Home Manager templates (leaf + gateway stacks)\n  demo.ex              # Seeds default, multi-machine, and home-manager demo graphs\n  backend/\n    flake.ex           # Generates flake.nix with inputs/outputs/nixosConfigurations/homeConfigurations\n  store.ex             # GenServer facade — lifecycle + thin handle_call dispatch\n  store/\n    state.ex           # %State{} struct + history operations (push/undo/redo)\n    mutations.ex       # Pure graph mutations (add/remove/update node, edge, gateway, set_backend)\n    oodn.ex            # Pure OODN mutations (put/remove/update/move)\n    machine.ex         # Pure add/3 for machine gateways (+ oodn_overrides)\n    persistence.ex     # JSON encode/decode + flush_to_disk + peek_graph_name\n    graph_files.ex     # list / load / new / save_as / delete / load_latest_or_create / slugify\n  deploy.ex            # Public deploy API — delegates to the submodules below\n  deploy/\n    config.ex          # merge_config + credential resolution (identity_file \u003e env \u003e ~/.ssh \u003e password)\n    ssh.ex             # connect (key or password auth), disconnect, exec, collect_output\n    sftp.ex            # upload, read_file\n    rebuild.ex         # rebuild_command, apply_config\n    diff.ex            # simple_diff\n\nlib/tomato_web/\n  live/\n    graph_live.ex              # Main LiveView — mount, render, event routing, modals\n    graph_live/\n      canvas_components.ex     # SVG function components — graph_node, edge_line, oodn_node\n                               #   + style helpers (node_color, node_rect_class, has_content?, ...)\n\nassets/js/\n  hooks/graph_canvas.js  # Drag, zoom/pan, long-press context menu, Bezier edges\n```\n\n### Persistence\n\nEach graph is a single JSON file in `priv/graphs/`. The Graph Manager (click filename in sidebar) lets you create, load, save-as, and delete graphs. The JSON file is the source of truth — loaded into memory on startup, flushed on every mutation with 200ms debounce.\n\n### Undo / Redo\n\nThe Store keeps a bounded history of the last 50 graph snapshots. Every mutation (add/remove/update node, edge, OODN, machine, backend toggle) pushes the prior state. OODN position drag is excluded from history to avoid noise.\n\n### DAG Constraints\n\nEnforced on every mutation: no cycles (Kahn's algorithm), single `:input`/`:output` per subgraph, edges same-floor only, gateway-subgraph integrity.\n\n## Development\n\n```bash\nmix deps.get            # install dependencies\nmix compile             # compile\nmix phx.server          # start dev server at localhost:4001\niex -S mix phx.server   # start with interactive shell\nmix test                # run the test suite\nmix format              # format code\n```\n\n## Requirements\n\n- Elixir 1.15+\n- Erlang/OTP 26+\n- **Nix 2.18+ with flakes enabled** on any host that will rebuild generated output. Add `experimental-features = nix-command flakes` to `/etc/nix/nix.conf` (or `~/.config/nix/nix.conf`). Tomato's default flake input pins `nixpkgs` to the `nixos-unstable` channel via `input_nixpkgs = github:nixos/nixpkgs?ref=nixos-unstable` — override the OODN if you want a stable channel.\n\n## Roadmap\n\n**v0.3 — paying down technical debt + small correctness fixes.** Landed in the current `v0.3` branch:\n\n- ✅ `Tomato.Deploy` split into `Deploy.SSH` / `SFTP` / `Rebuild` / `Diff` / `Config`\n- ✅ SSH public-key authentication (with password fallback)\n- ✅ `Tomato.Store` split into `Store.State` / `Mutations` / `OODN` / `Machine` / `Persistence` / `GraphFiles`\n- ✅ Seeder fix — demo graphs seed independently of each other\n- ✅ Leaf `target` field + walker filter for shared multi-machine fragments\n- ✅ Per-machine `oodn_overrides` overlay\n- ✅ `TomatoWeb.GraphLive` phase 4a — canvas SVG components extracted to `GraphLive.CanvasComponents`\n\nStill pending for v0.3:\n\n- ⏳ Sidebar editor for leaf `target` field\n- ⏳ Scoped OODN panel inside each machine subgraph (visual editor for `oodn_overrides`)\n- ⏳ `TomatoWeb.GraphLive` phase 4b — modal components extracted to `GraphLive.ModalComponents`\n- ⏳ `TomatoWeb.GraphLive` phase 4c — handle_event dispatch split into per-domain handler modules\n- ⏳ Local Nix-fragment validation (`nix-instantiate --parse` on each leaf before write)\n- ⏳ Windows dev-server zombie BEAM on restart (install a shutdown signal handler in `Tomato.Application`)\n\nFull plan and scoring in [docs/REFACTOR_v0.3.md](docs/REFACTOR_v0.3.md). The v0.2 plan that shipped the flake backend, multi-machine support, and Home Manager is archived in [docs/ROADMAP_v0.2.md](docs/ROADMAP_v0.2.md).\n\n## License\n\nApache License 2.0 — Copyright 2026 Alessio Battistutta. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthatsme%2Ftomato","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthatsme%2Ftomato","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthatsme%2Ftomato/lists"}