278 lines
10 KiB
Nix
278 lines
10 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;
|
|
|
|
# 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
|
|
// {
|
|
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-D9JdgI92vzAY8en2zGEEPd4hwOLVi3sr1joT4b17Be4=";
|
|
};
|
|
|
|
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
|
|
];
|
|
};
|
|
};
|
|
};
|
|
}
|