From 04f0b1231260fc2fa40dc6d7cfc30053934c18c5 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Tue, 24 Feb 2026 20:26:14 +0100 Subject: [PATCH 01/24] feat: initial port of Nix packaging from happier monorepo Port all Nix expressions (flake, packages, NixOS module, devshell, prisma-engines-prebuilt) into a standalone repo. The happier monorepo source is now a non-flake input so packages build from the upstream source tree. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/nix-build.yml | 77 +++++ flake.lock | 99 ++++++ flake.nix | 66 ++++ nix/modules/devshell.nix | 126 ++++++++ nix/modules/happier-server.nix | 303 ++++++++++++++++++ nix/modules/packages.nix | 385 +++++++++++++++++++++++ nix/packages/prisma-engines-prebuilt.nix | 105 +++++++ nix/scripts/update-prisma-hashes.sh | 91 ++++++ 8 files changed, 1252 insertions(+) create mode 100644 .github/workflows/nix-build.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/modules/devshell.nix create mode 100644 nix/modules/happier-server.nix create mode 100644 nix/modules/packages.nix create mode 100644 nix/packages/prisma-engines-prebuilt.nix create mode 100755 nix/scripts/update-prisma-hashes.sh diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml new file mode 100644 index 0000000..f4fd1be --- /dev/null +++ b/.github/workflows/nix-build.yml @@ -0,0 +1,77 @@ +name: CI — Nix Builds + +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + nix-build-x86_64-linux: + name: Nix Build (x86_64-linux) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Build happier-cli + run: nix build .#packages.x86_64-linux.happier-cli + + - name: Build happier-server + run: nix build .#packages.x86_64-linux.happier-server + + - name: Check formatting + run: nix fmt -- --check + + nix-build-aarch64-darwin: + name: Nix Build (aarch64-darwin) + runs-on: macos-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Build happier-cli + run: nix build .#packages.aarch64-darwin.happier-cli + + - name: Build happier-server + run: nix build .#packages.aarch64-darwin.happier-server + + nix-build-aarch64-linux: + name: Nix Build (aarch64-linux via QEMU) + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU for aarch64 + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static binfmt-support + sudo update-binfmts --enable qemu-aarch64 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + with: + extra-conf: extra-platforms = aarch64-linux + + - name: Build happier-cli + run: nix build .#packages.aarch64-linux.happier-cli --system aarch64-linux + + - name: Build happier-server + run: nix build .#packages.aarch64-linux.happier-server --system aarch64-linux + + - name: Evaluate NixOS module + run: nix eval .#nixosModules.happier-server diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4cd4d4b --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1769996383, + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "happier": { + "flake": false, + "locked": { + "lastModified": 1771832404, + "narHash": "sha256-yW1nyW+xe6LsTHCKq38qbhQdCz8Rvg+kkx+0Ovg/NV4=", + "owner": "happier-dev", + "repo": "happier", + "rev": "23e20d826d30e3532c7472aecb819f6ca1b56191", + "type": "github" + }, + "original": { + "owner": "happier-dev", + "repo": "happier", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771903837, + "narHash": "sha256-sdaqdnsQCv3iifzxwB22tUwN/fSHoN7j2myFW5EIkGk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1769909678, + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "72716169fe93074c333e8d0173151350670b824c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "happier": "happier", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a75dffb --- /dev/null +++ b/flake.nix @@ -0,0 +1,66 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + + devshell = { + url = "github:numtide/devshell"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-parts.url = "github:hercules-ci/flake-parts"; + + # The happier monorepo source (fetched as a plain source tree, not evaluated as a flake) + happier = { + url = "github:happier-dev/happier"; + flake = false; + }; + }; + + outputs = + inputs@{ self, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; + + imports = [ + inputs.devshell.flakeModule + ./nix/modules/devshell.nix + ./nix/modules/packages.nix + ]; + + flake = { + nixosModules.happier-server = ./nix/modules/happier-server.nix; + nixosModules.default = self.nixosModules.happier-server; + }; + + perSystem = + { + pkgs, + lib, + system, + ... + }: + { + formatter = pkgs.nixfmt-tree; + + _module.args.pkgs = import self.inputs.nixpkgs { + inherit system; + overlays = [ + # Prebuilt Prisma engines — version auto-derived from yarn.lock. + # When @prisma/client is bumped, run: ./nix/scripts/update-prisma-hashes.sh + (final: prev: { + prisma-engines = import ./nix/packages/prisma-engines-prebuilt.nix { + pkgs = final; + lib = final.lib; + yarnLock = "${inputs.happier}/yarn.lock"; + }; + }) + ]; + }; + }; + }; +} diff --git a/nix/modules/devshell.nix b/nix/modules/devshell.nix new file mode 100644 index 0000000..76099a1 --- /dev/null +++ b/nix/modules/devshell.nix @@ -0,0 +1,126 @@ +{ inputs, ... }: + +{ + perSystem = + { + system, + pkgs, + config, + ... + }: + { + devshells = { + default = { + packages = [ + pkgs.nodejs_22 + pkgs.yarn + pkgs.python3 + pkgs.ffmpeg + pkgs.git + pkgs.nixfmt-rfc-style + ]; + + env = [ + { + name = "LANG"; + value = "en_US.UTF-8"; + } + ]; + + commands = [ + { + name = "dev"; + help = "Run a workspace in dev mode: dev "; + command = '' + workspace="''${1:-}" + case "$workspace" in + cli) yarn workspace @happier-dev/cli dev ;; + server) yarn workspace @happier-dev/server dev ;; + app) yarn workspace @happier-dev/ui start ;; + website) yarn workspace @happier-dev/website dev ;; + docs) yarn workspace @happier-dev/docs dev ;; + *) + echo "Usage: dev " + exit 1 + ;; + esac + ''; + } + { + name = "build"; + help = "Build a workspace: build "; + command = '' + workspace="''${1:-all}" + case "$workspace" in + cli) yarn workspace @happier-dev/cli build ;; + server) yarn workspace @happier-dev/server build ;; + app) yarn workspace @happier-dev/ui build ;; + website) yarn workspace @happier-dev/website build ;; + all) yarn workspaces run build ;; + *) + echo "Usage: build " + exit 1 + ;; + esac + ''; + } + { + name = "test"; + help = "Run tests for a workspace: test "; + command = '' + workspace="''${1:-all}" + case "$workspace" in + cli) yarn workspace @happier-dev/cli test ;; + server) yarn workspace @happier-dev/server test ;; + protocol) yarn workspace @happier-dev/protocol test ;; + all) yarn workspaces run test ;; + *) + echo "Usage: test " + exit 1 + ;; + esac + ''; + } + { + name = "format"; + help = "Format code"; + command = '' + yarn workspaces run format 2>/dev/null || true + ''; + } + { + name = "lint"; + help = "Lint code"; + command = '' + yarn workspaces run lint 2>/dev/null || true + ''; + } + { + name = "db"; + help = "Run database commands for happier-server: db "; + command = '' + cmd="''${1:-start}" + case "$cmd" in + start) yarn workspace @happier-dev/server db ;; + migrate) yarn workspace @happier-dev/server prisma migrate dev ;; + seed) yarn workspace @happier-dev/server prisma db seed ;; + studio) yarn workspace @happier-dev/server prisma studio ;; + *) + echo "Usage: db " + exit 1 + ;; + esac + ''; + } + { + name = "nix-fmt"; + help = "Format Nix files"; + command = '' + find . -name "*.nix" -type f -print0 | xargs -0 nixfmt + ''; + } + ]; + }; + }; + }; +} diff --git a/nix/modules/happier-server.nix b/nix/modules/happier-server.nix new file mode 100644 index 0000000..5bcd2eb --- /dev/null +++ b/nix/modules/happier-server.nix @@ -0,0 +1,303 @@ +# Happier Server NixOS module +# Provides happier-server as a native systemd service with PostgreSQL, Redis, and MinIO +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.happier-server; + isFullMode = cfg.mode == "full"; +in +{ + options.services.happier-server = { + enable = lib.mkEnableOption "Happier Server"; + + package = lib.mkOption { + type = lib.types.package; + description = "The happier-server package to use"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3005; + description = "Port to listen on"; + }; + + mode = lib.mkOption { + type = lib.types.enum [ + "full" + "light" + ]; + default = "full"; + description = '' + Server mode. + - "full": PostgreSQL + Redis + MinIO (production stack) + - "light": SQLite-only, no external service dependencies + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Environment file with secrets in KEY=value format. + Should contain at minimum HANDY_MASTER_SECRET for encryption. + ''; + }; + + database = { + name = lib.mkOption { + type = lib.types.str; + default = "happier"; + description = "PostgreSQL database name (full mode only)"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "happier"; + description = "PostgreSQL user (full mode only)"; + }; + + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create database and user locally (full mode only)"; + }; + }; + + redis = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create Redis instance locally (full mode only)"; + }; + }; + + minio = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Create MinIO instance locally for S3-compatible storage (full mode only)"; + }; + + bucket = lib.mkOption { + type = lib.types.str; + default = "happier"; + description = "MinIO bucket name"; + }; + + rootCredentialsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to file containing MINIO_ROOT_USER and MINIO_ROOT_PASSWORD"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + # PostgreSQL configuration (full mode only) + services.postgresql = lib.mkIf (isFullMode && cfg.database.createLocally) { + enable = true; + package = pkgs.postgresql_15; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensureDBOwnership = true; + } + ]; + authentication = lib.mkForce '' + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + ''; + }; + + # Redis configuration (full mode only) + services.redis.servers.happier = lib.mkIf (isFullMode && cfg.redis.createLocally) { + enable = true; + port = 6379; + bind = "127.0.0.1"; + }; + + # MinIO configuration for S3-compatible storage (full mode only) + services.minio = lib.mkIf (isFullMode && cfg.minio.createLocally) { + enable = true; + listenAddress = "127.0.0.1:9000"; + consoleAddress = "127.0.0.1:9001"; + dataDir = [ "/var/lib/minio/data" ]; + inherit (cfg.minio) rootCredentialsFile; + }; + + # Create MinIO bucket after service starts (full mode only) + systemd.services.minio-bucket-init = lib.mkIf (isFullMode && cfg.minio.createLocally) { + description = "Create MinIO bucket for happier-server"; + wantedBy = [ "multi-user.target" ]; + after = [ "minio.service" ]; + requires = [ "minio.service" ]; + path = [ + pkgs.minio-client + pkgs.getent + ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + EnvironmentFile = cfg.minio.rootCredentialsFile; + }; + script = '' + # Wait for MinIO to be ready + for i in $(seq 1 30); do + mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && break + sleep 1 + done + + # Create bucket if it doesn't exist + mc mb --ignore-existing local/${cfg.minio.bucket} + ''; + }; + + # Run database migrations before server starts + systemd.services.happier-server-migrate = { + description = "Run happier-server database migrations"; + wantedBy = [ "happier-server.service" ]; + before = [ "happier-server.service" ]; + after = lib.optional (isFullMode && cfg.database.createLocally) "postgresql.service"; + requires = lib.optional (isFullMode && cfg.database.createLocally) "postgresql.service"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + # Share the same user and state directory as happier-server + # so migrations write to the same database the server reads + DynamicUser = true; + StateDirectory = "happier-server"; + } + // ( + if isFullMode then + { + ExecStart = "${cfg.package}/bin/happier-server-migrate"; + Environment = [ + "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}" + "NODE_ENV=production" + "HOME=%S/happier-server" + ]; + } + else + { + ExecStart = "${cfg.package}/bin/happier-server-migrate-light"; + Environment = [ + "NODE_ENV=production" + "HOME=%S/happier-server" + ]; + } + ); + }; + + # Enable WAL mode on the SQLite database (light mode only). + # WAL allows concurrent readers + one writer, eliminating the lock contention + # that causes "Socket timeout" and "Transaction already closed" Prisma errors. + systemd.services.happier-server-sqlite-wal = lib.mkIf (!isFullMode) { + description = "Enable WAL mode on happier-server SQLite database"; + wantedBy = [ "happier-server.service" ]; + before = [ "happier-server.service" ]; + after = [ "happier-server-migrate.service" ]; + requires = [ "happier-server-migrate.service" ]; + path = [ pkgs.sqlite ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + DynamicUser = true; + StateDirectory = "happier-server"; + }; + script = '' + DB="%S/happier-server/.happy/server-light/happier-server-light.sqlite" + if [ -f "$DB" ]; then + sqlite3 "$DB" "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;" + fi + ''; + }; + + # Main happier-server service + systemd.services.happier-server = { + description = "Happier Server"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + "happier-server-migrate.service" + ] + ++ lib.optional (!isFullMode) "happier-server-sqlite-wal.service" + ++ lib.optionals isFullMode ( + lib.optional cfg.database.createLocally "postgresql.service" + ++ lib.optional cfg.redis.createLocally "redis-happier.service" + ++ lib.optional cfg.minio.createLocally "minio-bucket-init.service" + ); + requires = [ + "happier-server-migrate.service" + ] + ++ lib.optional (!isFullMode) "happier-server-sqlite-wal.service" + ++ lib.optionals isFullMode ( + lib.optional cfg.database.createLocally "postgresql.service" + ++ lib.optional cfg.redis.createLocally "redis-happier.service" + ++ lib.optional cfg.minio.createLocally "minio.service" + ); + script = '' + ${lib.optionalString (cfg.environmentFile != null) '' + set -a + source "$CREDENTIALS_DIRECTORY/happier-env" + set +a + ''} + ${lib.optionalString + (isFullMode && cfg.minio.createLocally && cfg.minio.rootCredentialsFile != null) + '' + source "$CREDENTIALS_DIRECTORY/minio-creds" + export S3_ACCESS_KEY="$MINIO_ROOT_USER" + export S3_SECRET_KEY="$MINIO_ROOT_PASSWORD" + '' + } + exec ${cfg.package}/bin/${if isFullMode then "happier-server" else "happier-server-light"} + ''; + serviceConfig = { + Type = "simple"; + Restart = "on-failure"; + RestartSec = 5; + + Environment = [ + "NODE_ENV=production" + "PORT=${toString cfg.port}" + "HOME=%S/happier-server" + ] + ++ lib.optionals isFullMode ( + [ + "DATABASE_URL=postgresql://${cfg.database.user}@localhost/${cfg.database.name}" + ] + ++ lib.optional cfg.redis.createLocally "REDIS_URL=redis://localhost:6379" + ++ lib.optionals cfg.minio.createLocally [ + "S3_HOST=127.0.0.1" + "S3_PORT=9000" + "S3_USE_SSL=false" + "S3_BUCKET=${cfg.minio.bucket}" + "S3_PUBLIC_URL=http://127.0.0.1:9000/${cfg.minio.bucket}" + ] + ); + + # Use LoadCredential to make secret files readable by DynamicUser. + # systemd copies them to a private $CREDENTIALS_DIRECTORY. + LoadCredential = + lib.optional (cfg.environmentFile != null) "happier-env:${cfg.environmentFile}" + ++ lib.optional ( + isFullMode && cfg.minio.createLocally && cfg.minio.rootCredentialsFile != null + ) "minio-creds:${cfg.minio.rootCredentialsFile}"; + + # Hardening + DynamicUser = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + StateDirectory = "happier-server"; + WorkingDirectory = "${cfg.package}/lib/happier-server/apps/server"; + }; + }; + }; +} diff --git a/nix/modules/packages.nix b/nix/modules/packages.nix new file mode 100644 index 0000000..cf7f335 --- /dev/null +++ b/nix/modules/packages.nix @@ -0,0 +1,385 @@ +# Nix packages for happier-cli and happier-server +{ inputs, ... }: + +{ + perSystem = + { + system, + pkgs, + lib, + ... + }: + let + happierSrc = inputs.happier; + + # Source filter: exclude packages/dirs not needed for building CLI or server + filteredSrc = lib.cleanSourceWith { + src = happierSrc; + filter = + path: type: + let + relPath = lib.removePrefix (toString happierSrc + "/") (toString path); + in + !( + lib.hasPrefix "apps/ui" relPath + || lib.hasPrefix "apps/stack" relPath + || lib.hasPrefix "apps/website" relPath + || lib.hasPrefix "apps/docs" relPath + || lib.hasPrefix "packages/audio-stream-native" relPath + || lib.hasPrefix "packages/sherpa-native" relPath + || lib.hasPrefix "packages/relay-server" relPath + || lib.hasPrefix "packages/tests" relPath + || lib.hasPrefix ".git" relPath + || relPath == "node_modules" + || lib.hasPrefix "node_modules/" relPath + || relPath == "dist" + || lib.hasPrefix ".pgdata" relPath + || lib.hasPrefix ".minio" relPath + || lib.hasPrefix ".logs" relPath + || lib.hasPrefix "result" relPath + || lib.hasPrefix ".project" relPath + ); + }; + + # Offline yarn cache from the root yarn.lock + yarnOfflineCache = pkgs.fetchYarnDeps { + yarnLock = "${happierSrc}/yarn.lock"; + hash = "sha256-5SeMv0NQ0KbfHsSSO9k/jFhYxw77I1sBn0AxxQVpMjc="; + }; + in + { + packages = { + # -- happier-cli (CLI) ------------------------------------------------- + happier-cli = pkgs.stdenv.mkDerivation { + pname = "happier-cli"; + version = "0.1.0"; + + src = filteredSrc; + + nativeBuildInputs = with pkgs; [ + nodejs_22 + yarn + yarnConfigHook + makeWrapper + python3 + ]; + + inherit yarnOfflineCache; + + preConfigure = '' + # Skip server postinstall (only need CLI scope) + export HAPPIER_INSTALL_SCOPE=cli + export HOME=$(mktemp -d) + ''; + + buildPhase = '' + runHook preBuild + + # Build shared workspace packages in dependency order: + # protocol (no deps) -> agents (needs protocol) -> cli-common (needs agents) -> release-runtime (no internal deps) + # Protocol needs its codegen step first + node packages/protocol/scripts/generate-embedded-feature-policies.mjs + node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/cli-common/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/release-runtime/tsconfig.json + + # Sync bundled workspace dist into CLI's node_modules so tsc/pkgroll can resolve them + node -e " + const { syncBundledWorkspaceDist } = await import('./apps/cli/scripts/buildSharedDeps.mjs'); + syncBundledWorkspaceDist({ repoRoot: process.cwd() }); + " + + # Build the CLI: clean dist, typecheck, then bundle with pkgroll + # Using subshells to avoid cd state leaking on errors + node apps/cli/scripts/rmDist.mjs + (cd apps/cli && node ../../node_modules/typescript/bin/tsc --noEmit) + (cd apps/cli && node ../../node_modules/.bin/pkgroll) + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + # Replicate monorepo layout so Node.js module resolution works + mkdir -p $out/lib/happier-cli/apps/cli + mkdir -p $out/lib/happier-cli/packages/protocol + mkdir -p $out/lib/happier-cli/packages/agents + mkdir -p $out/lib/happier-cli/packages/cli-common + mkdir -p $out/lib/happier-cli/packages/release-runtime + + # Root node_modules (hoisted dependencies) + cp -r node_modules $out/lib/happier-cli/ + + # Remove broken symlinks (workspace cross-references we don't ship) + find $out/lib/happier-cli/node_modules -xtype l -delete + + # -- apps/cli artifacts -- + cp -r apps/cli/dist $out/lib/happier-cli/apps/cli/ + cp -r apps/cli/bin $out/lib/happier-cli/apps/cli/ + cp -r apps/cli/scripts $out/lib/happier-cli/apps/cli/ + cp apps/cli/package.json $out/lib/happier-cli/apps/cli/ + if [ -d apps/cli/node_modules ]; then + cp -r apps/cli/node_modules $out/lib/happier-cli/apps/cli/ + fi + + # -- packages/protocol -- + cp -r packages/protocol/dist $out/lib/happier-cli/packages/protocol/ + cp packages/protocol/package.json $out/lib/happier-cli/packages/protocol/ + if [ -d packages/protocol/node_modules ]; then + cp -r packages/protocol/node_modules $out/lib/happier-cli/packages/protocol/ + fi + + # -- packages/agents -- + cp -r packages/agents/dist $out/lib/happier-cli/packages/agents/ + cp packages/agents/package.json $out/lib/happier-cli/packages/agents/ + if [ -d packages/agents/node_modules ]; then + cp -r packages/agents/node_modules $out/lib/happier-cli/packages/agents/ + fi + + # -- packages/cli-common -- + cp -r packages/cli-common/dist $out/lib/happier-cli/packages/cli-common/ + cp packages/cli-common/package.json $out/lib/happier-cli/packages/cli-common/ + if [ -d packages/cli-common/node_modules ]; then + cp -r packages/cli-common/node_modules $out/lib/happier-cli/packages/cli-common/ + fi + + # -- packages/release-runtime -- + cp -r packages/release-runtime/dist $out/lib/happier-cli/packages/release-runtime/ + cp packages/release-runtime/package.json $out/lib/happier-cli/packages/release-runtime/ + if [ -d packages/release-runtime/node_modules ]; then + cp -r packages/release-runtime/node_modules $out/lib/happier-cli/packages/release-runtime/ + fi + + # Create wrapper scripts + mkdir -p $out/bin + + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier \ + --add-flags "--no-warnings" \ + --add-flags "--no-deprecation" \ + --add-flags "$out/lib/happier-cli/apps/cli/dist/index.mjs" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.difftastic + pkgs.ripgrep + ] + } + + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier-mcp \ + --add-flags "--no-warnings" \ + --add-flags "--no-deprecation" \ + --add-flags "$out/lib/happier-cli/apps/cli/dist/backends/codex/happyMcpStdioBridge.mjs" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.difftastic + pkgs.ripgrep + ] + } + + runHook postInstall + ''; + + meta = { + description = "Happier CLI - mobile and web client for Claude Code"; + homepage = "https://github.com/happier-dev/happier"; + license = lib.licenses.mit; + mainProgram = "happier"; + }; + }; + + # -- happier-server ---------------------------------------------------- + happier-server = pkgs.stdenv.mkDerivation { + pname = "happier-server"; + version = "0.1.2"; + + src = filteredSrc; + + nativeBuildInputs = with pkgs; [ + nodejs_22 + yarn + yarnConfigHook + makeWrapper + python3 + ]; + + buildInputs = with pkgs; [ + prisma-engines + # sharp bundles its own libvips via @img/sharp-* prebuilts — no system vips needed + ]; + + inherit yarnOfflineCache; + + preConfigure = '' + # Skip CLI postinstall (only need server scope) + export HAPPIER_INSTALL_SCOPE=server + export HOME=$(mktemp -d) + + # Point Prisma at nixpkgs engines + export PRISMA_QUERY_ENGINE_LIBRARY="${pkgs.prisma-engines}/lib/libquery_engine.node" + export PRISMA_SCHEMA_ENGINE_BINARY="${pkgs.prisma-engines}/bin/schema-engine" + export PRISMA_SKIP_POSTINSTALL_GENERATE=true + ''; + + buildPhase = '' + runHook preBuild + + # Build shared workspace packages in dependency order: + # protocol (no deps) -> agents (needs protocol) + node packages/protocol/scripts/generate-embedded-feature-policies.mjs + node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json + + # Generate Prisma clients for all providers (postgres, mysql, sqlite) + # generate:providers handles schema:sync internally and generates all three + yarn workspace @happier-dev/server generate:providers + + # Typecheck directly to avoid prebuild re-running buildSharedDeps. + # Note: prisma-json-types-generator patches @prisma/client types in-place; + # if the patch silently fails in the sandbox, PrismaJson types won't resolve. + # The server runs via tsx at runtime so this is a validation-only step. + (cd apps/server && node ../../node_modules/typescript/bin/tsc --noEmit) || echo "WARN: tsc --noEmit had errors (non-fatal for tsx runtime)" + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + # Replicate monorepo layout so Node.js module resolution works + mkdir -p $out/lib/happier-server/apps/server + mkdir -p $out/lib/happier-server/packages/protocol + mkdir -p $out/lib/happier-server/packages/agents + + # Root node_modules (hoisted dependencies) + cp -r node_modules $out/lib/happier-server/ + + # Remove broken symlinks (workspace cross-references we don't ship) + find $out/lib/happier-server/node_modules -xtype l -delete + + # -- packages/protocol -- + cp -r packages/protocol/dist $out/lib/happier-server/packages/protocol/ + cp packages/protocol/package.json $out/lib/happier-server/packages/protocol/ + if [ -d packages/protocol/node_modules ]; then + cp -r packages/protocol/node_modules $out/lib/happier-server/packages/protocol/ + fi + + # -- packages/agents -- + cp -r packages/agents/dist $out/lib/happier-server/packages/agents/ + cp packages/agents/package.json $out/lib/happier-server/packages/agents/ + if [ -d packages/agents/node_modules ]; then + cp -r packages/agents/node_modules $out/lib/happier-server/packages/agents/ + fi + + # -- apps/server sources and config -- + cp -r apps/server/sources $out/lib/happier-server/apps/server/ + cp -r apps/server/prisma $out/lib/happier-server/apps/server/ + cp -r apps/server/scripts $out/lib/happier-server/apps/server/ + cp apps/server/tsconfig.json $out/lib/happier-server/apps/server/ + cp apps/server/package.json $out/lib/happier-server/apps/server/ + + # Generated Prisma clients for sqlite and mysql (relative to apps/server/) + if [ -d apps/server/generated ]; then + cp -r apps/server/generated $out/lib/happier-server/apps/server/ + fi + + # Workspace node_modules (including generated Prisma client) + if [ -d apps/server/node_modules ]; then + cp -r apps/server/node_modules $out/lib/happier-server/apps/server/ + fi + + # Generated Prisma client (.prisma at root) — dereference symlinks + # since engine binaries are nix store paths (read-only in the store) + if [ -d node_modules/.prisma ]; then + rm -rf $out/lib/happier-server/node_modules/.prisma + cp -rL node_modules/.prisma $out/lib/happier-server/node_modules/ + fi + + # Create wrapper scripts + mkdir -p $out/bin + + # Main server binary (full mode): run via tsx + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier-server \ + --add-flags "--import" \ + --add-flags "tsx" \ + --add-flags "$out/lib/happier-server/apps/server/sources/main.ts" \ + --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ + --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ + --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ + --chdir "$out/lib/happier-server/apps/server" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.ffmpeg + pkgs.python3 + ] + } + + # Light/SQLite server binary + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier-server-light \ + --add-flags "--import" \ + --add-flags "tsx" \ + --add-flags "$out/lib/happier-server/apps/server/sources/main.light.ts" \ + --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ + --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ + --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ + --chdir "$out/lib/happier-server/apps/server" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.ffmpeg + pkgs.python3 + ] + } + + # Migration binary (full mode — Prisma migrate deploy) + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier-server-migrate \ + --add-flags "$out/lib/happier-server/node_modules/.bin/prisma" \ + --add-flags "migrate" \ + --add-flags "deploy" \ + --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ + --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ + --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ + --chdir "$out/lib/happier-server/apps/server" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.yarn + ] + } + + # Light migration binary (SQLite deploy script) + makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/happier-server-migrate-light \ + --add-flags "--import" \ + --add-flags "tsx" \ + --add-flags "$out/lib/happier-server/apps/server/scripts/migrate.sqlite.deploy.ts" \ + --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ + --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ + --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ + --chdir "$out/lib/happier-server/apps/server" \ + --prefix PATH : ${ + lib.makeBinPath [ + pkgs.nodejs_22 + pkgs.yarn + ] + } + + runHook postInstall + ''; + + meta = { + description = "Happier Server - backend for Happier mobile and CLI clients"; + homepage = "https://github.com/happier-dev/happier"; + license = lib.licenses.mit; + mainProgram = "happier-server"; + }; + }; + }; + }; +} diff --git a/nix/packages/prisma-engines-prebuilt.nix b/nix/packages/prisma-engines-prebuilt.nix new file mode 100644 index 0000000..9ad5e15 --- /dev/null +++ b/nix/packages/prisma-engines-prebuilt.nix @@ -0,0 +1,105 @@ +# Fetch prebuilt Prisma engine binaries instead of compiling from source. +# +# The engine commit hash is auto-derived from @prisma/engines-version in yarn.lock. +# Only the binary download hashes below need manual updating when Prisma is bumped. +# +# When @prisma/client is bumped and yarn.lock changes: +# 1. nix build will fail with a hash mismatch (the engine hash changed → new URL) +# 2. Run: ./nix/scripts/update-prisma-hashes.sh +# 3. Commit the updated hashes +{ + pkgs, + lib, + yarnLock, +}: +let + # Auto-derive engine commit hash and version from yarn.lock + yarnLockContent = builtins.readFile yarnLock; + lines = builtins.filter builtins.isString (builtins.split "\n" yarnLockContent); + + engineVersionLines = builtins.filter ( + l: builtins.match ".*@prisma/engines-version.*" l != null + ) lines; + engineHash = builtins.elemAt (builtins.match ".*\\.([a-f0-9]+).*" (builtins.head engineVersionLines)) 0; + + prismaVersionLines = builtins.filter (l: builtins.match "\"@prisma/engines@.*" l != null) lines; + version = builtins.elemAt (builtins.match "\"@prisma/engines@([^\"]+)\".*" (builtins.head prismaVersionLines)) 0; + + baseUrl = "https://binaries.prisma.sh/all_commits/${engineHash}"; + + # Platform-specific binary config + # Update these hashes after bumping Prisma: ./nix/update-prisma-hashes.sh + platformConfig = { + "aarch64-linux" = { + platform = "linux-arm64-openssl-3.0.x"; + queryEngineFile = "libquery_engine.so.node.gz"; + schemaEngineFile = "schema-engine.gz"; + queryEngineHash = "1bkp5a5m8jmq2l3slc4lfaaji1z54zc7rg65rv9jyh6pz94mqv7l"; + schemaEngineHash = "09pxr9djichrpi9dxmr4q02l7qayl0cbx274zak66vda97g546rg"; + }; + "aarch64-darwin" = { + platform = "darwin-arm64"; + queryEngineFile = "libquery_engine.dylib.node.gz"; + schemaEngineFile = "schema-engine.gz"; + queryEngineHash = "0kl0g4y84qy2krlh4djr1i9cjzkxv9aqmf8m1x5knb31n4fba544"; + schemaEngineHash = "0wypyw9djpqwizk90f2xlj458p8ywcgah8kqpx2y251jv00bcld9"; + }; + "x86_64-linux" = { + platform = "debian-openssl-3.0.x"; + queryEngineFile = "libquery_engine.so.node.gz"; + schemaEngineFile = "schema-engine.gz"; + queryEngineHash = "046nqra0rvdiazmnphyxa6yzpjsg1w0dqjdjxg310wx1r0n8g06k"; + schemaEngineHash = "12ixm3mhrr6advyb800cklybvqa744av68gxi2q8g12k6kzgs7bc"; + }; + }; + + cfg = + platformConfig.${pkgs.system} + or (throw "prisma-engines-prebuilt: unsupported system ${pkgs.system}"); + + queryEngineSrc = pkgs.fetchurl { + url = "${baseUrl}/${cfg.platform}/${cfg.queryEngineFile}"; + sha256 = cfg.queryEngineHash; + }; + + schemaEngineSrc = pkgs.fetchurl { + url = "${baseUrl}/${cfg.platform}/${cfg.schemaEngineFile}"; + sha256 = cfg.schemaEngineHash; + }; +in +pkgs.stdenv.mkDerivation { + pname = "prisma-engines"; + inherit version; + + dontUnpack = true; + + nativeBuildInputs = [ + pkgs.gzip + pkgs.autoPatchelfHook + ]; + + # Runtime libraries needed by the prebuilt binaries + buildInputs = [ + pkgs.openssl + pkgs.stdenv.cc.cc.lib # libstdc++/libgcc + pkgs.zlib + ]; + + installPhase = '' + mkdir -p $out/lib $out/bin + + # Query engine (shared library loaded by @prisma/client) + gzip -dc ${queryEngineSrc} > $out/lib/libquery_engine.node + chmod 755 $out/lib/libquery_engine.node + + # Schema engine (binary used by prisma migrate) + gzip -dc ${schemaEngineSrc} > $out/bin/schema-engine + chmod 755 $out/bin/schema-engine + ''; + + meta = { + description = "Prisma engines (prebuilt binaries)"; + homepage = "https://github.com/prisma/prisma-engines"; + license = lib.licenses.asl20; + }; +} diff --git a/nix/scripts/update-prisma-hashes.sh b/nix/scripts/update-prisma-hashes.sh new file mode 100755 index 0000000..f511fb9 --- /dev/null +++ b/nix/scripts/update-prisma-hashes.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Update prebuilt Prisma engine binary hashes after bumping @prisma/client. +# +# Usage: ./nix/scripts/update-prisma-hashes.sh [path/to/happier/yarn.lock] +# +# If no yarn.lock path is given, the script looks for ../happier/yarn.lock +# (assumes the happier monorepo is a sibling directory). +# +# This reads the engine commit hash from yarn.lock and prefetches +# binaries for all supported platforms, then updates the hashes +# in nix/packages/prisma-engines-prebuilt.nix. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" +NIX_FILE="$SCRIPT_DIR/../packages/prisma-engines-prebuilt.nix" + +YARN_LOCK="${1:-}" +if [ -z "$YARN_LOCK" ]; then + # Default: look for happier monorepo as a sibling directory + YARN_LOCK="$REPO_ROOT/../happier/yarn.lock" +fi + +if [ ! -f "$YARN_LOCK" ]; then + echo "ERROR: yarn.lock not found at $YARN_LOCK" + echo "Usage: $0 [path/to/happier/yarn.lock]" + exit 1 +fi + +echo "Using yarn.lock: $YARN_LOCK" + +# Extract engine hash from yarn.lock +ENGINE_HASH=$(grep '@prisma/engines-version' "$YARN_LOCK" | head -1 | grep -oE '[a-f0-9]{40}') + +if [ -z "$ENGINE_HASH" ]; then + echo "ERROR: Could not find @prisma/engines-version in yarn.lock" + exit 1 +fi + +echo "Engine commit hash: $ENGINE_HASH" + +BASE_URL="https://binaries.prisma.sh/all_commits/$ENGINE_HASH" + +prefetch() { + local url="$1" + echo " Fetching: $url" >&2 + nix-prefetch-url "$url" --type sha256 2>/dev/null +} + +update_hash() { + local old_hash="$1" + local new_hash="$2" + if [ "$old_hash" != "$new_hash" ]; then + tmp="$(mktemp)" + sed "s|$old_hash|$new_hash|g" "$NIX_FILE" > "$tmp" + mv "$tmp" "$NIX_FILE" + echo " Updated: $old_hash -> $new_hash" + else + echo " Unchanged: $old_hash" + fi +} + +echo "" +echo "Prefetching aarch64-linux binaries..." +AARCH64_LINUX_QE=$(prefetch "$BASE_URL/linux-arm64-openssl-3.0.x/libquery_engine.so.node.gz") +AARCH64_LINUX_SE=$(prefetch "$BASE_URL/linux-arm64-openssl-3.0.x/schema-engine.gz") + +echo "Prefetching aarch64-darwin binaries..." +AARCH64_DARWIN_QE=$(prefetch "$BASE_URL/darwin-arm64/libquery_engine.dylib.node.gz") +AARCH64_DARWIN_SE=$(prefetch "$BASE_URL/darwin-arm64/schema-engine.gz") + +echo "Prefetching x86_64-linux binaries..." +X86_64_LINUX_QE=$(prefetch "$BASE_URL/debian-openssl-3.0.x/libquery_engine.so.node.gz") +X86_64_LINUX_SE=$(prefetch "$BASE_URL/debian-openssl-3.0.x/schema-engine.gz") + +echo "" +echo "Updating hashes in $NIX_FILE..." + +# Read current hashes from the nix file (in order of appearance) +CURRENT_HASHES=($(grep -oE 'Hash = "[^"]+"' "$NIX_FILE" | sed 's/.*Hash = "//;s/"//')) + +# Update in order: aarch64-linux QE, SE, aarch64-darwin QE, SE, x86_64-linux QE, SE +NEW_HASHES=("$AARCH64_LINUX_QE" "$AARCH64_LINUX_SE" "$AARCH64_DARWIN_QE" "$AARCH64_DARWIN_SE" "$X86_64_LINUX_QE" "$X86_64_LINUX_SE") + +for i in "${!CURRENT_HASHES[@]}"; do + update_hash "${CURRENT_HASHES[$i]}" "${NEW_HASHES[$i]}" +done + +echo "" +echo "Done! Verify with: nix build .#happier-server" From c10bee4973a834160e820f05e8866401d7f04ffb Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Tue, 24 Feb 2026 20:27:49 +0100 Subject: [PATCH 02/24] refactor: flatten nix/ into top-level directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone Nix repo doesn't need the nix/ prefix — move modules/, packages/, and scripts/ to the repo root. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 10 +++++----- {nix/modules => modules}/devshell.nix | 0 {nix/modules => modules}/happier-server.nix | 0 {nix/modules => modules}/packages.nix | 0 {nix/packages => packages}/prisma-engines-prebuilt.nix | 0 {nix/scripts => scripts}/update-prisma-hashes.sh | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) rename {nix/modules => modules}/devshell.nix (100%) rename {nix/modules => modules}/happier-server.nix (100%) rename {nix/modules => modules}/packages.nix (100%) rename {nix/packages => packages}/prisma-engines-prebuilt.nix (100%) rename {nix/scripts => scripts}/update-prisma-hashes.sh (92%) diff --git a/flake.nix b/flake.nix index a75dffb..8879250 100644 --- a/flake.nix +++ b/flake.nix @@ -28,12 +28,12 @@ imports = [ inputs.devshell.flakeModule - ./nix/modules/devshell.nix - ./nix/modules/packages.nix + ./modules/devshell.nix + ./modules/packages.nix ]; flake = { - nixosModules.happier-server = ./nix/modules/happier-server.nix; + nixosModules.happier-server = ./modules/happier-server.nix; nixosModules.default = self.nixosModules.happier-server; }; @@ -51,9 +51,9 @@ inherit system; overlays = [ # Prebuilt Prisma engines — version auto-derived from yarn.lock. - # When @prisma/client is bumped, run: ./nix/scripts/update-prisma-hashes.sh + # When @prisma/client is bumped, run: ./scripts/update-prisma-hashes.sh (final: prev: { - prisma-engines = import ./nix/packages/prisma-engines-prebuilt.nix { + prisma-engines = import ./packages/prisma-engines-prebuilt.nix { pkgs = final; lib = final.lib; yarnLock = "${inputs.happier}/yarn.lock"; diff --git a/nix/modules/devshell.nix b/modules/devshell.nix similarity index 100% rename from nix/modules/devshell.nix rename to modules/devshell.nix diff --git a/nix/modules/happier-server.nix b/modules/happier-server.nix similarity index 100% rename from nix/modules/happier-server.nix rename to modules/happier-server.nix diff --git a/nix/modules/packages.nix b/modules/packages.nix similarity index 100% rename from nix/modules/packages.nix rename to modules/packages.nix diff --git a/nix/packages/prisma-engines-prebuilt.nix b/packages/prisma-engines-prebuilt.nix similarity index 100% rename from nix/packages/prisma-engines-prebuilt.nix rename to packages/prisma-engines-prebuilt.nix diff --git a/nix/scripts/update-prisma-hashes.sh b/scripts/update-prisma-hashes.sh similarity index 92% rename from nix/scripts/update-prisma-hashes.sh rename to scripts/update-prisma-hashes.sh index f511fb9..47443b1 100755 --- a/nix/scripts/update-prisma-hashes.sh +++ b/scripts/update-prisma-hashes.sh @@ -1,20 +1,20 @@ #!/usr/bin/env bash # Update prebuilt Prisma engine binary hashes after bumping @prisma/client. # -# Usage: ./nix/scripts/update-prisma-hashes.sh [path/to/happier/yarn.lock] +# Usage: ./scripts/update-prisma-hashes.sh [path/to/happier/yarn.lock] # # If no yarn.lock path is given, the script looks for ../happier/yarn.lock # (assumes the happier monorepo is a sibling directory). # # This reads the engine commit hash from yarn.lock and prefetches # binaries for all supported platforms, then updates the hashes -# in nix/packages/prisma-engines-prebuilt.nix. +# in packages/prisma-engines-prebuilt.nix. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" -NIX_FILE="$SCRIPT_DIR/../packages/prisma-engines-prebuilt.nix" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" +NIX_FILE="$REPO_ROOT/packages/prisma-engines-prebuilt.nix" YARN_LOCK="${1:-}" if [ -z "$YARN_LOCK" ]; then From fe98f11d44a727075c0b3092ba27f890fbcf9404 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Wed, 25 Feb 2026 00:32:56 +0100 Subject: [PATCH 03/24] =?UTF-8?q?refactor:=20idiomatic=20layout=20?= =?UTF-8?q?=E2=80=94=20modules/nixos/=20for=20exported=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow Blueprint/sops-nix convention: - modules/nixos/ for exported NixOS modules (future: darwin/, home/) - devshell.nix and packages.nix at root (internal flake-parts wiring) - packages/ for helper derivations (prisma-engines-prebuilt) Co-Authored-By: Claude Opus 4.6 --- modules/devshell.nix => devshell.nix | 0 flake.nix | 6 +++--- modules/{ => nixos}/happier-server.nix | 0 modules/packages.nix => packages.nix | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename modules/devshell.nix => devshell.nix (100%) rename modules/{ => nixos}/happier-server.nix (100%) rename modules/packages.nix => packages.nix (100%) diff --git a/modules/devshell.nix b/devshell.nix similarity index 100% rename from modules/devshell.nix rename to devshell.nix diff --git a/flake.nix b/flake.nix index 8879250..454b4f8 100644 --- a/flake.nix +++ b/flake.nix @@ -28,12 +28,12 @@ imports = [ inputs.devshell.flakeModule - ./modules/devshell.nix - ./modules/packages.nix + ./devshell.nix + ./packages.nix ]; flake = { - nixosModules.happier-server = ./modules/happier-server.nix; + nixosModules.happier-server = ./modules/nixos/happier-server.nix; nixosModules.default = self.nixosModules.happier-server; }; diff --git a/modules/happier-server.nix b/modules/nixos/happier-server.nix similarity index 100% rename from modules/happier-server.nix rename to modules/nixos/happier-server.nix diff --git a/modules/packages.nix b/packages.nix similarity index 100% rename from modules/packages.nix rename to packages.nix From 0dae6dc0e4194fdd50285750f2ccaff1a87eb259 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Wed, 25 Feb 2026 15:27:45 +0100 Subject: [PATCH 04/24] =?UTF-8?q?fix:=20CI=20failures=20=E2=80=94=20treefm?= =?UTF-8?q?t=20flag=20and=20autoPatchelfHook=20on=20darwin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. nix fmt uses treefmt which needs --fail-on-change, not --check 2. autoPatchelfHook is Linux-only — skip it on darwin where Prisma ships self-contained Mach-O binaries Co-Authored-By: Claude Opus 4.6 --- .github/workflows/nix-build.yml | 2 +- packages/prisma-engines-prebuilt.nix | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index f4fd1be..721baf5 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -29,7 +29,7 @@ jobs: run: nix build .#packages.x86_64-linux.happier-server - name: Check formatting - run: nix fmt -- --check + run: nix fmt -- --fail-on-change nix-build-aarch64-darwin: name: Nix Build (aarch64-darwin) diff --git a/packages/prisma-engines-prebuilt.nix b/packages/prisma-engines-prebuilt.nix index 9ad5e15..9c4163d 100644 --- a/packages/prisma-engines-prebuilt.nix +++ b/packages/prisma-engines-prebuilt.nix @@ -5,7 +5,7 @@ # # When @prisma/client is bumped and yarn.lock changes: # 1. nix build will fail with a hash mismatch (the engine hash changed → new URL) -# 2. Run: ./nix/scripts/update-prisma-hashes.sh +# 2. Run: ./scripts/update-prisma-hashes.sh # 3. Commit the updated hashes { pkgs, @@ -28,7 +28,7 @@ let baseUrl = "https://binaries.prisma.sh/all_commits/${engineHash}"; # Platform-specific binary config - # Update these hashes after bumping Prisma: ./nix/update-prisma-hashes.sh + # Update these hashes after bumping Prisma: ./scripts/update-prisma-hashes.sh platformConfig = { "aarch64-linux" = { platform = "linux-arm64-openssl-3.0.x"; @@ -75,11 +75,13 @@ pkgs.stdenv.mkDerivation { nativeBuildInputs = [ pkgs.gzip + ] ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.autoPatchelfHook ]; - # Runtime libraries needed by the prebuilt binaries - buildInputs = [ + # Runtime libraries needed by the prebuilt Linux binaries. + # On darwin the Mach-O binaries are self-contained. + buildInputs = lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.openssl pkgs.stdenv.cc.cc.lib # libstdc++/libgcc pkgs.zlib From 1e9fb98a98c8dffaff73adf0fe4c1a74cd5c3cda Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Wed, 25 Feb 2026 22:24:57 +0100 Subject: [PATCH 05/24] style: apply nixfmt to prisma-engines-prebuilt.nix Co-Authored-By: Claude Opus 4.6 --- packages/prisma-engines-prebuilt.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/prisma-engines-prebuilt.nix b/packages/prisma-engines-prebuilt.nix index 9c4163d..d359292 100644 --- a/packages/prisma-engines-prebuilt.nix +++ b/packages/prisma-engines-prebuilt.nix @@ -75,7 +75,8 @@ pkgs.stdenv.mkDerivation { nativeBuildInputs = [ pkgs.gzip - ] ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + ] + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.autoPatchelfHook ]; From 6cb0ec578dd07b8b89ec4399714dd3c8fbd46663 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 17:09:06 +0100 Subject: [PATCH 06/24] feat: add flake checks, CI improvements, and automated input updates Switch CI to idiomatic `nix flake check`, which builds all packages, runs linting (deadnix, statix), and on Linux runs a NixOS VM integration test for the happier-server light mode. Pin the happier input to a release tag and expose `nix run .#update-happier` as a flake app for updating it. Add automated workflows for happier release tracking (daily) and general input updates via update-flake-lock (weekly). --- .github/workflows/nix-build.yml | 24 +++------ .github/workflows/update-flake-lock.yml | 28 +++++++++++ .github/workflows/update-happier.yml | 60 +++++++++++++++++++++++ checks.nix | 59 ++++++++++++++++++++++ devshell.nix | 4 +- flake.lock | 7 +-- flake.nix | 65 +++++++++++++++++++++++-- packages.nix | 3 +- statix.toml | 9 ++++ 9 files changed, 229 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/update-flake-lock.yml create mode 100644 .github/workflows/update-happier.yml create mode 100644 checks.nix create mode 100644 statix.toml diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 721baf5..5b3e4ec 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -22,11 +22,8 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - name: Build happier-cli - run: nix build .#packages.x86_64-linux.happier-cli - - - name: Build happier-server - run: nix build .#packages.x86_64-linux.happier-server + - name: Nix flake check + run: nix flake check - name: Check formatting run: nix fmt -- --fail-on-change @@ -42,11 +39,8 @@ jobs: - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - - name: Build happier-cli - run: nix build .#packages.aarch64-darwin.happier-cli - - - name: Build happier-server - run: nix build .#packages.aarch64-darwin.happier-server + - name: Nix flake check + run: nix flake check nix-build-aarch64-linux: name: Nix Build (aarch64-linux via QEMU) @@ -67,11 +61,5 @@ jobs: with: extra-conf: extra-platforms = aarch64-linux - - name: Build happier-cli - run: nix build .#packages.aarch64-linux.happier-cli --system aarch64-linux - - - name: Build happier-server - run: nix build .#packages.aarch64-linux.happier-server --system aarch64-linux - - - name: Evaluate NixOS module - run: nix eval .#nixosModules.happier-server + - name: Nix flake check + run: nix flake check --system aarch64-linux diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 0000000..c0747d6 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,28 @@ +name: Update flake inputs + +on: + schedule: + - cron: '0 4 * * 0' # Weekly, Sunday 4 AM UTC + workflow_dispatch: + +jobs: + update: + name: Update flake.lock + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@main + with: + # Don't update happier — that's handled by update-happier.yml + inputs: nixpkgs devshell flake-parts + pr-title: 'chore: update flake inputs' + pr-labels: dependencies diff --git a/.github/workflows/update-happier.yml b/.github/workflows/update-happier.yml new file mode 100644 index 0000000..0a146ae --- /dev/null +++ b/.github/workflows/update-happier.yml @@ -0,0 +1,60 @@ +name: Update happier input + +on: + # Triggered from happier repo via repository_dispatch + repository_dispatch: + types: [happier-release] + # Fallback: check daily + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + +jobs: + update: + name: Check for new happier release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Check for happier update + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: nix run .#update-happier + + - name: Check for changes + id: changes + run: | + if git diff --quiet flake.nix flake.lock; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Build to verify + if: steps.changes.outputs.changed == 'true' + run: nix flake check + + - name: Create PR + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=$(grep 'github:happier-dev/happier/' flake.nix \ + | sed 's|.*github:happier-dev/happier/||;s|".*||') + BRANCH="update-happier/$TAG" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add flake.nix flake.lock + git commit -m "chore: update happier input to $TAG" + git push -u origin "$BRANCH" + gh pr create \ + --title "chore: update happier to $TAG" \ + --body "Automated update of happier input to release $TAG." diff --git a/checks.nix b/checks.nix new file mode 100644 index 0000000..c5fdc73 --- /dev/null +++ b/checks.nix @@ -0,0 +1,59 @@ +# Flake checks: linting (deadnix, statix) and NixOS VM integration test +{ + self, + ... +}: +{ + perSystem = + { + pkgs, + lib, + config, + ... + }: + { + checks = { + # Detect unused Nix bindings + deadnix = pkgs.runCommand "deadnix" { nativeBuildInputs = [ pkgs.deadnix ]; } '' + deadnix --fail ${self} + touch $out + ''; + + # Detect Nix anti-patterns + statix = pkgs.runCommand "statix" { nativeBuildInputs = [ pkgs.statix ]; } '' + cd ${self} && statix check . + touch $out + ''; + } + // lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + # NixOS VM integration test — light mode (SQLite-only, no external deps) + nixos-happier-server-light = pkgs.testers.runNixOSTest { + name = "happier-server-light"; + + nodes.server = + { ... }: + { + imports = [ self.nixosModules.happier-server ]; + + services.happier-server = { + enable = true; + package = config.packages.happier-server; + mode = "light"; + }; + + virtualisation.memorySize = 2048; + }; + + testScript = '' + server.wait_for_unit("happier-server-migrate.service") + server.wait_for_unit("happier-server-sqlite-wal.service") + server.wait_for_unit("happier-server.service") + server.wait_for_open_port(3005) + + # Verify server responds (any HTTP response = service is up) + server.succeed("curl -sf http://localhost:3005/ || curl -sf -o /dev/null -w '%{http_code}' http://localhost:3005/ | grep -qE '^[0-9]'") + ''; + }; + }; + }; +} diff --git a/devshell.nix b/devshell.nix index 76099a1..750a6db 100644 --- a/devshell.nix +++ b/devshell.nix @@ -1,11 +1,9 @@ -{ inputs, ... }: +_: { perSystem = { - system, pkgs, - config, ... }: { diff --git a/flake.lock b/flake.lock index 4cd4d4b..cb3c456 100644 --- a/flake.lock +++ b/flake.lock @@ -41,15 +41,16 @@ "happier": { "flake": false, "locked": { - "lastModified": 1771832404, - "narHash": "sha256-yW1nyW+xe6LsTHCKq38qbhQdCz8Rvg+kkx+0Ovg/NV4=", + "lastModified": 1771714854, + "narHash": "sha256-E5WruyOZcU+4c5pJokcCu54PaEmg+alcvlrEjADenpg=", "owner": "happier-dev", "repo": "happier", - "rev": "23e20d826d30e3532c7472aecb819f6ca1b56191", + "rev": "f8f20c7760a2cacedcdf89f33bf569398b71941b", "type": "github" }, "original": { "owner": "happier-dev", + "ref": "stack-v0.1.0-preview.1771759103.67820", "repo": "happier", "type": "github" } diff --git a/flake.nix b/flake.nix index 454b4f8..c244cc0 100644 --- a/flake.nix +++ b/flake.nix @@ -10,8 +10,9 @@ flake-parts.url = "github:hercules-ci/flake-parts"; # The happier monorepo source (fetched as a plain source tree, not evaluated as a flake) + # Pinned to a stack release tag; updated automatically by update-happier.yml happier = { - url = "github:happier-dev/happier"; + url = "github:happier-dev/happier/stack-v0.1.0-preview.1771759103.67820"; flake = false; }; }; @@ -30,6 +31,7 @@ inputs.devshell.flakeModule ./devshell.nix ./packages.nix + ./checks.nix ]; flake = { @@ -40,22 +42,77 @@ perSystem = { pkgs, - lib, system, ... }: { formatter = pkgs.nixfmt-tree; + # nix run .#update-happier — update the happier input to the latest stack release tag + apps.update-happier = + let + script = pkgs.writeShellApplication { + name = "update-happier"; + meta.description = "Update the happier flake input to the latest stack release tag"; + runtimeInputs = with pkgs; [ + gh + gnused + gnugrep + coreutils + ]; + text = '' + FLAKE_NIX="$(pwd)/flake.nix" + if [[ ! -f "$FLAKE_NIX" ]]; then + echo "error: flake.nix not found in current directory" >&2 + exit 1 + fi + + # Get latest stack-v* release tag from happier repo + LATEST_TAG=$(gh release list -R happier-dev/happier \ + --json tagName -q '.[].tagName' | grep '^stack-v' | head -1 || true) + + if [[ -z "$LATEST_TAG" ]]; then + echo "error: no stack-v* release tags found" >&2 + exit 1 + fi + + # Extract current tag from flake.nix + CURRENT_TAG=$(grep 'github:happier-dev/happier/' "$FLAKE_NIX" \ + | sed 's|.*github:happier-dev/happier/||;s|".*||') + + echo "Current: $CURRENT_TAG" + echo "Latest: $LATEST_TAG" + + if [[ "$CURRENT_TAG" == "$LATEST_TAG" ]]; then + echo "Already up to date." + exit 0 + fi + + # Update tag in flake.nix + sed -i "s|github:happier-dev/happier/[^\"]*|github:happier-dev/happier/$LATEST_TAG|" "$FLAKE_NIX" + + # Update the lockfile to match + nix flake update happier + + echo "Updated happier: $CURRENT_TAG -> $LATEST_TAG" + ''; + }; + in + { + type = "app"; + program = pkgs.lib.getExe script; + meta.description = "Update the happier flake input to the latest stack release tag"; + }; + _module.args.pkgs = import self.inputs.nixpkgs { inherit system; overlays = [ # Prebuilt Prisma engines — version auto-derived from yarn.lock. # When @prisma/client is bumped, run: ./scripts/update-prisma-hashes.sh - (final: prev: { + (final: _prev: { prisma-engines = import ./packages/prisma-engines-prebuilt.nix { pkgs = final; - lib = final.lib; + inherit (final) lib; yarnLock = "${inputs.happier}/yarn.lock"; }; }) diff --git a/packages.nix b/packages.nix index cf7f335..bdacae5 100644 --- a/packages.nix +++ b/packages.nix @@ -4,7 +4,6 @@ { perSystem = { - system, pkgs, lib, ... @@ -16,7 +15,7 @@ filteredSrc = lib.cleanSourceWith { src = happierSrc; filter = - path: type: + path: _type: let relPath = lib.removePrefix (toString happierSrc + "/") (toString path); in diff --git a/statix.toml b/statix.toml new file mode 100644 index 0000000..ca3407f --- /dev/null +++ b/statix.toml @@ -0,0 +1,9 @@ +# statix configuration +# W20 (repeated keys) is suppressed because NixOS modules idiomatically +# use separate top-level assignments with lib.mkIf guards per service. +disabled = ["repeated_keys"] + +ignore = [ + ".direnv", + "result", +] From 7e4bcb1b85078bb3ba6e5014f1d3112a55587680 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 17:33:04 +0100 Subject: [PATCH 07/24] refactor: drop update workflows and tag pinning for now Happier is still in preview with no stable releases. Revert to following the default branch and remove the automated update workflows and flake app. These are preserved on the ci/auto-update-workflows branch for when official releases begin. --- .github/workflows/update-flake-lock.yml | 28 ------------ .github/workflows/update-happier.yml | 60 ------------------------- flake.lock | 7 ++- flake.nix | 59 +----------------------- 4 files changed, 4 insertions(+), 150 deletions(-) delete mode 100644 .github/workflows/update-flake-lock.yml delete mode 100644 .github/workflows/update-happier.yml diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml deleted file mode 100644 index c0747d6..0000000 --- a/.github/workflows/update-flake-lock.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Update flake inputs - -on: - schedule: - - cron: '0 4 * * 0' # Weekly, Sunday 4 AM UTC - workflow_dispatch: - -jobs: - update: - name: Update flake.lock - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Update flake.lock - uses: DeterminateSystems/update-flake-lock@main - with: - # Don't update happier — that's handled by update-happier.yml - inputs: nixpkgs devshell flake-parts - pr-title: 'chore: update flake inputs' - pr-labels: dependencies diff --git a/.github/workflows/update-happier.yml b/.github/workflows/update-happier.yml deleted file mode 100644 index 0a146ae..0000000 --- a/.github/workflows/update-happier.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Update happier input - -on: - # Triggered from happier repo via repository_dispatch - repository_dispatch: - types: [happier-release] - # Fallback: check daily - schedule: - - cron: '0 5 * * *' - workflow_dispatch: - -jobs: - update: - name: Check for new happier release - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Check for happier update - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: nix run .#update-happier - - - name: Check for changes - id: changes - run: | - if git diff --quiet flake.nix flake.lock; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Build to verify - if: steps.changes.outputs.changed == 'true' - run: nix flake check - - - name: Create PR - if: steps.changes.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG=$(grep 'github:happier-dev/happier/' flake.nix \ - | sed 's|.*github:happier-dev/happier/||;s|".*||') - BRANCH="update-happier/$TAG" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add flake.nix flake.lock - git commit -m "chore: update happier input to $TAG" - git push -u origin "$BRANCH" - gh pr create \ - --title "chore: update happier to $TAG" \ - --body "Automated update of happier input to release $TAG." diff --git a/flake.lock b/flake.lock index cb3c456..08e93ed 100644 --- a/flake.lock +++ b/flake.lock @@ -41,16 +41,15 @@ "happier": { "flake": false, "locked": { - "lastModified": 1771714854, - "narHash": "sha256-E5WruyOZcU+4c5pJokcCu54PaEmg+alcvlrEjADenpg=", + "lastModified": 1772114292, + "narHash": "sha256-nSF/bbZKakatNl2JSQS9WV3r+Mgtgvl5mVfj+GFYWbs=", "owner": "happier-dev", "repo": "happier", - "rev": "f8f20c7760a2cacedcdf89f33bf569398b71941b", + "rev": "e7a17e05528bc787f09267a8019816c113025eb2", "type": "github" }, "original": { "owner": "happier-dev", - "ref": "stack-v0.1.0-preview.1771759103.67820", "repo": "happier", "type": "github" } diff --git a/flake.nix b/flake.nix index c244cc0..61b76eb 100644 --- a/flake.nix +++ b/flake.nix @@ -10,9 +10,8 @@ flake-parts.url = "github:hercules-ci/flake-parts"; # The happier monorepo source (fetched as a plain source tree, not evaluated as a flake) - # Pinned to a stack release tag; updated automatically by update-happier.yml happier = { - url = "github:happier-dev/happier/stack-v0.1.0-preview.1771759103.67820"; + url = "github:happier-dev/happier"; flake = false; }; }; @@ -48,62 +47,6 @@ { formatter = pkgs.nixfmt-tree; - # nix run .#update-happier — update the happier input to the latest stack release tag - apps.update-happier = - let - script = pkgs.writeShellApplication { - name = "update-happier"; - meta.description = "Update the happier flake input to the latest stack release tag"; - runtimeInputs = with pkgs; [ - gh - gnused - gnugrep - coreutils - ]; - text = '' - FLAKE_NIX="$(pwd)/flake.nix" - if [[ ! -f "$FLAKE_NIX" ]]; then - echo "error: flake.nix not found in current directory" >&2 - exit 1 - fi - - # Get latest stack-v* release tag from happier repo - LATEST_TAG=$(gh release list -R happier-dev/happier \ - --json tagName -q '.[].tagName' | grep '^stack-v' | head -1 || true) - - if [[ -z "$LATEST_TAG" ]]; then - echo "error: no stack-v* release tags found" >&2 - exit 1 - fi - - # Extract current tag from flake.nix - CURRENT_TAG=$(grep 'github:happier-dev/happier/' "$FLAKE_NIX" \ - | sed 's|.*github:happier-dev/happier/||;s|".*||') - - echo "Current: $CURRENT_TAG" - echo "Latest: $LATEST_TAG" - - if [[ "$CURRENT_TAG" == "$LATEST_TAG" ]]; then - echo "Already up to date." - exit 0 - fi - - # Update tag in flake.nix - sed -i "s|github:happier-dev/happier/[^\"]*|github:happier-dev/happier/$LATEST_TAG|" "$FLAKE_NIX" - - # Update the lockfile to match - nix flake update happier - - echo "Updated happier: $CURRENT_TAG -> $LATEST_TAG" - ''; - }; - in - { - type = "app"; - program = pkgs.lib.getExe script; - meta.description = "Update the happier flake input to the latest stack release tag"; - }; - _module.args.pkgs = import self.inputs.nixpkgs { inherit system; overlays = [ From 8911b06c4c0fe329ceb852a15bcd6d9ce318f97c Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 19:58:52 +0100 Subject: [PATCH 08/24] feat(nix): replace update-prisma-hashes script with flake apps Package the hash updater as `apps.update-prisma-hashes` via writeShellApplication, and add `apps.update` that runs `nix flake update` followed by the hash refresh. Yarn.lock is resolved from the `happier` flake input at eval time instead of assuming a sibling directory. Uses `nix store prefetch-file` for native SRI hashes, replacing legacy `nix-prefetch-url` (base32). - Convert all 6 platform hashes to SRI format - Add `update` command to devshell - Delete scripts/update-prisma-hashes.sh --- devshell.nix | 7 ++ flake.nix | 115 +++++++++++++++++++++++++++ packages/prisma-engines-prebuilt.nix | 12 +-- scripts/update-prisma-hashes.sh | 91 --------------------- 4 files changed, 128 insertions(+), 97 deletions(-) delete mode 100755 scripts/update-prisma-hashes.sh diff --git a/devshell.nix b/devshell.nix index 750a6db..d1a39bf 100644 --- a/devshell.nix +++ b/devshell.nix @@ -117,6 +117,13 @@ _: find . -name "*.nix" -type f -print0 | xargs -0 nixfmt ''; } + { + name = "update"; + help = "Update all flake inputs and refresh Prisma engine hashes"; + command = '' + nix run .#update + ''; + } ]; }; }; diff --git a/flake.nix b/flake.nix index 61b76eb..95ed7da 100644 --- a/flake.nix +++ b/flake.nix @@ -41,12 +41,127 @@ perSystem = { pkgs, + lib, system, ... }: { formatter = pkgs.nixfmt-tree; + apps.update-prisma-hashes = { + type = "app"; + meta.description = "Update Prisma engine binary hashes in prisma-engines-prebuilt.nix"; + program = lib.getExe ( + pkgs.writeShellApplication { + name = "update-prisma-hashes"; + runtimeInputs = with pkgs; [ + jq + gnugrep + gnused + coreutils + ]; + text = '' + NIX_FILE="./packages/prisma-engines-prebuilt.nix" + YARN_LOCK="${inputs.happier}/yarn.lock" + + if [ ! -f "$NIX_FILE" ]; then + echo "ERROR: $NIX_FILE not found — run from the nix-happier repo root" >&2 + exit 1 + fi + + # Extract engine hash from yarn.lock + ENGINE_HASH=$(grep '@prisma/engines-version' "$YARN_LOCK" | head -1 | grep -oE '[a-f0-9]{40}') + + if [ -z "$ENGINE_HASH" ]; then + echo "ERROR: Could not find @prisma/engines-version in yarn.lock" >&2 + exit 1 + fi + + echo "Engine commit hash: $ENGINE_HASH" + + BASE_URL="https://binaries.prisma.sh/all_commits/$ENGINE_HASH" + + # Platform configs: system prisma-platform queryEngineFile schemaEngineFile + PLATFORMS=( + "aarch64-linux linux-arm64-openssl-3.0.x libquery_engine.so.node.gz schema-engine.gz" + "aarch64-darwin darwin-arm64 libquery_engine.dylib.node.gz schema-engine.gz" + "x86_64-linux debian-openssl-3.0.x libquery_engine.so.node.gz schema-engine.gz" + ) + + declare -A NEW_HASHES + + for entry in "''${PLATFORMS[@]}"; do + read -r sys plat qe_file se_file <<< "$entry" + echo "" + echo "Prefetching $sys binaries..." + + qe_url="$BASE_URL/$plat/$qe_file" + se_url="$BASE_URL/$plat/$se_file" + + echo " Fetching: $qe_url" + qe_hash=$(nix store prefetch-file --json --hash-type sha256 "$qe_url" | jq -r .hash) + echo " -> $qe_hash" + + echo " Fetching: $se_url" + se_hash=$(nix store prefetch-file --json --hash-type sha256 "$se_url" | jq -r .hash) + echo " -> $se_hash" + + NEW_HASHES["''${sys}_qe"]="$qe_hash" + NEW_HASHES["''${sys}_se"]="$se_hash" + done + + echo "" + echo "Updating hashes in $NIX_FILE..." + + # Read current hashes in order of appearance + mapfile -t CURRENT_HASHES < <(grep -oP 'Hash = "\K[^"]+' "$NIX_FILE") + + # Order must match nix file: aarch64-linux QE, SE, aarch64-darwin QE, SE, x86_64-linux QE, SE + ORDERED_NEW=( + "''${NEW_HASHES[aarch64-linux_qe]}" + "''${NEW_HASHES[aarch64-linux_se]}" + "''${NEW_HASHES[aarch64-darwin_qe]}" + "''${NEW_HASHES[aarch64-darwin_se]}" + "''${NEW_HASHES[x86_64-linux_qe]}" + "''${NEW_HASHES[x86_64-linux_se]}" + ) + + for i in "''${!CURRENT_HASHES[@]}"; do + old="''${CURRENT_HASHES[$i]}" + new="''${ORDERED_NEW[$i]}" + if [ "$old" != "$new" ]; then + sed -i "s|$old|$new|g" "$NIX_FILE" + echo " Updated: $old -> $new" + else + echo " Unchanged: $old" + fi + done + + echo "" + echo "Done! Verify with: nix build .#happier-server" + ''; + } + ); + }; + + apps.update = { + type = "app"; + meta.description = "Update all flake inputs and refresh Prisma engine hashes"; + program = lib.getExe ( + pkgs.writeShellApplication { + name = "update"; + text = '' + echo "Updating flake inputs..." + nix flake update + + echo "" + echo "Updating Prisma engine hashes..." + nix run .#update-prisma-hashes + ''; + } + ); + }; + _module.args.pkgs = import self.inputs.nixpkgs { inherit system; overlays = [ diff --git a/packages/prisma-engines-prebuilt.nix b/packages/prisma-engines-prebuilt.nix index d359292..402c889 100644 --- a/packages/prisma-engines-prebuilt.nix +++ b/packages/prisma-engines-prebuilt.nix @@ -34,22 +34,22 @@ let platform = "linux-arm64-openssl-3.0.x"; queryEngineFile = "libquery_engine.so.node.gz"; schemaEngineFile = "schema-engine.gz"; - queryEngineHash = "1bkp5a5m8jmq2l3slc4lfaaji1z54zc7rg65rv9jyh6pz94mqv7l"; - schemaEngineHash = "09pxr9djichrpi9dxmr4q02l7qayl0cbx274zak66vda97g546rg"; + queryEngineHash = "sha256-9GxcSfrXQC/TzsW8fNgn5YcolXKUMKoHFbhKVIsqd64="; + schemaEngineHash = "sha256-LxtS3kmqbWOm+uSIvhigXuFDBcAk195SvBmyKFvK/SY="; }; "aarch64-darwin" = { platform = "darwin-arm64"; queryEngineFile = "libquery_engine.dylib.node.gz"; schemaEngineFile = "schema-engine.gz"; - queryEngineHash = "0kl0g4y84qy2krlh4djr1i9cjzkxv9aqmf8m1x5knb31n4fba544"; - schemaEngineHash = "0wypyw9djpqwizk90f2xlj458p8ywcgah8kqpx2y251jv00bcld9"; + queryEngineHash = "sha256-hBS1HLFhLDtLDxW5ilXafX7JUgxZNgJpnsJjgjx5gE4="; + schemaEngineHash = "sha256-qVG2ANgyFOFFv3giqB7jHl1UiKRdOJDmjxxf2RL313M="; }; "x86_64-linux" = { platform = "debian-openssl-3.0.x"; queryEngineFile = "libquery_engine.so.node.gz"; schemaEngineFile = "schema-engine.gz"; - queryEngineHash = "046nqra0rvdiazmnphyxa6yzpjsg1w0dqjdjxg310wx1r0n8g06k"; - schemaEngineHash = "12ixm3mhrr6advyb800cklybvqa744av68gxi2q8g12k6kzgs7bc"; + queryEngineHash = "sha256-04CHLMihcxDG67JJ3AAPT8v7vVHdw2vrV7HtDFTG1hA="; + schemaEngineHash = "sha256-bB39/jRThIewiP0hsxUhR+G9PJ0MALT8bsrkDOuoPYo="; }; }; diff --git a/scripts/update-prisma-hashes.sh b/scripts/update-prisma-hashes.sh deleted file mode 100755 index 47443b1..0000000 --- a/scripts/update-prisma-hashes.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# Update prebuilt Prisma engine binary hashes after bumping @prisma/client. -# -# Usage: ./scripts/update-prisma-hashes.sh [path/to/happier/yarn.lock] -# -# If no yarn.lock path is given, the script looks for ../happier/yarn.lock -# (assumes the happier monorepo is a sibling directory). -# -# This reads the engine commit hash from yarn.lock and prefetches -# binaries for all supported platforms, then updates the hashes -# in packages/prisma-engines-prebuilt.nix. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(dirname "$SCRIPT_DIR")" -NIX_FILE="$REPO_ROOT/packages/prisma-engines-prebuilt.nix" - -YARN_LOCK="${1:-}" -if [ -z "$YARN_LOCK" ]; then - # Default: look for happier monorepo as a sibling directory - YARN_LOCK="$REPO_ROOT/../happier/yarn.lock" -fi - -if [ ! -f "$YARN_LOCK" ]; then - echo "ERROR: yarn.lock not found at $YARN_LOCK" - echo "Usage: $0 [path/to/happier/yarn.lock]" - exit 1 -fi - -echo "Using yarn.lock: $YARN_LOCK" - -# Extract engine hash from yarn.lock -ENGINE_HASH=$(grep '@prisma/engines-version' "$YARN_LOCK" | head -1 | grep -oE '[a-f0-9]{40}') - -if [ -z "$ENGINE_HASH" ]; then - echo "ERROR: Could not find @prisma/engines-version in yarn.lock" - exit 1 -fi - -echo "Engine commit hash: $ENGINE_HASH" - -BASE_URL="https://binaries.prisma.sh/all_commits/$ENGINE_HASH" - -prefetch() { - local url="$1" - echo " Fetching: $url" >&2 - nix-prefetch-url "$url" --type sha256 2>/dev/null -} - -update_hash() { - local old_hash="$1" - local new_hash="$2" - if [ "$old_hash" != "$new_hash" ]; then - tmp="$(mktemp)" - sed "s|$old_hash|$new_hash|g" "$NIX_FILE" > "$tmp" - mv "$tmp" "$NIX_FILE" - echo " Updated: $old_hash -> $new_hash" - else - echo " Unchanged: $old_hash" - fi -} - -echo "" -echo "Prefetching aarch64-linux binaries..." -AARCH64_LINUX_QE=$(prefetch "$BASE_URL/linux-arm64-openssl-3.0.x/libquery_engine.so.node.gz") -AARCH64_LINUX_SE=$(prefetch "$BASE_URL/linux-arm64-openssl-3.0.x/schema-engine.gz") - -echo "Prefetching aarch64-darwin binaries..." -AARCH64_DARWIN_QE=$(prefetch "$BASE_URL/darwin-arm64/libquery_engine.dylib.node.gz") -AARCH64_DARWIN_SE=$(prefetch "$BASE_URL/darwin-arm64/schema-engine.gz") - -echo "Prefetching x86_64-linux binaries..." -X86_64_LINUX_QE=$(prefetch "$BASE_URL/debian-openssl-3.0.x/libquery_engine.so.node.gz") -X86_64_LINUX_SE=$(prefetch "$BASE_URL/debian-openssl-3.0.x/schema-engine.gz") - -echo "" -echo "Updating hashes in $NIX_FILE..." - -# Read current hashes from the nix file (in order of appearance) -CURRENT_HASHES=($(grep -oE 'Hash = "[^"]+"' "$NIX_FILE" | sed 's/.*Hash = "//;s/"//')) - -# Update in order: aarch64-linux QE, SE, aarch64-darwin QE, SE, x86_64-linux QE, SE -NEW_HASHES=("$AARCH64_LINUX_QE" "$AARCH64_LINUX_SE" "$AARCH64_DARWIN_QE" "$AARCH64_DARWIN_SE" "$X86_64_LINUX_QE" "$X86_64_LINUX_SE") - -for i in "${!CURRENT_HASHES[@]}"; do - update_hash "${CURRENT_HASHES[$i]}" "${NEW_HASHES[$i]}" -done - -echo "" -echo "Done! Verify with: nix build .#happier-server" From 3431d43b0fdfed9c56acbd06ebb68e0e0aaf5dbf Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 20:15:13 +0100 Subject: [PATCH 09/24] refactor(nix): extract light server example from checks.nix Move the happier-server light mode config into examples/ so it serves as both runnable documentation and the actual config consumed by CI. --- checks.nix | 13 +++++-------- examples/happier-server-light.nix | 10 ++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 examples/happier-server-light.nix diff --git a/checks.nix b/checks.nix index c5fdc73..d3d5444 100644 --- a/checks.nix +++ b/checks.nix @@ -33,14 +33,11 @@ nodes.server = { ... }: { - imports = [ self.nixosModules.happier-server ]; - - services.happier-server = { - enable = true; - package = config.packages.happier-server; - mode = "light"; - }; - + imports = [ + self.nixosModules.happier-server + ./examples/happier-server-light.nix + ]; + services.happier-server.package = config.packages.happier-server; virtualisation.memorySize = 2048; }; diff --git a/examples/happier-server-light.nix b/examples/happier-server-light.nix new file mode 100644 index 0000000..ca6e278 --- /dev/null +++ b/examples/happier-server-light.nix @@ -0,0 +1,10 @@ +# Example: Happier Server in light mode (SQLite, no external dependencies) +# +# Minimal configuration — the consumer must set services.happier-server.package. +# In production, also set environmentFile for secrets (HANDY_MASTER_SECRET). +{ + services.happier-server = { + enable = true; + mode = "light"; + }; +} From b564a6b277b69ab61ec52078240ec0449840c1dc Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 20:25:33 +0100 Subject: [PATCH 10/24] docs: add README and trim devshell to nix-happier scope Add README.md covering flake outputs, quick start, module options, examples, development, and repo structure. Remove yarn-based devshell commands and packages that were carried over from the happier monorepo and don't work in this repo. --- README.md | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++ devshell.nix | 90 +--------------------------- 2 files changed, 166 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 8b13789..46566cd 100644 --- a/README.md +++ b/README.md @@ -1 +1,166 @@ +# nix-happier +Nix flake for building and deploying [Happier](https://github.com/happier-dev/happier) Server and CLI. + +## Flake outputs + +| Output | Description | +|--------|-------------| +| `packages.happier-server` | Happier Server (full + light mode binaries) | +| `packages.happier-cli` | Happier CLI | +| `nixosModules.happier-server` | NixOS module for running Happier Server as a systemd service | +| `checks.deadnix` | Unused binding detection | +| `checks.statix` | Nix anti-pattern linting | +| `checks.nixos-happier-server-light` | NixOS VM integration test (Linux only) | +| `apps.update-prisma-hashes` | Update Prisma engine binary hashes | +| `apps.update` | Update all flake inputs + Prisma hashes | +| `devShells.default` | Dev shell with git and nixfmt | + +Supported systems: `aarch64-darwin`, `aarch64-linux`, `x86_64-linux` + +## Quick start + +Add the flake input to your NixOS configuration: + +```nix +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nix-happier.url = "github:happier-dev/nix-happier"; + }; + + outputs = { nixpkgs, nix-happier, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + nix-happier.nixosModules.happier-server + { + services.happier-server = { + enable = true; + package = nix-happier.packages.x86_64-linux.happier-server; + mode = "light"; # or "full" + }; + } + ]; + }; + }; +} +``` + +### Light mode (SQLite, no external deps) + +See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for a minimal configuration. Light mode uses SQLite with WAL enabled automatically — no PostgreSQL, Redis, or MinIO required. + +### Full mode (PostgreSQL + Redis + MinIO) + +```nix +{ + services.happier-server = { + enable = true; + package = nix-happier.packages.x86_64-linux.happier-server; + mode = "full"; + port = 3005; + environmentFile = "/run/secrets/happier-env"; # must contain HANDY_MASTER_SECRET + + database = { + name = "happier"; + user = "happier"; + createLocally = true; # provisions PostgreSQL 15 + }; + + redis.createLocally = true; + + minio = { + createLocally = true; + bucket = "happier"; + rootCredentialsFile = "/run/secrets/minio-creds"; # MINIO_ROOT_USER + MINIO_ROOT_PASSWORD + }; + }; +} +``` + +## Module options + +### Core + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enable` | bool | `false` | Enable Happier Server | +| `package` | package | — | The `happier-server` package to use | +| `port` | port | `3005` | Port to listen on | +| `mode` | `"full"` \| `"light"` | `"full"` | `full` = PostgreSQL + Redis + MinIO; `light` = SQLite only | +| `environmentFile` | path \| null | `null` | Secrets file (`KEY=value`). Should contain `HANDY_MASTER_SECRET` | + +### Database (full mode) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `database.name` | string | `"happier"` | PostgreSQL database name | +| `database.user` | string | `"happier"` | PostgreSQL user | +| `database.createLocally` | bool | `true` | Provision PostgreSQL locally | + +### Redis (full mode) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `redis.createLocally` | bool | `true` | Provision Redis locally | + +### MinIO (full mode) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `minio.createLocally` | bool | `true` | Provision MinIO locally for S3-compatible storage | +| `minio.bucket` | string | `"happier"` | MinIO bucket name | +| `minio.rootCredentialsFile` | path \| null | `null` | File with `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD` | + +## Examples + +The [`examples/`](examples/) directory contains ready-to-use NixOS configurations. These examples are tested in CI via `nix flake check` (the light-mode example runs as a NixOS VM integration test on Linux). + +## Development + +Enter the dev shell: + +```sh +nix develop +``` + +Available commands inside the shell: + +| Command | Description | +|---------|-------------| +| `fmt` | Format Nix files | +| `update` | Update flake inputs + Prisma hashes | + +Run linting and integration tests: + +```sh +nix flake check +``` + +Update all inputs and refresh Prisma engine hashes: + +```sh +nix run .#update +``` + +## Repo structure + +``` +. +├── flake.nix # Flake entrypoint +├── flake.lock +├── packages.nix # happier-server + happier-cli derivations +├── checks.nix # deadnix, statix, NixOS VM test +├── devshell.nix # Dev shell with commands +├── modules/ +│ └── nixos/ +│ └── happier-server.nix # NixOS module +├── packages/ +│ └── prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries +├── examples/ +│ └── happier-server-light.nix # Light mode example config +└── .github/ + └── workflows/ + └── nix-build.yml # CI workflow +``` diff --git a/devshell.nix b/devshell.nix index d1a39bf..12bc991 100644 --- a/devshell.nix +++ b/devshell.nix @@ -10,10 +10,6 @@ _: devshells = { default = { packages = [ - pkgs.nodejs_22 - pkgs.yarn - pkgs.python3 - pkgs.ffmpeg pkgs.git pkgs.nixfmt-rfc-style ]; @@ -27,91 +23,7 @@ _: commands = [ { - name = "dev"; - help = "Run a workspace in dev mode: dev "; - command = '' - workspace="''${1:-}" - case "$workspace" in - cli) yarn workspace @happier-dev/cli dev ;; - server) yarn workspace @happier-dev/server dev ;; - app) yarn workspace @happier-dev/ui start ;; - website) yarn workspace @happier-dev/website dev ;; - docs) yarn workspace @happier-dev/docs dev ;; - *) - echo "Usage: dev " - exit 1 - ;; - esac - ''; - } - { - name = "build"; - help = "Build a workspace: build "; - command = '' - workspace="''${1:-all}" - case "$workspace" in - cli) yarn workspace @happier-dev/cli build ;; - server) yarn workspace @happier-dev/server build ;; - app) yarn workspace @happier-dev/ui build ;; - website) yarn workspace @happier-dev/website build ;; - all) yarn workspaces run build ;; - *) - echo "Usage: build " - exit 1 - ;; - esac - ''; - } - { - name = "test"; - help = "Run tests for a workspace: test "; - command = '' - workspace="''${1:-all}" - case "$workspace" in - cli) yarn workspace @happier-dev/cli test ;; - server) yarn workspace @happier-dev/server test ;; - protocol) yarn workspace @happier-dev/protocol test ;; - all) yarn workspaces run test ;; - *) - echo "Usage: test " - exit 1 - ;; - esac - ''; - } - { - name = "format"; - help = "Format code"; - command = '' - yarn workspaces run format 2>/dev/null || true - ''; - } - { - name = "lint"; - help = "Lint code"; - command = '' - yarn workspaces run lint 2>/dev/null || true - ''; - } - { - name = "db"; - help = "Run database commands for happier-server: db "; - command = '' - cmd="''${1:-start}" - case "$cmd" in - start) yarn workspace @happier-dev/server db ;; - migrate) yarn workspace @happier-dev/server prisma migrate dev ;; - seed) yarn workspace @happier-dev/server prisma db seed ;; - studio) yarn workspace @happier-dev/server prisma studio ;; - *) - echo "Usage: db " - exit 1 - ;; - esac - ''; - } - { - name = "nix-fmt"; + name = "fmt"; help = "Format Nix files"; command = '' find . -name "*.nix" -type f -print0 | xargs -0 nixfmt From e59ca9b656f14ba6bf15fa873abeb83f3b810b79 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 20:29:18 +0100 Subject: [PATCH 11/24] docs: add pre-release notice about branch tracking and manual updates --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 46566cd..bc3cc58 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Nix flake for building and deploying [Happier](https://github.com/happier-dev/happier) Server and CLI. +> **Pre-release notice:** The `happier` flake input currently tracks the `main` branch. This will be pinned to tagged releases once Happier reaches a stable version. Updates to this flake are made manually for now — run `nix run .#update` to pull the latest. + ## Flake outputs | Output | Description | From a1c0f933ae779b7c8cec8b12ca8354c8e61bc8bb Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 20:33:00 +0100 Subject: [PATCH 12/24] docs: add AGENTS.md with Nix-specific guidelines and symlink CLAUDE.md Provides AI coding agents with repo context, Nix style conventions, key patterns (flake-parts, NixOS module, Prisma engine updates), verification steps, and do's/don'ts. CLAUDE.md symlinks to AGENTS.md so both Claude Code and other tools pick up the same instructions. --- AGENTS.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 86 insertions(+) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f9acfa9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,85 @@ +# nix-happier — Agent Guidelines + +This file provides guidance for AI coding agents (Claude Code, Copilot, Codex, etc.) working in this repo. + +## What this repo is + +A Nix flake that builds and deploys [Happier](https://github.com/happier-dev/happier) Server and CLI. It does **not** contain application source code — the happier monorepo is fetched as a flake input. This repo only has Nix expressions, NixOS modules, and supporting config. + +## Repo layout + +``` +flake.nix # Flake entrypoint (inputs, systems, imports) +packages.nix # happier-server + happier-cli derivations +checks.nix # deadnix, statix, NixOS VM integration test +devshell.nix # Dev shell (fmt, update commands) +modules/nixos/happier-server.nix # NixOS service module +packages/prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries +examples/happier-server-light.nix # Light mode example (used by VM test) +``` + +## Language and tooling + +- **All code is Nix.** There is no TypeScript, Python, or other source code here. +- **Formatter**: `nixfmt-tree` (the flake's `formatter`). Run `nix fmt` to format. +- **Linters**: `deadnix` (unused bindings) and `statix` (anti-patterns). Both run via `nix flake check`. +- **statix config**: `statix.toml` disables `repeated_keys` (W20) — NixOS modules idiomatically use separate top-level assignments with `lib.mkIf` guards. + +## Nix style conventions + +- Use `nixfmt-tree` style (the RFC-style formatter). Do not manually reformat — run `nix fmt`. +- Prefer `lib.mkIf` / `lib.mkOption` / `lib.optional` over raw `if-then-else` in NixOS modules. +- Use `let ... in` for local bindings. Keep `let` blocks close to where they're used. +- Avoid `with pkgs;` at module scope — use it only in narrow scopes (e.g. inside `buildInputs` lists). +- `flake-parts` is used for per-system logic. New per-system outputs go in dedicated `.nix` files imported by `flake.nix`. +- Comments should explain *why*, not *what*. Nix expressions are usually self-documenting. + +## Key patterns + +### Flake structure + +The flake uses [flake-parts](https://github.com/hercules-ci/flake-parts). Per-system outputs (packages, checks, devshell, apps) are split into separate files and imported in `flake.nix`. Flake-level outputs (`nixosModules`) are defined in the `flake = { ... }` attrset. + +### NixOS module + +`modules/nixos/happier-server.nix` defines `services.happier-server` options. It supports two modes: +- **full**: PostgreSQL + Redis + MinIO (production stack) +- **light**: SQLite-only, no external deps + +The module provisions supporting services (PostgreSQL, Redis, MinIO) when `createLocally = true` and handles migrations, WAL mode, and secret loading via `systemd` `LoadCredential`. + +### Package builds + +Both `happier-server` and `happier-cli` are built from the happier monorepo source (`inputs.happier`). Prisma engines come from `packages/prisma-engines-prebuilt.nix` which fetches prebuilt binaries by hash. + +### Updating dependencies + +When the happier monorepo updates `@prisma/client`, the engine hashes here must also be updated: + +```sh +nix run .#update # updates flake inputs + Prisma hashes +nix run .#update-prisma-hashes # updates only Prisma hashes +``` + +## Verification + +Always verify changes with: + +```sh +nix fmt # format +nix flake check # lint (deadnix, statix) + VM integration test (Linux) +nix build .#happier-server # build the server package +``` + +`nix flake check` runs the NixOS VM integration test on Linux — it boots a VM with the light-mode example and verifies the server starts and responds on port 3005. + +## Do's and don'ts + +- **Do** run `nix fmt` after every change. +- **Do** run `nix flake check` before considering work done (or at minimum `nix build`). +- **Do** keep the NixOS module options in sync with the README's "Module options" table. +- **Do** add examples to `examples/` for new configurations and reference them in checks if testable. +- **Don't** modify `flake.lock` by hand — use `nix flake update` or `nix run .#update`. +- **Don't** add packages, commands, or dependencies that belong in the happier monorepo, not here. +- **Don't** introduce `with pkgs;` at module or file scope. +- **Don't** create migrations or modify Prisma schemas — that happens in the happier monorepo. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 8c3f982deba80d6178fdb15eccb39100b753c2a9 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 20:43:55 +0100 Subject: [PATCH 13/24] docs: add Tailscale example and clarify environmentFile requirement Add examples/happier-server-tailscale.nix showing the recommended production setup: light mode + Tailscale + nginx TLS reverse proxy with auto-renewed certs. Add a Secrets section to the README making the HANDY_MASTER_SECRET environment file requirement prominent. Restructure Quick Start to lead with the Tailscale example. --- AGENTS.md | 5 +- README.md | 42 ++++++++-- examples/happier-server-light.nix | 9 ++- examples/happier-server-tailscale.nix | 112 ++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 examples/happier-server-tailscale.nix diff --git a/AGENTS.md b/AGENTS.md index f9acfa9..dee1549 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,8 @@ checks.nix # deadnix, statix, NixOS VM integration test devshell.nix # Dev shell (fmt, update commands) modules/nixos/happier-server.nix # NixOS service module packages/prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries -examples/happier-server-light.nix # Light mode example (used by VM test) +examples/happier-server-tailscale.nix # Recommended production setup (Tailscale + TLS) +examples/happier-server-light.nix # Minimal config (used by CI VM test) ``` ## Language and tooling @@ -48,6 +49,8 @@ The flake uses [flake-parts](https://github.com/hercules-ci/flake-parts). Per-sy The module provisions supporting services (PostgreSQL, Redis, MinIO) when `createLocally = true` and handles migrations, WAL mode, and secret loading via `systemd` `LoadCredential`. +Both modes require an `environmentFile` containing `HANDY_MASTER_SECRET` for production use. The only exception is the CI VM test, which omits it. + ### Package builds Both `happier-server` and `happier-cli` are built from the happier monorepo source (`inputs.happier`). Prisma engines come from `packages/prisma-engines-prebuilt.nix` which fetches prebuilt binaries by hash. diff --git a/README.md b/README.md index bc3cc58..a589589 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,19 @@ Nix flake for building and deploying [Happier](https://github.com/happier-dev/ha Supported systems: `aarch64-darwin`, `aarch64-linux`, `x86_64-linux` +## Secrets + +All deployment modes require an **environment file** containing secrets. At minimum: + +```sh +HANDY_MASTER_SECRET= +``` + +Pass it to the module via `environmentFile`. Use [agenix](https://github.com/ryantm/agenix), [sops-nix](https://github.com/Mic92/sops-nix), or a plain file with restricted permissions — whatever fits your secrets workflow. + ## Quick start -Add the flake input to your NixOS configuration: +Add the flake input and import the NixOS module: ```nix { @@ -40,7 +50,8 @@ Add the flake input to your NixOS configuration: services.happier-server = { enable = true; package = nix-happier.packages.x86_64-linux.happier-server; - mode = "light"; # or "full" + mode = "light"; + environmentFile = "/run/secrets/happier-env"; }; } ]; @@ -49,9 +60,18 @@ Add the flake input to your NixOS configuration: } ``` -### Light mode (SQLite, no external deps) +### Recommended: Tailscale + nginx TLS -See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for a minimal configuration. Light mode uses SQLite with WAL enabled automatically — no PostgreSQL, Redis, or MinIO required. +The most common setup serves Happier over your Tailscale network with TLS termination via nginx. See [`examples/happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) for a complete configuration that includes: + +- Happier Server in light mode on `localhost:3005` +- Tailscale for private networking +- Auto-renewed TLS certs via `tailscale cert` +- nginx reverse proxy listening only on the Tailscale interface + +### Light mode (minimal) + +See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for the bare minimum. This is what the CI integration test uses — it omits `environmentFile` and networking, so it's useful as a starting point but not production-ready on its own. ### Full mode (PostgreSQL + Redis + MinIO) @@ -62,7 +82,7 @@ See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for package = nix-happier.packages.x86_64-linux.happier-server; mode = "full"; port = 3005; - environmentFile = "/run/secrets/happier-env"; # must contain HANDY_MASTER_SECRET + environmentFile = "/run/secrets/happier-env"; database = { name = "happier"; @@ -91,7 +111,7 @@ See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for | `package` | package | — | The `happier-server` package to use | | `port` | port | `3005` | Port to listen on | | `mode` | `"full"` \| `"light"` | `"full"` | `full` = PostgreSQL + Redis + MinIO; `light` = SQLite only | -| `environmentFile` | path \| null | `null` | Secrets file (`KEY=value`). Should contain `HANDY_MASTER_SECRET` | +| `environmentFile` | path \| null | `null` | **Required for production.** Secrets file (`KEY=value`) — must contain `HANDY_MASTER_SECRET` | ### Database (full mode) @@ -117,7 +137,12 @@ See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for ## Examples -The [`examples/`](examples/) directory contains ready-to-use NixOS configurations. These examples are tested in CI via `nix flake check` (the light-mode example runs as a NixOS VM integration test on Linux). +The [`examples/`](examples/) directory contains NixOS configurations: + +| Example | Description | +|---------|-------------| +| [`happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) | Recommended production setup — light mode + Tailscale + nginx TLS | +| [`happier-server-light.nix`](examples/happier-server-light.nix) | Bare minimum for CI — tested via `nix flake check` VM integration test | ## Development @@ -161,7 +186,8 @@ nix run .#update ├── packages/ │ └── prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries ├── examples/ -│ └── happier-server-light.nix # Light mode example config +│ ├── happier-server-tailscale.nix # Production setup with Tailscale + TLS +│ └── happier-server-light.nix # Minimal config (used by CI) └── .github/ └── workflows/ └── nix-build.yml # CI workflow diff --git a/examples/happier-server-light.nix b/examples/happier-server-light.nix index ca6e278..92b9683 100644 --- a/examples/happier-server-light.nix +++ b/examples/happier-server-light.nix @@ -1,7 +1,10 @@ -# Example: Happier Server in light mode (SQLite, no external dependencies) +# Minimal Happier Server config — light mode (SQLite, no external dependencies). # -# Minimal configuration — the consumer must set services.happier-server.package. -# In production, also set environmentFile for secrets (HANDY_MASTER_SECRET). +# Used by the CI integration test (nix flake check). The consumer must set +# services.happier-server.package externally. +# +# For a production-ready setup with TLS and Tailscale, see: +# examples/happier-server-tailscale.nix { services.happier-server = { enable = true; diff --git a/examples/happier-server-tailscale.nix b/examples/happier-server-tailscale.nix new file mode 100644 index 0000000..1c1ec8d --- /dev/null +++ b/examples/happier-server-tailscale.nix @@ -0,0 +1,112 @@ +# Example: Happier Server behind Tailscale with nginx TLS reverse proxy. +# +# This is the recommended production setup for light mode. It: +# - Runs happier-server on localhost:3005 (SQLite, no external deps) +# - Uses Tailscale for private networking (not exposed to the public internet) +# - Terminates TLS via nginx using auto-renewed Tailscale certs +# - Loads secrets (HANDY_MASTER_SECRET) from an environment file +# +# Prerequisites: +# - A Tailscale account and auth key +# - An environment file containing at minimum: +# HANDY_MASTER_SECRET= +# +# Adapt the tailnetDomain and listen address to your Tailscale network. + +{ pkgs, ... }: +let + # Replace with your machine's Tailscale FQDN + tailnetDomain = "happier.example.ts.net"; + certDir = "/var/lib/tailscale-certs"; +in +{ + # -- Happier Server (light mode) ------------------------------------------- + + services.happier-server = { + enable = true; + mode = "light"; + port = 3005; + # Required — must contain HANDY_MASTER_SECRET at minimum. + # Use agenix, sops-nix, or a plain file with restricted permissions. + environmentFile = "/run/secrets/happier-env"; + }; + + # -- Tailscale -------------------------------------------------------------- + + services.tailscale.enable = true; + + networking.firewall = { + trustedInterfaces = [ "tailscale0" ]; + allowedUDPPorts = [ 41641 ]; # Tailscale + }; + + # -- TLS certificates from Tailscale ---------------------------------------- + + systemd.services.tailscale-cert = { + description = "Generate Tailscale TLS certificates"; + after = [ "tailscaled.service" ]; + wants = [ "tailscaled.service" ]; + wantedBy = [ "multi-user.target" ]; + before = [ "nginx.service" ]; + path = [ pkgs.tailscale ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + mkdir -p ${certDir} + for i in $(seq 1 30); do + tailscale status && break + sleep 1 + done + tailscale cert \ + --cert-file ${certDir}/${tailnetDomain}.crt \ + --key-file ${certDir}/${tailnetDomain}.key \ + ${tailnetDomain} + chmod 640 ${certDir}/${tailnetDomain}.key + chown root:nginx ${certDir}/${tailnetDomain}.key + ''; + }; + + # Renew certs daily + systemd.timers.tailscale-cert = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + }; + }; + + # -- Nginx reverse proxy (Tailscale interface only) ------------------------- + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + + virtualHosts.${tailnetDomain} = { + # Listen only on the Tailscale interface — replace with your Tailscale IP. + listen = [ + { + addr = "100.x.y.z"; # <- your machine's Tailscale IP + port = 443; + ssl = true; + } + ]; + onlySSL = true; + sslCertificate = "${certDir}/${tailnetDomain}.crt"; + sslCertificateKey = "${certDir}/${tailnetDomain}.key"; + + locations."/" = { + proxyPass = "http://127.0.0.1:3005"; + proxyWebsockets = true; + }; + }; + }; + + # Ensure nginx starts after certs are generated + systemd.services.nginx = { + after = [ "tailscale-cert.service" ]; + requires = [ "tailscale-cert.service" ]; + }; +} From e10ede520ff6ca7cb2d4f60f6539af609d3c71a4 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 21:35:54 +0100 Subject: [PATCH 14/24] refactor(examples): replace nginx with Caddy for Tailscale TLS Caddy natively recognizes Tailscale URLs and auto-provisions TLS certs, eliminating the need for manual cert generation, timers, and permission fixups. This reduces the Tailscale example from ~60 lines to ~20. --- AGENTS.md | 2 +- README.md | 11 ++-- examples/happier-server-tailscale.nix | 87 ++++----------------------- 3 files changed, 17 insertions(+), 83 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dee1549..4cc2563 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ checks.nix # deadnix, statix, NixOS VM integration test devshell.nix # Dev shell (fmt, update commands) modules/nixos/happier-server.nix # NixOS service module packages/prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries -examples/happier-server-tailscale.nix # Recommended production setup (Tailscale + TLS) +examples/happier-server-tailscale.nix # Recommended production setup (Tailscale + Caddy) examples/happier-server-light.nix # Minimal config (used by CI VM test) ``` diff --git a/README.md b/README.md index a589589..65ece27 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,13 @@ Add the flake input and import the NixOS module: } ``` -### Recommended: Tailscale + nginx TLS +### Recommended: Tailscale + Caddy -The most common setup serves Happier over your Tailscale network with TLS termination via nginx. See [`examples/happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) for a complete configuration that includes: +The most common setup serves Happier over your Tailscale network with automatic TLS via Caddy. See [`examples/happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) for a complete configuration that includes: - Happier Server in light mode on `localhost:3005` - Tailscale for private networking -- Auto-renewed TLS certs via `tailscale cert` -- nginx reverse proxy listening only on the Tailscale interface +- Caddy reverse proxy with automatic TLS cert provisioning ### Light mode (minimal) @@ -141,7 +140,7 @@ The [`examples/`](examples/) directory contains NixOS configurations: | Example | Description | |---------|-------------| -| [`happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) | Recommended production setup — light mode + Tailscale + nginx TLS | +| [`happier-server-tailscale.nix`](examples/happier-server-tailscale.nix) | Recommended production setup — light mode + Tailscale + Caddy TLS | | [`happier-server-light.nix`](examples/happier-server-light.nix) | Bare minimum for CI — tested via `nix flake check` VM integration test | ## Development @@ -186,7 +185,7 @@ nix run .#update ├── packages/ │ └── prisma-engines-prebuilt.nix # Prebuilt Prisma engine binaries ├── examples/ -│ ├── happier-server-tailscale.nix # Production setup with Tailscale + TLS +│ ├── happier-server-tailscale.nix # Production setup with Tailscale + Caddy │ └── happier-server-light.nix # Minimal config (used by CI) └── .github/ └── workflows/ diff --git a/examples/happier-server-tailscale.nix b/examples/happier-server-tailscale.nix index 1c1ec8d..5c87e07 100644 --- a/examples/happier-server-tailscale.nix +++ b/examples/happier-server-tailscale.nix @@ -1,24 +1,18 @@ -# Example: Happier Server behind Tailscale with nginx TLS reverse proxy. +# Example: Happier Server behind Tailscale with Caddy TLS reverse proxy. # # This is the recommended production setup for light mode. It: # - Runs happier-server on localhost:3005 (SQLite, no external deps) # - Uses Tailscale for private networking (not exposed to the public internet) -# - Terminates TLS via nginx using auto-renewed Tailscale certs +# - Terminates TLS via Caddy, which auto-provisions certs from Tailscale # - Loads secrets (HANDY_MASTER_SECRET) from an environment file # # Prerequisites: -# - A Tailscale account and auth key +# - A Tailscale account with HTTPS enabled (admin console → DNS → Enable HTTPS) # - An environment file containing at minimum: # HANDY_MASTER_SECRET= # -# Adapt the tailnetDomain and listen address to your Tailscale network. +# Replace "happier.example.ts.net" with your machine's Tailscale FQDN. -{ pkgs, ... }: -let - # Replace with your machine's Tailscale FQDN - tailnetDomain = "happier.example.ts.net"; - certDir = "/var/lib/tailscale-certs"; -in { # -- Happier Server (light mode) ------------------------------------------- @@ -34,79 +28,20 @@ in # -- Tailscale -------------------------------------------------------------- services.tailscale.enable = true; + # Allow Caddy to provision TLS certs via Tailscale + services.tailscale.permitCertUid = "caddy"; networking.firewall = { trustedInterfaces = [ "tailscale0" ]; allowedUDPPorts = [ 41641 ]; # Tailscale }; - # -- TLS certificates from Tailscale ---------------------------------------- + # -- Caddy reverse proxy (auto TLS via Tailscale) --------------------------- - systemd.services.tailscale-cert = { - description = "Generate Tailscale TLS certificates"; - after = [ "tailscaled.service" ]; - wants = [ "tailscaled.service" ]; - wantedBy = [ "multi-user.target" ]; - before = [ "nginx.service" ]; - path = [ pkgs.tailscale ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - mkdir -p ${certDir} - for i in $(seq 1 30); do - tailscale status && break - sleep 1 - done - tailscale cert \ - --cert-file ${certDir}/${tailnetDomain}.crt \ - --key-file ${certDir}/${tailnetDomain}.key \ - ${tailnetDomain} - chmod 640 ${certDir}/${tailnetDomain}.key - chown root:nginx ${certDir}/${tailnetDomain}.key - ''; - }; - - # Renew certs daily - systemd.timers.tailscale-cert = { - wantedBy = [ "timers.target" ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - }; - }; - - # -- Nginx reverse proxy (Tailscale interface only) ------------------------- - - services.nginx = { + services.caddy = { enable = true; - recommendedTlsSettings = true; - recommendedProxySettings = true; - - virtualHosts.${tailnetDomain} = { - # Listen only on the Tailscale interface — replace with your Tailscale IP. - listen = [ - { - addr = "100.x.y.z"; # <- your machine's Tailscale IP - port = 443; - ssl = true; - } - ]; - onlySSL = true; - sslCertificate = "${certDir}/${tailnetDomain}.crt"; - sslCertificateKey = "${certDir}/${tailnetDomain}.key"; - - locations."/" = { - proxyPass = "http://127.0.0.1:3005"; - proxyWebsockets = true; - }; - }; - }; - - # Ensure nginx starts after certs are generated - systemd.services.nginx = { - after = [ "tailscale-cert.service" ]; - requires = [ "tailscale-cert.service" ]; + virtualHosts."happier.example.ts.net".extraConfig = '' + reverse_proxy localhost:3005 + ''; }; } From df299d761ce7eee5bd138b7eed46842548c3e9df Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Thu, 26 Feb 2026 22:20:54 +0100 Subject: [PATCH 15/24] docs: add backup responsibility note for full mode --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 65ece27..a492e18 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ See [`examples/happier-server-light.nix`](examples/happier-server-light.nix) for ### Full mode (PostgreSQL + Redis + MinIO) +With `createLocally = true` (the default), PostgreSQL, Redis, and MinIO are all provisioned on the same host as single-node instances. This is convenient for small deployments but comes with no built-in replication or backups — **you are responsible for setting up your own backup strategy** (e.g. `pgBackRest`, `restic`, or filesystem snapshots). + ```nix { services.happier-server = { From 94a05c818331c1b139c4078aa6dc948327225ef7 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Fri, 27 Feb 2026 17:14:24 +0100 Subject: [PATCH 16/24] ci: switch aarch64-linux to native ARM runner Replace QEMU user-space emulation with GitHub's native ubuntu-24.04-arm runner. The NixOS VM integration test is skipped on aarch64 since GitHub ARM runners lack KVM; it continues to run in the x86_64-linux job. --- .github/workflows/nix-build.yml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 5b3e4ec..4f70254 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -43,23 +43,25 @@ jobs: run: nix flake check nix-build-aarch64-linux: - name: Nix Build (aarch64-linux via QEMU) - runs-on: ubuntu-latest - timeout-minutes: 120 + name: Nix Build (aarch64-linux) + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU for aarch64 - run: | - sudo apt-get update - sudo apt-get install -y qemu-user-static binfmt-support - sudo update-binfmts --enable qemu-aarch64 - - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - with: - extra-conf: extra-platforms = aarch64-linux - - name: Nix flake check - run: nix flake check --system aarch64-linux + # KVM is not available on GitHub ARM runners, so the NixOS VM + # integration test cannot run here. Build packages and run linters + # only; the full VM test runs in the x86_64-linux job. + - name: Build packages + run: | + nix build .#packages.aarch64-linux.happier-server + nix build .#packages.aarch64-linux.happier-cli + + - name: Lint + run: | + nix build .#checks.aarch64-linux.deadnix + nix build .#checks.aarch64-linux.statix From 385000ca81752a845e7ccda23161970b60f7f86e Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Fri, 27 Feb 2026 19:51:24 +0100 Subject: [PATCH 17/24] docs(examples): add full mode example with Tailscale + Caddy Production-ready config showing PostgreSQL + Redis + MinIO with agenix secret management and Tailscale TLS via Caddy reverse proxy. --- examples/happier-server-full.nix | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/happier-server-full.nix diff --git a/examples/happier-server-full.nix b/examples/happier-server-full.nix new file mode 100644 index 0000000..3f7ce65 --- /dev/null +++ b/examples/happier-server-full.nix @@ -0,0 +1,58 @@ +# Example: Happier Server in full mode (PostgreSQL + Redis + MinIO). +# +# This is the recommended production setup. It: +# - Runs happier-server on localhost:3005 with the full backing stack +# - Provisions PostgreSQL, Redis, and MinIO locally (createLocally = true) +# - Uses Tailscale for private networking (not exposed to the public internet) +# - Terminates TLS via Caddy, which auto-provisions certs from Tailscale +# - Loads secrets via agenix (environmentFile + MinIO credentials) +# +# Prerequisites: +# - A Tailscale account with HTTPS enabled (admin console → DNS → Enable HTTPS) +# - An environment file containing at minimum: +# HANDY_MASTER_SECRET= +# - A MinIO credentials file containing: +# MINIO_ROOT_USER= +# MINIO_ROOT_PASSWORD= +# +# Replace "happier.example.ts.net" with your machine's Tailscale FQDN. + +{ config, ... }: +{ + # -- Happier Server (full mode) ---------------------------------------------- + + services.happier-server = { + enable = true; + port = 3005; + mode = "full"; + # Required — must contain HANDY_MASTER_SECRET at minimum. + # Use agenix, sops-nix, or a plain file with restricted permissions. + environmentFile = config.age.secrets.happier-env.path; + minio.rootCredentialsFile = config.age.secrets.minio-credentials.path; + }; + + # -- Secrets (agenix) -------------------------------------------------------- + + age.secrets.happier-env.file = ../secrets/happier-env.age; + age.secrets.minio-credentials.file = ../secrets/minio-credentials.age; + + # -- Tailscale ---------------------------------------------------------------- + + services.tailscale.enable = true; + # Allow Caddy to provision TLS certs via Tailscale + services.tailscale.permitCertUid = "caddy"; + + networking.firewall = { + trustedInterfaces = [ "tailscale0" ]; + allowedUDPPorts = [ 41641 ]; # Tailscale + }; + + # -- Caddy reverse proxy (auto TLS via Tailscale) ----------------------------- + + services.caddy = { + enable = true; + virtualHosts."happier.example.ts.net".extraConfig = '' + reverse_proxy localhost:3005 + ''; + }; +} From be7a27bdcb31304077c9198e1c61070a2c91943d Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Sat, 28 Feb 2026 00:15:35 +0100 Subject: [PATCH 18/24] feat: build and bundle web UI with happier-server Add happier-ui-web derivation that builds the Expo web bundle as a separate package (following the lldap/Immich pattern from nixpkgs). The server wrappers set HAPPIER_SERVER_UI_DIR so the web UI is served automatically. The UI is also exposed via passthru.web for overrides. --- packages.nix | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages.nix b/packages.nix index bdacae5..f54efb3 100644 --- a/packages.nix +++ b/packages.nix @@ -40,11 +40,88 @@ ); }; + # Source filter for the web UI: include apps/ui, exclude server/CLI apps + uiFilteredSrc = lib.cleanSourceWith { + src = happierSrc; + filter = + path: _type: + let + relPath = lib.removePrefix (toString happierSrc + "/") (toString path); + in + !( + lib.hasPrefix "apps/server" relPath + || lib.hasPrefix "apps/cli" relPath + || lib.hasPrefix "apps/stack" relPath + || lib.hasPrefix "apps/website" relPath + || lib.hasPrefix "apps/docs" relPath + || lib.hasPrefix "packages/relay-server" relPath + || lib.hasPrefix "packages/tests" relPath + || lib.hasPrefix ".git" relPath + || relPath == "node_modules" + || lib.hasPrefix "node_modules/" relPath + || relPath == "dist" + || lib.hasPrefix ".pgdata" relPath + || lib.hasPrefix ".minio" relPath + || lib.hasPrefix ".logs" relPath + || lib.hasPrefix "result" relPath + || lib.hasPrefix ".project" relPath + ); + }; + # Offline yarn cache from the root yarn.lock yarnOfflineCache = pkgs.fetchYarnDeps { yarnLock = "${happierSrc}/yarn.lock"; hash = "sha256-5SeMv0NQ0KbfHsSSO9k/jFhYxw77I1sBn0AxxQVpMjc="; }; + + # Pre-built web UI bundle (Expo static export) + happier-ui-web = pkgs.stdenv.mkDerivation { + pname = "happier-ui-web"; + version = "0.1.0"; + + src = uiFilteredSrc; + + nativeBuildInputs = with pkgs; [ + nodejs_22 + yarn + yarnConfigHook + ]; + + inherit yarnOfflineCache; + + preConfigure = '' + export HAPPIER_INSTALL_SCOPE=ui,protocol,agents + export HOME=$(mktemp -d) + export APP_ENV=production + export EXPO_NO_GIT_STATUS=1 + ''; + + buildPhase = '' + runHook preBuild + + # Build shared workspace packages in dependency order + node packages/protocol/scripts/generate-embedded-feature-policies.mjs + node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json + + # Export static web bundle (invoke via node to bypass shebang issues on linux builders) + (cd apps/ui && node node_modules/expo/bin/cli export --platform web --output-dir dist) + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + cp -r apps/ui/dist $out + runHook postInstall + ''; + + meta = { + description = "Happier Web UI - static Expo web bundle"; + homepage = "https://github.com/happier-dev/happier"; + license = lib.licenses.mit; + }; + }; in { packages = { @@ -307,6 +384,7 @@ --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --set HAPPIER_SERVER_UI_DIR "${happier-ui-web}" \ --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ --chdir "$out/lib/happier-server/apps/server" \ --prefix PATH : ${ @@ -325,6 +403,7 @@ --set PRISMA_QUERY_ENGINE_LIBRARY "${pkgs.prisma-engines}/lib/libquery_engine.node" \ --set PRISMA_SCHEMA_ENGINE_BINARY "${pkgs.prisma-engines}/bin/schema-engine" \ --set PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING "1" \ + --set HAPPIER_SERVER_UI_DIR "${happier-ui-web}" \ --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath [ pkgs.openssl ]}" \ --chdir "$out/lib/happier-server/apps/server" \ --prefix PATH : ${ @@ -372,6 +451,8 @@ runHook postInstall ''; + passthru.web = happier-ui-web; + meta = { description = "Happier Server - backend for Happier mobile and CLI clients"; homepage = "https://github.com/happier-dev/happier"; From dcf0f17a94c53de72e53dd82f0bd650b4dce3c6c Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Mon, 2 Mar 2026 23:23:23 +0100 Subject: [PATCH 19/24] chore: switch happier input to preview branch --- flake.lock | 7 ++++--- flake.nix | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 08e93ed..db4c93d 100644 --- a/flake.lock +++ b/flake.lock @@ -41,15 +41,16 @@ "happier": { "flake": false, "locked": { - "lastModified": 1772114292, - "narHash": "sha256-nSF/bbZKakatNl2JSQS9WV3r+Mgtgvl5mVfj+GFYWbs=", + "lastModified": 1772259841, + "narHash": "sha256-ryQuh4QDBbesjv3OG/4ASlfmMZDGStVYL7yrQN0M6Ig=", "owner": "happier-dev", "repo": "happier", - "rev": "e7a17e05528bc787f09267a8019816c113025eb2", + "rev": "1037536329cc13dbad12d896169fb7dc84c12337", "type": "github" }, "original": { "owner": "happier-dev", + "ref": "preview", "repo": "happier", "type": "github" } diff --git a/flake.nix b/flake.nix index 95ed7da..ce3f7a6 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ # The happier monorepo source (fetched as a plain source tree, not evaluated as a flake) happier = { - url = "github:happier-dev/happier"; + url = "github:happier-dev/happier/preview"; flake = false; }; }; From 111c3ad9d5e2a4c409fa7b0c914c796c7920d6ba Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Wed, 8 Apr 2026 11:59:05 +0200 Subject: [PATCH 20/24] track happier dev branch, update flake inputs --- flake.lock | 26 +++++++++++++------------- flake.nix | 2 +- packages.nix | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flake.lock b/flake.lock index db4c93d..6dab48a 100644 --- a/flake.lock +++ b/flake.lock @@ -25,11 +25,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1769996383, - "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -41,27 +41,27 @@ "happier": { "flake": false, "locked": { - "lastModified": 1772259841, - "narHash": "sha256-ryQuh4QDBbesjv3OG/4ASlfmMZDGStVYL7yrQN0M6Ig=", + "lastModified": 1775533951, + "narHash": "sha256-gCrzhcB5ZzydXx9t7hxqO2ST1O2xgkySiUzc0+QPjbU=", "owner": "happier-dev", "repo": "happier", - "rev": "1037536329cc13dbad12d896169fb7dc84c12337", + "rev": "bad71f84f5d6214ed4abfd3d635861d07a3c72ba", "type": "github" }, "original": { "owner": "happier-dev", - "ref": "preview", + "ref": "dev", "repo": "happier", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1771903837, - "narHash": "sha256-sdaqdnsQCv3iifzxwB22tUwN/fSHoN7j2myFW5EIkGk=", + "lastModified": 1775305101, + "narHash": "sha256-/74n1oQPtKG52Yw41cbToxspxHbYz6O3vi+XEw16Qe8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951", + "rev": "36a601196c4ebf49e035270e10b2d103fe39076b", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1769909678, - "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "72716169fe93074c333e8d0173151350670b824c", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ce3f7a6..4fe697b 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ # The happier monorepo source (fetched as a plain source tree, not evaluated as a flake) happier = { - url = "github:happier-dev/happier/preview"; + url = "github:happier-dev/happier/dev"; flake = false; }; }; diff --git a/packages.nix b/packages.nix index f54efb3..6b180c1 100644 --- a/packages.nix +++ b/packages.nix @@ -71,7 +71,7 @@ # Offline yarn cache from the root yarn.lock yarnOfflineCache = pkgs.fetchYarnDeps { yarnLock = "${happierSrc}/yarn.lock"; - hash = "sha256-5SeMv0NQ0KbfHsSSO9k/jFhYxw77I1sBn0AxxQVpMjc="; + hash = "sha256-p2eG1eRiy/HjWDZ6lNgdzy9xZEo6NGXCFi7Vj1uaBX0="; }; # Pre-built web UI bundle (Expo static export) From 39417429c3871ff9bb61cf98b20dadc2346d2d6f Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Wed, 8 Apr 2026 19:36:33 +0200 Subject: [PATCH 21/24] fix: add new workspace packages for happier dev branch The dev branch introduced transfers, connection-supervisor, and release-runtime as dependencies of cli-common and apps/cli. Fix the build order (release-runtime before cli-common) and add build + install steps for the two new packages. --- packages.nix | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages.nix b/packages.nix index 6b180c1..77889eb 100644 --- a/packages.nix +++ b/packages.nix @@ -152,13 +152,15 @@ runHook preBuild # Build shared workspace packages in dependency order: - # protocol (no deps) -> agents (needs protocol) -> cli-common (needs agents) -> release-runtime (no internal deps) + # protocol (no deps) -> agents, release-runtime, transfers, connection-supervisor (need protocol at most) -> cli-common (needs agents + release-runtime) # Protocol needs its codegen step first node packages/protocol/scripts/generate-embedded-feature-policies.mjs node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json - node node_modules/typescript/bin/tsc -p packages/cli-common/tsconfig.json node node_modules/typescript/bin/tsc -p packages/release-runtime/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/transfers/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/connection-supervisor/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/cli-common/tsconfig.json # Sync bundled workspace dist into CLI's node_modules so tsc/pkgroll can resolve them node -e " @@ -184,6 +186,8 @@ mkdir -p $out/lib/happier-cli/packages/agents mkdir -p $out/lib/happier-cli/packages/cli-common mkdir -p $out/lib/happier-cli/packages/release-runtime + mkdir -p $out/lib/happier-cli/packages/transfers + mkdir -p $out/lib/happier-cli/packages/connection-supervisor # Root node_modules (hoisted dependencies) cp -r node_modules $out/lib/happier-cli/ @@ -228,6 +232,20 @@ cp -r packages/release-runtime/node_modules $out/lib/happier-cli/packages/release-runtime/ fi + # -- packages/transfers -- + cp -r packages/transfers/dist $out/lib/happier-cli/packages/transfers/ + cp packages/transfers/package.json $out/lib/happier-cli/packages/transfers/ + if [ -d packages/transfers/node_modules ]; then + cp -r packages/transfers/node_modules $out/lib/happier-cli/packages/transfers/ + fi + + # -- packages/connection-supervisor -- + cp -r packages/connection-supervisor/dist $out/lib/happier-cli/packages/connection-supervisor/ + cp packages/connection-supervisor/package.json $out/lib/happier-cli/packages/connection-supervisor/ + if [ -d packages/connection-supervisor/node_modules ]; then + cp -r packages/connection-supervisor/node_modules $out/lib/happier-cli/packages/connection-supervisor/ + fi + # Create wrapper scripts mkdir -p $out/bin From 77be10a484c1a0db7aed2e3af4d6733e5be4f607 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Sat, 18 Apr 2026 09:31:34 +0200 Subject: [PATCH 22/24] bump happier input --- flake.lock | 6 +++--- packages.nix | 30 ++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 6dab48a..1fd731d 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ "happier": { "flake": false, "locked": { - "lastModified": 1775533951, - "narHash": "sha256-gCrzhcB5ZzydXx9t7hxqO2ST1O2xgkySiUzc0+QPjbU=", + "lastModified": 1776496788, + "narHash": "sha256-zVcfx9Z0jcUKnP2IrQhgDj49iA0ify+zOxHH+VqHIfY=", "owner": "happier-dev", "repo": "happier", - "rev": "bad71f84f5d6214ed4abfd3d635861d07a3c72ba", + "rev": "80ff370eca70753aad184ec364c2d62b86185032", "type": "github" }, "original": { diff --git a/packages.nix b/packages.nix index 77889eb..1ad6592 100644 --- a/packages.nix +++ b/packages.nix @@ -71,7 +71,7 @@ # Offline yarn cache from the root yarn.lock yarnOfflineCache = pkgs.fetchYarnDeps { yarnLock = "${happierSrc}/yarn.lock"; - hash = "sha256-p2eG1eRiy/HjWDZ6lNgdzy9xZEo6NGXCFi7Vj1uaBX0="; + hash = "sha256-kph4Y7WtP7lXLWwg3NJu4ifHCCvCQA+sKORSj7v6PFE="; }; # Pre-built web UI bundle (Expo static export) @@ -90,7 +90,7 @@ inherit yarnOfflineCache; preConfigure = '' - export HAPPIER_INSTALL_SCOPE=ui,protocol,agents + export HAPPIER_INSTALL_SCOPE=ui,protocol,agents,connection-supervisor,transfers,release-runtime,cli-common export HOME=$(mktemp -d) export APP_ENV=production export EXPO_NO_GIT_STATUS=1 @@ -103,6 +103,10 @@ node packages/protocol/scripts/generate-embedded-feature-policies.mjs node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/release-runtime/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/transfers/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/connection-supervisor/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/cli-common/tsconfig.json # Export static web bundle (invoke via node to bypass shebang issues on linux builders) (cd apps/ui && node node_modules/expo/bin/cli export --platform web --output-dir dist) @@ -308,7 +312,7 @@ preConfigure = '' # Skip CLI postinstall (only need server scope) - export HAPPIER_INSTALL_SCOPE=server + export HAPPIER_INSTALL_SCOPE=server,cli-common,release-runtime export HOME=$(mktemp -d) # Point Prisma at nixpkgs engines @@ -321,10 +325,12 @@ runHook preBuild # Build shared workspace packages in dependency order: - # protocol (no deps) -> agents (needs protocol) + # protocol (no deps) -> agents (needs protocol) -> release-runtime, cli-common (needs agents + release-runtime) node packages/protocol/scripts/generate-embedded-feature-policies.mjs node node_modules/typescript/bin/tsc -p packages/protocol/tsconfig.json node node_modules/typescript/bin/tsc -p packages/agents/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/release-runtime/tsconfig.json + node node_modules/typescript/bin/tsc -p packages/cli-common/tsconfig.json # Generate Prisma clients for all providers (postgres, mysql, sqlite) # generate:providers handles schema:sync internally and generates all three @@ -346,6 +352,8 @@ mkdir -p $out/lib/happier-server/apps/server mkdir -p $out/lib/happier-server/packages/protocol mkdir -p $out/lib/happier-server/packages/agents + mkdir -p $out/lib/happier-server/packages/release-runtime + mkdir -p $out/lib/happier-server/packages/cli-common # Root node_modules (hoisted dependencies) cp -r node_modules $out/lib/happier-server/ @@ -367,6 +375,20 @@ cp -r packages/agents/node_modules $out/lib/happier-server/packages/agents/ fi + # -- packages/release-runtime -- + cp -r packages/release-runtime/dist $out/lib/happier-server/packages/release-runtime/ + cp packages/release-runtime/package.json $out/lib/happier-server/packages/release-runtime/ + if [ -d packages/release-runtime/node_modules ]; then + cp -r packages/release-runtime/node_modules $out/lib/happier-server/packages/release-runtime/ + fi + + # -- packages/cli-common -- + cp -r packages/cli-common/dist $out/lib/happier-server/packages/cli-common/ + cp packages/cli-common/package.json $out/lib/happier-server/packages/cli-common/ + if [ -d packages/cli-common/node_modules ]; then + cp -r packages/cli-common/node_modules $out/lib/happier-server/packages/cli-common/ + fi + # -- apps/server sources and config -- cp -r apps/server/sources $out/lib/happier-server/apps/server/ cp -r apps/server/prisma $out/lib/happier-server/apps/server/ From 6415421fb55353b438c9e9c85626bad85a1c8984 Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Tue, 21 Apr 2026 09:32:42 +0200 Subject: [PATCH 23/24] bump happier input --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 1fd731d..270a6e4 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ "happier": { "flake": false, "locked": { - "lastModified": 1776496788, - "narHash": "sha256-zVcfx9Z0jcUKnP2IrQhgDj49iA0ify+zOxHH+VqHIfY=", + "lastModified": 1776627034, + "narHash": "sha256-43/AthzVHSeuNqoqfRu3qwdaOIwYSlu3johoRDa2vSU=", "owner": "happier-dev", "repo": "happier", - "rev": "80ff370eca70753aad184ec364c2d62b86185032", + "rev": "326b10887227087c54d7d9088351ba45897343e5", "type": "github" }, "original": { From 3e03c2e1f9eb9019e3646602e887e2db040e527a Mon Sep 17 00:00:00 2001 From: Simon Wasle Date: Tue, 21 Apr 2026 10:57:37 +0200 Subject: [PATCH 24/24] feat: allow overriding default server URL for happier-cli Self-hosted users can now point the CLI at their own server instead of the upstream cloud API. The wrapper bakes HAPPIER_SERVER_URL/WEBAPP_URL env vars into the binary; CLI args (--server-url) still take precedence. --- packages.nix | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages.nix b/packages.nix index 1ad6592..0efc4ea 100644 --- a/packages.nix +++ b/packages.nix @@ -126,11 +126,24 @@ license = lib.licenses.mit; }; }; - in - { - packages = { - # -- happier-cli (CLI) ------------------------------------------------- - happier-cli = pkgs.stdenv.mkDerivation { + + # Builder function for happier-cli — allows overriding the default server URL. + # CLI args (--server-url) still take precedence over these env vars. + # + # Example — point CLI at a self-hosted server: + # + # environment.systemPackages = [ + # (nix-happier.packages.${system}.happier-cli.override { + # serverUrl = "https://happier.myhost.com"; + # webappUrl = "https://happier.myhost.com"; + # }) + # ]; + mkHappierCli = + { + serverUrl ? null, + webappUrl ? null, + }: + pkgs.stdenv.mkDerivation { pname = "happier-cli"; version = "0.1.0"; @@ -257,6 +270,8 @@ --add-flags "--no-warnings" \ --add-flags "--no-deprecation" \ --add-flags "$out/lib/happier-cli/apps/cli/dist/index.mjs" \ + ${lib.optionalString (serverUrl != null) ''--set HAPPIER_SERVER_URL "${serverUrl}"''} \ + ${lib.optionalString (webappUrl != null) ''--set HAPPIER_WEBAPP_URL "${webappUrl}"''} \ --prefix PATH : ${ lib.makeBinPath [ pkgs.nodejs_22 @@ -269,6 +284,8 @@ --add-flags "--no-warnings" \ --add-flags "--no-deprecation" \ --add-flags "$out/lib/happier-cli/apps/cli/dist/backends/codex/happyMcpStdioBridge.mjs" \ + ${lib.optionalString (serverUrl != null) ''--set HAPPIER_SERVER_URL "${serverUrl}"''} \ + ${lib.optionalString (webappUrl != null) ''--set HAPPIER_WEBAPP_URL "${webappUrl}"''} \ --prefix PATH : ${ lib.makeBinPath [ pkgs.nodejs_22 @@ -280,6 +297,8 @@ runHook postInstall ''; + passthru.override = mkHappierCli; + meta = { description = "Happier CLI - mobile and web client for Claude Code"; homepage = "https://github.com/happier-dev/happier"; @@ -287,6 +306,13 @@ mainProgram = "happier"; }; }; + in + { + packages = { + # Default CLI (no server URL override — uses upstream defaults). + # Use happier-cli.override to set a default server URL: + # happier-cli.override { serverUrl = "https://happier.myhost.com"; } + happier-cli = mkHappierCli { }; # -- happier-server ---------------------------------------------------- happier-server = pkgs.stdenv.mkDerivation {