{ lib, stdenv, fetchFromGitHub, bun, nodejs, node-gyp, electron, autoPatchelfHook, makeWrapper, copyDesktopItems, imagemagick, python3, cacert, }: let version = "2.1.4"; src = fetchFromGitHub { owner = "iOfficeAI"; repo = "AionUi"; rev = "v${version}"; hash = "sha256-A7dKGEuo5n+M7D9fJR3TN95q1P7En/ulsTO+ev6SAUY="; }; # FOD 1: All dependencies (dev + prod) for the build phase. bunBuildDeps = stdenv.mkDerivation { name = "aionui-build-deps-${version}"; inherit src; nativeBuildInputs = [ bun cacert ]; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "sha256-Fuj55vxDgpx5A4iwnU/5IZKPfaYMH3DeZ1yauuZHn8g="; dontPatchShebangs = true; dontFixup = true; SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; buildPhase = '' export HOME=$TMPDIR bun install --frozen-lockfile --ignore-scripts ''; installPhase = '' mkdir -p $out cp -r node_modules $out/ cp -r packages $out/ cp tsconfig.json $out/ ''; }; # FOD 2: Production-only dependencies for the runtime output. # Mirrors the Dockerfile's `bun install --production --ignore-scripts`. bunProdDeps = stdenv.mkDerivation { name = "aionui-prod-deps-${version}"; inherit src; nativeBuildInputs = [ bun cacert ]; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "sha256-eCQ9ObhttZ10CehRRvl4IaOKkdK9m1TQJuhLmhiMkBY="; dontPatchShebangs = true; dontFixup = true; SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt"; buildPhase = '' export HOME=$TMPDIR bun install --frozen-lockfile --production --ignore-scripts ''; installPhase = '' mkdir -p $out cp -r node_modules $out/ ''; }; in stdenv.mkDerivation rec { pname = "aionui"; inherit version src; nativeBuildInputs = [ bun nodejs node-gyp autoPatchelfHook makeWrapper copyDesktopItems imagemagick python3 ]; buildInputs = [ stdenv.cc.cc.lib ]; buildPhase = '' runHook preBuild export HOME=$TMPDIR # Use the full dependency set for the build ln -sf ${bunBuildDeps}/node_modules . # ── Build better-sqlite3 native addon ────────────────────── echo "Building better-sqlite3 from source..." BT3_SRC=$TMPDIR/bts3-src BT3_OUT=$TMPDIR/bts3-out mkdir -p "$BT3_SRC" "$BT3_OUT" cp -rL node_modules/better-sqlite3/* "$BT3_SRC/" chmod -R u+w "$BT3_SRC" ( cd "$BT3_SRC" export npm_config_nodedir=${nodejs} node-gyp rebuild mkdir -p "$BT3_OUT/build/Release" cp build/Release/better_sqlite3.node "$BT3_OUT/build/Release/" 2>/dev/null || true ) # ── electron-vite build ─────────────────────────────────── echo "Running electron-vite build..." ${nodejs}/bin/node node_modules/.bin/electron-vite build \ --config packages/desktop/electron.vite.config.ts # ── Bundle MCP servers ──────────────────────────────────── echo "Building MCP servers..." ${nodejs}/bin/node scripts/build-mcp-servers.js runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out/{bin,lib/aionui} # ── Built output ────────────────────────────────────────── cp -r out $out/lib/aionui/ cp -r public $out/lib/aionui/ cp package.json $out/lib/aionui/ # ── Production-only node_modules (mirrors Dockerfile) ───── cp -a ${bunProdDeps}/node_modules $out/lib/aionui/node_modules chmod -R u+w $out/lib/aionui/node_modules # Remove workspace symlink – @aionui/web-host is bundled by # electron-vite (excluded from externalizeDepsPlugin), so the # symlink to packages/web-host is not needed at runtime. rm -f $out/lib/aionui/node_modules/@aionui/web-host # @sentry/electron requires @sentry/node at runtime, but bun # nests it inside .bun/ rather than hoisting to top-level. # Electron's require() walks up from the resolved realpath # and needs it accessible either at top-level or via the # symlink target's node_modules. We ensure top-level access. if [ ! -e $out/lib/aionui/node_modules/@sentry/node ]; then SRC=$(echo $out/lib/aionui/node_modules/.bun/@sentry+node@*/node_modules/@sentry/node) if [ -d "$SRC" ]; then ln -sf "$(realpath --relative-to=$out/lib/aionui/node_modules/@sentry "$SRC")" \ $out/lib/aionui/node_modules/@sentry/node fi fi # Inject compiled better-sqlite3 .node into the runtime tree if [ -f "$TMPDIR/bts3-out/build/Release/better_sqlite3.node" ]; then rm -rf $out/lib/aionui/node_modules/better-sqlite3 cp -rL ${bunProdDeps}/node_modules/better-sqlite3 $out/lib/aionui/node_modules/better-sqlite3 2>/dev/null || true chmod -R u+w $out/lib/aionui/node_modules/better-sqlite3 mkdir -p $out/lib/aionui/node_modules/better-sqlite3/build/Release cp "$TMPDIR/bts3-out/build/Release/better_sqlite3.node" \ $out/lib/aionui/node_modules/better-sqlite3/build/Release/ fi # ── Desktop entry ───────────────────────────────────────── mkdir -p $out/share/applications cat > $out/share/applications/aionui.desktop << EOF [Desktop Entry] Name=AionUi Comment=Free, open-source Cowork app with AI Agents Exec=$out/bin/aionui Icon=aionui Terminal=false Type=Application Categories=Office;Utility; EOF mkdir -p $out/share/icons/hicolor/256x256/apps if [ -f resources/app.png ]; then convert resources/app.png -resize 256x256 \ $out/share/icons/hicolor/256x256/apps/aionui.png fi # ── Launcher ────────────────────────────────────────────── makeWrapper ${electron}/bin/electron $out/bin/aionui \ --add-flags "$out/lib/aionui" runHook postInstall ''; dontStrip = true; autoPatchelfIgnoreMissingDeps = true; desktopItems = [ "aionui.desktop" ]; doCheck = false; passthru = { category = "AI Coding Agents"; updateScript = [ "nix-update" "--flake" ".#aionui" ]; }; meta = with lib; { description = "Free, open-source, Cowork app with AI Agents"; homepage = "https://github.com/iOfficeAI/AionUi"; changelog = "https://github.com/iOfficeAI/AionUi/releases/tag/v${version}"; license = licenses.asl20; sourceProvenance = with sourceTypes; [ fromSource ]; mainProgram = "aionui"; platforms = platforms.linux; }; }