feat: add nix packaging (#480)
* feat: add Nix flake and module for hermes-workspace deployment * chore: modernize Nix build by migrating to generic nodejs/pnpm packages and adding direnv integration
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
|
# Nix build outputs
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
.direnv/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
|
|||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778869304,
|
||||||
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
119
flake.nix
Normal file
119
flake.nix
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
description = "Hermes Workspace — desktop workspace for Hermes Agent";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# NixOS module — available on all systems
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
nixosModules.default = import ./nix/module.nix;
|
||||||
|
nixosModules.hermes-workspace = nixosModules.default;
|
||||||
|
|
||||||
|
# Overlay that adds hermes-workspace into any nixpkgs instance
|
||||||
|
overlays.default = final: _prev: {
|
||||||
|
hermes-workspace = final.callPackage ./nix/package.nix { };
|
||||||
|
};
|
||||||
|
overlays.hermes-workspace = overlays.default;
|
||||||
|
in
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Per-system outputs
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ overlays.default ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Packages
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
packages = {
|
||||||
|
default = pkgs.hermes-workspace;
|
||||||
|
hermes-workspace = pkgs.hermes-workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Apps (nix run . or nix run .#hermes-workspace)
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
apps =
|
||||||
|
let
|
||||||
|
app = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgs.hermes-workspace}/bin/hermes-workspace";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = app;
|
||||||
|
hermes-workspace = app;
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Dev shell (nix develop)
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "hermes-workspace-dev";
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
# Node / JS toolchain
|
||||||
|
nodejs
|
||||||
|
pnpm
|
||||||
|
typescript
|
||||||
|
|
||||||
|
# Python for pty-helper and build scripts
|
||||||
|
python3
|
||||||
|
|
||||||
|
# Nix tooling
|
||||||
|
nil # Nix LSP
|
||||||
|
nixfmt-rfc-style
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo ""
|
||||||
|
echo " 🚀 hermes-workspace dev shell"
|
||||||
|
echo " node $(node --version)"
|
||||||
|
echo " pnpm $(pnpm --version)"
|
||||||
|
echo " python $(python3 --version)"
|
||||||
|
echo ""
|
||||||
|
echo " Quick start:"
|
||||||
|
echo " pnpm install"
|
||||||
|
echo " pnpm dev # Vite dev server on :3000"
|
||||||
|
echo " pnpm build # Production build → dist/"
|
||||||
|
echo " node server-entry.js # Serve production build"
|
||||||
|
echo ""
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Formatter (nix fmt)
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
formatter = pkgs.nixfmt-rfc-style;
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Checks (nix flake check)
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
checks = {
|
||||||
|
# Verify the package evaluates without building it
|
||||||
|
package-eval = pkgs.runCommand "hermes-workspace-pkg-eval" { } ''
|
||||||
|
echo "Package evaluated: ${pkgs.hermes-workspace.name}" > $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// {
|
||||||
|
# Expose module + overlay at the top level (system-agnostic)
|
||||||
|
inherit nixosModules overlays;
|
||||||
|
};
|
||||||
|
}
|
||||||
241
nix/module.nix
Normal file
241
nix/module.nix
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# NixOS module: services.hermes-workspace
|
||||||
|
#
|
||||||
|
# Runs the hermes-workspace web server as a systemd service.
|
||||||
|
# The companion hermes-agent gateway must be running separately
|
||||||
|
# (see https://github.com/NousResearch/hermes-agent).
|
||||||
|
#
|
||||||
|
# Minimal NixOS configuration example:
|
||||||
|
#
|
||||||
|
# services.hermes-workspace = {
|
||||||
|
# enable = true;
|
||||||
|
# hermesApiUrl = "http://127.0.0.1:8642";
|
||||||
|
# # For remote access, set a password and open the port:
|
||||||
|
# host = "0.0.0.0";
|
||||||
|
# passwordFile = config.sops.secrets."hermes-workspace-password".path;
|
||||||
|
# };
|
||||||
|
# networking.firewall.allowedTCPPorts = [ 3000 ];
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.hermes-workspace;
|
||||||
|
inherit (lib)
|
||||||
|
mkEnableOption
|
||||||
|
mkIf
|
||||||
|
mkOption
|
||||||
|
mkPackageOption
|
||||||
|
types
|
||||||
|
;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.hermes-workspace = {
|
||||||
|
enable = mkEnableOption "Hermes Workspace — web UI for Hermes Agent";
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "hermes-workspace" { };
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3000;
|
||||||
|
description = "TCP port the workspace server listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = ''
|
||||||
|
Address to bind the HTTP server to.
|
||||||
|
Set to "0.0.0.0" to expose on all interfaces (requires passwordFile
|
||||||
|
or allowInsecureRemote = true).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hermesApiUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "http://127.0.0.1:8642";
|
||||||
|
description = ''
|
||||||
|
URL of the Hermes Agent gateway HTTP API.
|
||||||
|
Requires API_SERVER_ENABLED=true in the gateway's environment.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hermesDashboardUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "http://127.0.0.1:9119";
|
||||||
|
description = "URL of the Hermes Agent dashboard.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Path to a file whose first line is the workspace session password.
|
||||||
|
Required when host is not a loopback address.
|
||||||
|
Use a secrets manager (sops-nix, agenix, etc.) to manage this file.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
cookieSecure = mkOption {
|
||||||
|
type = types.nullOr types.bool;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Override the Secure flag on session cookies.
|
||||||
|
null means "auto" (enabled in production mode).
|
||||||
|
Set to false for plain-HTTP LAN deployments behind a proxy.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
trustProxy = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Trust X-Forwarded-For / X-Real-IP headers from a reverse proxy.
|
||||||
|
Only enable when the server is behind a trusted proxy (Nginx, Traefik, etc.).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
allowInsecureRemote = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Allow binding to non-loopback addresses without a password.
|
||||||
|
NOT recommended — only use behind a custom auth layer.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hermesWorldEnabled = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Show the HermesWorld multiplayer link in the sidebar.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraEnvironment = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
STREAM_ACCEPTED_TIMEOUT_MS = "120000";
|
||||||
|
VITE_PLAYGROUND_WS_URL = "wss://my-hub.example.com/playground";
|
||||||
|
};
|
||||||
|
description = "Extra environment variables passed to the service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Path to a file containing additional environment variables
|
||||||
|
(KEY=value, one per line). Useful for secrets not covered by
|
||||||
|
the structured options above.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "hermes-workspace";
|
||||||
|
description = "System user to run the service as.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "hermes-workspace";
|
||||||
|
description = "System group to run the service as.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/hermes-workspace";
|
||||||
|
description = ''
|
||||||
|
State directory for the workspace (sessions, runtime data).
|
||||||
|
The service user must have write access.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users.users.${cfg.user} = lib.mkDefault {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = true;
|
||||||
|
description = "Hermes Workspace service user";
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${cfg.group} = lib.mkDefault { };
|
||||||
|
|
||||||
|
systemd.services.hermes-workspace = {
|
||||||
|
description = "Hermes Workspace Web Server";
|
||||||
|
documentation = [ "https://github.com/outsourc-e/hermes-workspace" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
environment =
|
||||||
|
{
|
||||||
|
NODE_ENV = "production";
|
||||||
|
PORT = toString cfg.port;
|
||||||
|
HOST = cfg.host;
|
||||||
|
HERMES_API_URL = cfg.hermesApiUrl;
|
||||||
|
HERMES_DASHBOARD_URL = cfg.hermesDashboardUrl;
|
||||||
|
VITE_HERMESWORLD_ENABLED = if cfg.hermesWorldEnabled then "1" else "0";
|
||||||
|
TRUST_PROXY = if cfg.trustProxy then "1" else "0";
|
||||||
|
HERMES_ALLOW_INSECURE_REMOTE = if cfg.allowInsecureRemote then "1" else "0";
|
||||||
|
# Point HOME to the data dir so session files land there
|
||||||
|
HOME = cfg.dataDir;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.cookieSecure != null) {
|
||||||
|
COOKIE_SECURE = if cfg.cookieSecure then "1" else "0";
|
||||||
|
}
|
||||||
|
// cfg.extraEnvironment;
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
|
||||||
|
ExecStart = "${lib.getExe cfg.package}";
|
||||||
|
|
||||||
|
# Load the password file as an env file when specified.
|
||||||
|
# The file must contain: HERMES_PASSWORD=<value>
|
||||||
|
EnvironmentFile = lib.optional (cfg.passwordFile != null) cfg.passwordFile
|
||||||
|
++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
|
||||||
|
|
||||||
|
# Restart on failure with backoff
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
StartLimitIntervalSec = "120";
|
||||||
|
StartLimitBurst = "5";
|
||||||
|
|
||||||
|
# Runtime directories
|
||||||
|
RuntimeDirectory = "hermes-workspace";
|
||||||
|
StateDirectory = lib.removePrefix "/var/lib/" cfg.dataDir;
|
||||||
|
LogsDirectory = "hermes-workspace";
|
||||||
|
|
||||||
|
# Security hardening (balanced against PTY + terminal needs)
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
# PTY helper needs /dev/ptmx and /dev/pts
|
||||||
|
PrivateDevices = false;
|
||||||
|
DeviceAllow = [
|
||||||
|
"/dev/ptmx rw"
|
||||||
|
"char-pts rw"
|
||||||
|
];
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = false; # Node.js JIT requires this off
|
||||||
|
SystemCallFilter = "@system-service";
|
||||||
|
SystemCallErrorNumber = "EPERM";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
106
nix/package.nix
Normal file
106
nix/package.nix
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenv,
|
||||||
|
nodejs,
|
||||||
|
pnpm,
|
||||||
|
fetchPnpmDeps,
|
||||||
|
pnpmConfigHook,
|
||||||
|
python3,
|
||||||
|
makeWrapper,
|
||||||
|
}:
|
||||||
|
|
||||||
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
|
pname = "hermes-workspace";
|
||||||
|
version = "2.3.0";
|
||||||
|
|
||||||
|
src = lib.cleanSourceWith {
|
||||||
|
src = ../.;
|
||||||
|
filter = name: type:
|
||||||
|
let
|
||||||
|
baseName = builtins.baseNameOf name;
|
||||||
|
relPath = lib.removePrefix (toString ../.) name;
|
||||||
|
in
|
||||||
|
# Exclude dirs that don't affect the build
|
||||||
|
!(lib.hasPrefix "/.git" relPath)
|
||||||
|
&& !(lib.hasPrefix "/node_modules" relPath)
|
||||||
|
&& !(lib.hasPrefix "/dist" relPath)
|
||||||
|
&& !(lib.hasPrefix "/.output" relPath)
|
||||||
|
&& !(lib.hasPrefix "/.tanstack" relPath)
|
||||||
|
&& !(lib.hasPrefix "/.vinxi" relPath)
|
||||||
|
&& !(lib.hasPrefix "/release" relPath)
|
||||||
|
&& !(lib.hasPrefix "/electron/server-bundle.cjs" relPath)
|
||||||
|
&& !(lib.hasPrefix "/memory" relPath)
|
||||||
|
&& !(lib.hasPrefix "/screenshots" relPath)
|
||||||
|
&& baseName != ".env"
|
||||||
|
&& baseName != ".env.local";
|
||||||
|
};
|
||||||
|
|
||||||
|
pnpmDeps = fetchPnpmDeps {
|
||||||
|
inherit (finalAttrs) pname version src;
|
||||||
|
pnpm = pnpm; # Ensure fetcher uses the same pnpm binary as the build
|
||||||
|
fetcherVersion = 3;
|
||||||
|
hash = "sha256-cgK1/KQkA9zOb1Zn5/OjV9qTXQEIVBaTWldbCbdRULs=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
nodejs
|
||||||
|
pnpm # provides the pnpm binary used by pnpmConfigHook
|
||||||
|
pnpmConfigHook
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [ python3 ];
|
||||||
|
|
||||||
|
# Give the build plenty of memory — same as the package.json script
|
||||||
|
NODE_OPTIONS = "--max-old-space-size=2048";
|
||||||
|
|
||||||
|
# Vite / TanStack Start require NODE_ENV=production for the SSR build so
|
||||||
|
# runtime env vars aren't inlined into client bundles.
|
||||||
|
NODE_ENV = "production";
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
pnpm run build
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
local appDir="$out/lib/hermes-workspace"
|
||||||
|
mkdir -p "$appDir"
|
||||||
|
|
||||||
|
# Copy build artefacts and runtime sources
|
||||||
|
cp -r dist "$appDir/"
|
||||||
|
cp -r node_modules "$appDir/"
|
||||||
|
cp -r skills "$appDir/"
|
||||||
|
cp package.json server-entry.js "$appDir/"
|
||||||
|
|
||||||
|
# pty-helper.py: Vite's copy-pty-helper plugin writes it during build
|
||||||
|
# but we also ensure it's present here as a belt-and-suspenders measure.
|
||||||
|
local ptyHelper="$appDir/dist/server/assets/pty-helper.py"
|
||||||
|
if [ ! -f "$ptyHelper" ]; then
|
||||||
|
mkdir -p "$(dirname "$ptyHelper")"
|
||||||
|
cp src/server/pty-helper.py "$ptyHelper"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a wrapper script so the binary lands in $out/bin
|
||||||
|
mkdir -p "$out/bin"
|
||||||
|
makeWrapper "${nodejs}/bin/node" "$out/bin/hermes-workspace" \
|
||||||
|
--add-flags "--max-old-space-size=2048" \
|
||||||
|
--add-flags "$appDir/server-entry.js" \
|
||||||
|
--set NODE_ENV "production" \
|
||||||
|
--prefix PATH : "${python3}/bin"
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Desktop workspace for Hermes Agent — chat, orchestration, and multi-agent coding pipelines";
|
||||||
|
homepage = "https://github.com/outsourc-e/hermes-workspace";
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
maintainers = [ ];
|
||||||
|
platforms = lib.platforms.linux ++ lib.platforms.darwin;
|
||||||
|
mainProgram = "hermes-workspace";
|
||||||
|
};
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user