This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/flake.nix
T
Miguel PalhasandClaude Opus 5 eaf60d9dac feat(maestro): stamp the daemon with its build commit
`GET /api/info` now carries `build` (commit hash, commit date, dirty flag)
and `instance`, a fresh uuid per daemon process. Together they let a client
name the release it is talking to and, after losing its socket, tell an
upgrade from a plain restart from a network blip.

The stamp comes from `maestro-daemon/build.rs`, which reads the git checkout
it compiles in. The Nix build has no `.git` in the crane-filtered source, so
`flake.nix` passes the flake's own revision and last-modified date through
`MAESTRO_BUILD_*` instead. Neither available leaves the fields empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:40:41 +01:00

298 lines
11 KiB
Nix

{
description = "Maestro — terminal-session orchestrator for one machine";
inputs = {
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 =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-linux"
];
imports = [
./nix/home-manager-module.nix
inputs.process-compose-flake.flakeModule
];
perSystem =
{
lib,
pkgs,
self',
...
}:
let
craneLib = inputs.crane.mkLib pkgs;
version = (lib.importTOML ./Cargo.toml).workspace.package.version;
# Build provenance for `GET /api/info`. The Rust source tree crane
# hands to the compiler has no `.git`, so `maestro-daemon/build.rs`
# cannot ask git itself — the flake's own revision is passed instead.
# A dirty tree has no `rev`, and `dirtyRev` carries a `-dirty` suffix.
rev = inputs.self.rev or inputs.self.dirtyRev or "";
# `lastModifiedDate` is `YYYYMMDDhhmmss` in UTC; clients want RFC 3339.
ld = inputs.self.lastModifiedDate or "";
iso =
if ld == "" then
""
else
"${builtins.substring 0 4 ld}-${builtins.substring 4 2 ld}-${builtins.substring 6 2 ld}"
+ "T${builtins.substring 8 2 ld}:${builtins.substring 10 2 ld}:${builtins.substring 12 2 ld}Z";
buildStamp = {
MAESTRO_BUILD_COMMIT = builtins.substring 0 12 rev;
MAESTRO_BUILD_DATE = iso;
MAESTRO_BUILD_DIRTY = if inputs.self ? rev then "0" else "1";
};
# Cargo source filtering keeps only *.rs, *.toml and Cargo.lock. The
# SPA is built separately (below) and handed to the Rust build as a
# path, so nothing under web/ needs to be in this source tree.
commonArgs = {
src = craneLib.cleanCargoSource ./.;
inherit version;
strictDeps = true;
# rustls everywhere (reqwest, tokio-tungstenite): no openssl, no
# pkg-config, nothing to link against.
buildInputs = [ ];
nativeBuildInputs = [ ];
};
cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { pname = "maestro-workspace"; });
# Runtime dependencies the daemon shells out to. Suffixed onto PATH so
# a user's own tmux/git still wins inside spawned sessions.
runtimeDeps = [
pkgs.tmux
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
// buildStamp
// {
inherit cargoArtifacts;
pname = "maestro";
cargoExtraArgs = "--package maestro";
doCheck = false;
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ pkgs.makeWrapper ];
postInstall = ''
wrapProgram $out/bin/maestro \
--suffix PATH : ${lib.makeBinPath runtimeDeps}
'';
meta = {
description = "Unified maestro binary: CLI + per-host daemon + TUI";
homepage = "https://git.naps.pt/naps62/maestro";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "maestro";
};
}
);
# The SPA, built on its own so the Rust build never needs node or a
# network fetch. `pnpm.fetchDeps` pins the store from pnpm-lock.yaml;
# bump `hash` whenever that lockfile changes (set it to
# lib.fakeHash, build, copy the `got:` value).
webFrontend = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "maestro-web-frontend";
inherit version;
src = ./web;
nativeBuildInputs = [
pkgs.nodejs
# pnpm 10, not the default 11: web/pnpm-lock.yaml is
# lockfileVersion 9.0, and package.json pins pnpm@9.15.4.
pkgs.pnpm_10
(pkgs.pnpmConfigHook.override { pnpm = pkgs.pnpm_10; })
];
pnpmDeps = (pkgs.fetchPnpmDeps.override { pnpm = pkgs.pnpm_10; }) {
inherit (finalAttrs) pname version src;
fetcherVersion = 3;
hash = "sha256-AvIrpXipbcp7hRujh/L4UAc6tAJOxe67P/iji6L/oL0=";
};
buildPhase = ''
runHook preBuild
pnpm build
runHook postBuild
'';
# $out IS the bundle directory — that is what MAESTRO_WEB_DIST and
# `include_dir!` expect, so index.html sits at its root.
installPhase = ''
runHook preInstall
cp -r dist $out
runHook postInstall
'';
meta = {
description = "Maestro browser SPA bundle";
license = lib.licenses.mit;
};
});
maestro-web = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
pname = "maestro-web";
cargoExtraArgs = "--package maestro-web";
doCheck = false;
# Skips pnpm entirely in build.rs and embeds this bundle as-is.
MAESTRO_WEB_DIST = webFrontend;
meta = {
description = "Maestro web UI: serves the SPA and proxies to a co-located daemon";
homepage = "https://git.naps.pt/naps62/maestro";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "maestro-web";
};
}
);
in
{
packages = {
default = maestro;
inherit maestro maestro-web webFrontend;
};
checks = {
inherit maestro maestro-web;
maestro-fmt = craneLib.cargoFmt { inherit (commonArgs) src; };
maestro-clippy = craneLib.cargoClippy (
commonArgs
// {
inherit cargoArtifacts;
pname = "maestro-clippy";
# maestro-web is excluded: clippy on it would run build.rs, and
# a workspace-wide MAESTRO_WEB_DIST would make every other
# crate's cache key depend on the frontend. `checks.maestro-web`
# already compiles it.
cargoClippyExtraArgs = "--workspace --exclude maestro-web --all-targets -- --deny warnings";
}
);
};
# `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 = [
pkgs.rust-analyzer
pkgs.tmux
pkgs.git
pkgs.just
pkgs.nodejs
pkgs.pnpm_10
];
};
};
};
}