feat(maestro): add a process-compose dev stack (#52)
This commit was merged in pull request #52.
This commit is contained in:
@@ -9,3 +9,9 @@ worktrees/
|
||||
# nix build output symlinks
|
||||
/result
|
||||
/result-*
|
||||
|
||||
# direnv (`use flake`)
|
||||
/.direnv/
|
||||
|
||||
# `nix run .#dev` scratch state (tmux socket)
|
||||
/.dev/
|
||||
|
||||
@@ -56,12 +56,19 @@ crates/
|
||||
├── maestro-installer/ # `maestro-install` wizard: cargo install + systemd (NAP-225)
|
||||
└── maestro-e2e/ # cross-process integration tests (NAP-217)
|
||||
web/ # browser SPA (NAP-218)
|
||||
flake.nix # Nix flake: `maestro`, `maestro-web`, `webFrontend`
|
||||
flake.nix # Nix flake: packages, checks, dev shell, `nix run .#dev`
|
||||
nix/ # Home Manager module (`services.maestro`)
|
||||
```
|
||||
|
||||
See `docs/INSTALL.md` for end-user install instructions.
|
||||
|
||||
`nix run .#dev` (or `just dev`) brings up the whole local stack under
|
||||
process-compose: a debug daemon on 127.0.0.1:7071, then Vite on :5173 once the
|
||||
daemon answers `/api/info`. The SPA proxies `/api/*` back to the daemon, so only
|
||||
the UI is exposed on the network. The dev port is deliberately not the daemon's
|
||||
own default 7070 (`devDaemonPort` in `flake.nix`), so the stack never fights a
|
||||
real daemon on the same host.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **TDD by default.** Failing test first, then minimal impl. Commit test + impl together.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Common dev / build entry points. Run `just` with no args to list.
|
||||
|
||||
# Run the full local stack — daemon on 127.0.0.1:7071 plus Vite on :5173,
|
||||
# ordered and health-gated by process-compose. Ctrl-C stops everything.
|
||||
dev:
|
||||
nix run .#dev
|
||||
|
||||
# Build the maestro-web binary together with the SPA bundle it embeds.
|
||||
web:
|
||||
pnpm -C web install --frozen-lockfile
|
||||
|
||||
@@ -76,6 +76,10 @@ impl Tmux {
|
||||
if let Some(d) = &self.tmpdir {
|
||||
c.env("TMUX_TMPDIR", d);
|
||||
}
|
||||
// A daemon started from inside a tmux pane inherits `TMUX`, and tmux
|
||||
// resolves that ahead of `TMUX_TMPDIR` — so without this the daemon
|
||||
// drives the developer's own server and `tmux_tmpdir` does nothing.
|
||||
c.env_remove("TMUX");
|
||||
c
|
||||
}
|
||||
|
||||
@@ -463,6 +467,39 @@ mod tests {
|
||||
assert_eq!(flat[zed - 1], "-e");
|
||||
}
|
||||
|
||||
/// `TMUX` beats `TMUX_TMPDIR` when tmux looks for a server, so leaving an
|
||||
/// inherited one in place would silently ignore the configured tmpdir.
|
||||
#[test]
|
||||
fn cmd_clears_an_inherited_tmux_and_sets_the_configured_tmpdir() {
|
||||
let tmux = Tmux::new().with_tmpdir("/run/maestro-dev");
|
||||
let cmd = tmux.cmd();
|
||||
let envs: Vec<_> = cmd.get_envs().collect();
|
||||
|
||||
assert!(
|
||||
envs.contains(&(OsStr::new("TMUX"), None)),
|
||||
"TMUX must be removed, got {envs:?}"
|
||||
);
|
||||
assert!(
|
||||
envs.contains(&(
|
||||
OsStr::new("TMUX_TMPDIR"),
|
||||
Some(OsStr::new("/run/maestro-dev"))
|
||||
)),
|
||||
"TMUX_TMPDIR must be set, got {envs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The removal is unconditional: a daemon manages its own server whether or
|
||||
/// not a tmpdir was configured.
|
||||
#[test]
|
||||
fn cmd_clears_an_inherited_tmux_without_a_configured_tmpdir() {
|
||||
let cmd = Tmux::new().cmd();
|
||||
let envs: Vec<_> = cmd.get_envs().collect();
|
||||
assert!(
|
||||
envs.contains(&(OsStr::new("TMUX"), None)),
|
||||
"TMUX must be removed, got {envs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_no_server_error_matches_every_known_phrasing() {
|
||||
for phrase in NO_SERVER_PHRASES {
|
||||
|
||||
Generated
+17
-1
@@ -64,11 +64,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"process-compose-flake": {
|
||||
"locked": {
|
||||
"lastModified": 1782060835,
|
||||
"narHash": "sha256-Q8HH2t1l3MITtWCWY4ytcH5F/Q7aAHo3Iq/QTMAKTe0=",
|
||||
"owner": "Platonic-Systems",
|
||||
"repo": "process-compose-flake",
|
||||
"rev": "464ff6880737f063c3f0d3d2c7781fda9190868f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Platonic-Systems",
|
||||
"repo": "process-compose-flake",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"process-compose-flake": "process-compose-flake"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
process-compose-flake.url = "github:Platonic-Systems/process-compose-flake";
|
||||
};
|
||||
|
||||
outputs =
|
||||
@@ -15,7 +16,10 @@
|
||||
"aarch64-linux"
|
||||
];
|
||||
|
||||
imports = [ ./nix/home-manager-module.nix ];
|
||||
imports = [
|
||||
./nix/home-manager-module.nix
|
||||
inputs.process-compose-flake.flakeModule
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{
|
||||
@@ -51,6 +55,34 @@
|
||||
pkgs.git
|
||||
];
|
||||
|
||||
# The dev daemon's port, shared by its bind address, the readiness
|
||||
# probe, and the Vite proxy so they can never drift apart.
|
||||
devDaemonPort = 7071;
|
||||
|
||||
# Everything `nix run .#dev` needs on PATH. Listed explicitly so the
|
||||
# dev stack runs standalone, not only from inside `nix develop`.
|
||||
devTools = [
|
||||
# The base shell utilities. PATH is set explicitly below rather than
|
||||
# inherited, and node wrapper scripts reach for sed/dirname/uname,
|
||||
# which a NixOS host has nowhere near /usr/bin.
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.gnused
|
||||
pkgs.gnugrep
|
||||
pkgs.findutils
|
||||
pkgs.cargo
|
||||
pkgs.rustc
|
||||
pkgs.nodejs
|
||||
pkgs.pnpm_10
|
||||
# A personal ~/.cargo/config.toml commonly points the linker at
|
||||
# clang+mold. Cargo reads it regardless of where the toolchain came
|
||||
# from, so ship both rather than fail on a machine that has such a
|
||||
# config and a nix shell that doesn't.
|
||||
pkgs.clang
|
||||
pkgs.mold
|
||||
]
|
||||
++ runtimeDeps;
|
||||
|
||||
maestro = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
@@ -160,6 +192,75 @@
|
||||
);
|
||||
};
|
||||
|
||||
# `nix run .#dev` — the whole local stack under one supervisor:
|
||||
# a debug-build daemon on 127.0.0.1:7071 (`devDaemonPort`, not the
|
||||
# daemon's own default 7070) and Vite on :5173 with the SPA proxying
|
||||
# `/api/*` back to that daemon (see web/vite.config.ts).
|
||||
# Ctrl-C stops both.
|
||||
process-compose.dev = {
|
||||
# No HTTP control server: nothing here drives process-compose
|
||||
# remotely, and its default :8080 collides with whatever else the
|
||||
# developer happens to be running.
|
||||
cli = {
|
||||
environment.PC_NO_SERVER = true;
|
||||
# The tmux socket the dev daemon uses (see the daemon process
|
||||
# below) lives here.
|
||||
preHook = ''mkdir -p "$PWD/.dev/tmux"'';
|
||||
};
|
||||
|
||||
settings = {
|
||||
# `nix run .#dev` works from a bare shell, not only from inside
|
||||
# `nix develop`, so the toolchain is put on PATH here rather than
|
||||
# inherited. `/usr/bin:/bin` stays last for anything else a
|
||||
# spawned session reaches for.
|
||||
environment = [
|
||||
"PATH=${lib.makeBinPath devTools}:/usr/bin:/bin"
|
||||
# Not :7070: that is the daemon's own default, and the dev
|
||||
# stack must not fight a real daemon running on the same host.
|
||||
"MAESTRO_DAEMON_BIND_ADDR=127.0.0.1:${toString devDaemonPort}"
|
||||
"MAESTRO_DAEMON_URL=http://127.0.0.1:${toString devDaemonPort}"
|
||||
];
|
||||
|
||||
processes = {
|
||||
# Built once, up front, so the daemon and Vite don't race a
|
||||
# cold `cargo build` and trip their own readiness probes.
|
||||
build.command = "cargo build --bin maestro";
|
||||
|
||||
daemon = {
|
||||
# Both values need a shell: `$PWD` is only known at run time,
|
||||
# and process-compose does not expand anything in
|
||||
# `environment`. A tmux socket of its own keeps dev sessions
|
||||
# out of — and safe from — the tmux server belonging to
|
||||
# whatever daemon the developer actually runs.
|
||||
command = ''
|
||||
MAESTRO_DAEMON_TMUX_TMPDIR="$PWD/.dev/tmux" \
|
||||
RUST_LOG="''${RUST_LOG:-maestro=debug,info}" \
|
||||
target/debug/maestro daemon
|
||||
'';
|
||||
depends_on.build.condition = "process_completed_successfully";
|
||||
readiness_probe = {
|
||||
http_get = {
|
||||
host = "127.0.0.1";
|
||||
port = devDaemonPort;
|
||||
path = "/api/info";
|
||||
};
|
||||
initial_delay_seconds = 1;
|
||||
period_seconds = 1;
|
||||
failure_threshold = 30;
|
||||
};
|
||||
};
|
||||
|
||||
web = {
|
||||
# `--host` binds 0.0.0.0: the daemon stays on loopback and only
|
||||
# the UI is exposed, so the API is reachable solely through
|
||||
# the proxy.
|
||||
command = "pnpm -C web install && pnpm -C web dev --host";
|
||||
depends_on.daemon.condition = "process_healthy";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
inherit (self') checks;
|
||||
packages = [
|
||||
|
||||
Reference in New Issue
Block a user