Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CODEBASE_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ FRONTEND: js/ - Browser SPA (vanilla JS, no framework)
BACKEND: server/ - Refactored backend modules (services, gateways, routes)
STYLES: css/ - CSS custom properties + component styles
TESTS: test/ - Vitest unit + integration, Puppeteer E2E
CONFIG: vitest.config.js, vitest.unit.config.js, package.json
CONFIG: vitest.config.js, vitest.unit.config.js, package.json, flake.nix
ASSETS: assets/ - Favicons + screenshots
DOCS: refactoring-analysis/ - Refactor plans/reports, CLI-COMPATIBILITY.md
```
Expand Down Expand Up @@ -183,6 +183,8 @@ vitest.config.js - Main test config (all tests)
vitest.unit.config.js - Unit-only test config
bin/cli.js - CLI entry point (gastown-gui command)
scripts/extract_user_prompts.mjs - Sanitized prompt log builder
flake.nix - Flake outputs for package/app + NixOS module export
nix/deployment.nix - NixOS module defining services.gastown-gui
```

## Documentation
Expand Down
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,68 @@ gastown-gui doctor

---

## Nix / NixOS

### Build with Nix flake

```bash
nix build .#gastown-gui
./result/bin/gastown-gui start
```

### Run as a NixOS service

Import the module from this repository's flake and enable it:

```nix
{
inputs.gastown-gui.url = "github:web3dev1337/gastown-gui";

outputs = { self, nixpkgs, gastown-gui, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
gastown-gui.nixosModules.deployment
({
services.gastown-gui = {
enable = true;
host = "127.0.0.1";
port = 7667;
openFirewall = false; # keep false when reverse-proxying locally

# Optional: add runtime tools to PATH for service subprocesses
# gtPackage = pkgs.gastown-gt;
# beadsPackage = pkgs.beads;

# Defaults: create and run as system user/group "gastown"
# user = "gastown";
# group = "gastown";
# createUser = true;
# createGroup = true;

# Optional: where your Gas Town rigs live
# gtRoot = "/var/lib/gastown/gt";

# Optional: extra env vars
# environment = { CORS_ORIGINS = "http://localhost:3000"; };
};
})
];
};
};
}
```

Then rebuild your system:

```bash
sudo nixos-rebuild switch --flake .#my-host
```

Service hardening defaults are enabled in the module (for example `NoNewPrivileges`, `PrivateTmp`, `ProtectSystem`).

---

## Features

- **Rig Management** - Add, view, and organize project repositories
Expand Down
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
description = "Nix flake for gastown-gui";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
packageJson = builtins.fromJSON (builtins.readFile ./package.json);
in {
packages = {
gastown-gui = pkgs.buildNpmPackage {
pname = packageJson.name;
version = packageJson.version;
src = ./.;

npmDepsHash = "sha256-xjwBig9CPR6OUWgRLD9pAJAmAq7oVvW/ulrqRqRilrs=";

dontNpmBuild = true;
PUPPETEER_SKIP_DOWNLOAD = "true";

meta = {
description = packageJson.description;
homepage = packageJson.homepage;
license = pkgs.lib.licenses.mit;
mainProgram = "gastown-gui";
};
};

default = self.packages.${system}.gastown-gui;
};

apps.default = {
type = "app";
program = "${self.packages.${system}.gastown-gui}/bin/gastown-gui";
};
})
// {
nixosModules.deployment = import ./nix/deployment.nix { inherit self; };
};
}
147 changes: 147 additions & 0 deletions nix/deployment.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{ self }:
{ config, lib, pkgs, ... }:
let
cfg = config.services.gastown-gui;
isAbsolutePath = value: builtins.match "^/.*" value != null;
in {
options.services.gastown-gui = {
enable = lib.mkEnableOption "gastown-gui web service";

package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.default;
description = "Gastown GUI package to run.";
};

gtPackage = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = "Optional package that provides the gt binary.";
};

beadsPackage = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = "Optional package that provides the beads binary.";
};

openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open firewall for the configured port.";
};

host = lib.mkOption {
type = lib.types.nonEmptyStr;
default = "127.0.0.1";
description = "Host interface for the gastown-gui HTTP server.";
};

port = lib.mkOption {
type = lib.types.port;
default = 7667;
description = "Port for the gastown-gui HTTP server.";
};

user = lib.mkOption {
type = lib.types.nonEmptyStr;
default = "gastown";
description = "User to run service under";
};

group = lib.mkOption {
type = lib.types.nonEmptyStr;
default = "gastown";
description = "Group to run service under";
};

createUser = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Create the configured service user when enabled.";
};

createGroup = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Create the configured service group when enabled.";
};

gtRoot = lib.mkOption {
type = lib.types.nullOr lib.types.nonEmptyStr;
default = null;
example = "/var/lib/gastown/gt";
description = "Optional GT_ROOT path passed to gastown-gui.";
};

environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Extra environment variables for the gastown-gui service.";
};
};

config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.gtRoot == null || isAbsolutePath cfg.gtRoot;
message = "services.gastown-gui.gtRoot must be an absolute path when set.";
}
];

users.groups = lib.mkIf cfg.createGroup {
"${cfg.group}" = { };
};

users.users = lib.mkIf cfg.createUser {
"${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
description = "Gastown GUI service account";
home = "/var/lib/gastown-gui";
};
};

networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ];

systemd.services.gastown-gui = {
description = "Gastown GUI";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path =
[ pkgs.nodejs ]
++ lib.optional (cfg.gtPackage != null) cfg.gtPackage
++ lib.optional (cfg.beadsPackage != null) cfg.beadsPackage;
environment = cfg.environment // lib.optionalAttrs (cfg.gtRoot != null) {
GT_ROOT = toString cfg.gtRoot;
};
serviceConfig = {
Type = "simple";
ExecStart = "${lib.getExe cfg.package} start --host ${cfg.host} --port ${toString cfg.port}";
DynamicUser = false;
User = cfg.user;
Group = cfg.group;
StateDirectory = "gastown-gui";
Restart = "on-failure";
RestartSec = "2s";
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = "full";
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectClock = true;
ProtectHostname = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
LockPersonality = true;
SystemCallArchitectures = "native";
CapabilityBoundingSet = [ "" ];
AmbientCapabilities = [ ];
UMask = "0077";
};
};
};
}