Add support for Nix and NixOS (#130)

* fix: add missing resolved/integrity fields to package-lock.json

npm omits resolved URLs and integrity hashes for workspace-local
node_modules overrides. This breaks offline installers like Nix's
npm ci. Add the missing fields for 25 workspace-hoisted packages.

* feat: add Nix flake with package and NixOS module

Add a Nix flake that builds the Paseo daemon (server + CLI) and
provides a NixOS module for declarative deployment.

Package (nix/package.nix):
- Builds relay, server, and CLI workspaces
- Skips onnxruntime-node install script (sandbox-incompatible)
- Rebuilds only node-pty for native terminal support
- Source filter excludes app/website/desktop workspaces

NixOS module (nix/module.nix):
- Systemd service with configurable user, port, listen address
- allowedHosts for DNS rebinding protection
- relay.enable to toggle remote access via app.paseo.sh
- inheritUserEnvironment to expose user tools (git, ssh) to agents
- openFirewall and extra environment variables

ci: add Nix hash maintenance scripts and workflows

scripts/fix-lockfile.mjs:
  Adds missing resolved/integrity fields to package-lock.json for
  workspace-local overrides. Idempotent, uses `npm view`.

scripts/update-nix.sh:
  Runs fix-lockfile.mjs, prefetches deps, computes NAR hash, and
  updates npmDepsHash in nix/package.nix. Supports --check for CI.

.github/workflows/nix-build.yml:
  Builds the Nix package on push/PR and verifies the lockfile and
  hash are up to date.

.github/workflows/fix-nix-hash.yml:
  Auto-fixes lockfile signatures and Nix hash on dependabot PRs.

fix: update npmDepsHash after upstream sync

nix: allowlist workspace symlinks instead of blocklist

Prevents build failures when upstream adds new workspace packages.

* don't block PRs on nix failures

* better document npm workaround

* fix hash update script, and update hash

* integrate with npm run build:daemon

* ci: trigger nix build on highlight changes

* fix(nix): update npmDepsHash

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
José Albornoz
2026-03-25 23:42:44 +07:00
committed by GitHub
co-authored by Mohamed Boudra
parent 765a11c7ab
commit bb560809c7
9 changed files with 716 additions and 25 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
// Workaround for https://github.com/npm/cli/issues/4460
//
// npm silently omits `resolved` and `integrity` fields from some
// package-lock.json entries in workspace monorepos (especially for
// workspace-hoisted packages). npm acknowledged this as a bug in 2022
// but has never shipped a fix.
//
// This is harmless for regular `npm ci`, but breaks offline installers
// like Nix that need every entry to have a resolved URL + integrity hash
// so they can pre-fetch all tarballs in a sandbox with no network access.
//
// This script finds incomplete entries and fills them in using `npm view`.
// It's idempotent — running it on an already-complete lockfile is a no-op.
//
// See also: https://github.com/npm/cli/issues/4263
// https://github.com/npm/cli/issues/6301
//
// Usage:
// node scripts/fix-lockfile.mjs
// node scripts/fix-lockfile.mjs path/to/package-lock.json
import fs from "fs";
import { execSync } from "child_process";
const lockPath = process.argv[2] || "package-lock.json";
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
// Collect workspace package roots (local packages, not from npm)
const workspaceRoots = new Set();
for (const [key, val] of Object.entries(lock.packages || {})) {
if (val.link) {
workspaceRoots.add(val.resolved || key);
}
}
let fixed = 0;
for (const [key, val] of Object.entries(lock.packages || {})) {
if (
!key || // root package
key.startsWith("node_modules/") || // top-level (already has resolved)
val.link || // workspace link entry
(val.resolved && val.integrity) || // already complete
!val.version || // no version to look up
workspaceRoots.has(key) // workspace package root (local, not on npm)
)
continue;
const pkgName = key.replace(/.*node_modules\//, "");
const version = val.version;
try {
const info = JSON.parse(
execSync(`npm view ${pkgName}@${version} --json dist`, {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
})
);
if (info.tarball && info.integrity) {
val.resolved = info.tarball;
val.integrity = info.integrity;
fixed++;
}
} catch {
console.error(`Warning: could not fetch info for ${pkgName}@${version}`);
}
}
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n");
if (fixed > 0) {
console.log(`Fixed ${fixed} lockfile entries with missing resolved/integrity`);
} else {
console.log("Lockfile is already complete");
}
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Fix workspace-local lockfile entries and update the Nix dependency hash.
# Requires: node, npm, nix
#
# Usage:
# ./scripts/update-nix.sh # fix lockfile + update hash
# ./scripts/update-nix.sh --check # verify everything is up to date (CI mode)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCK_FILE="$ROOT_DIR/package-lock.json"
PACKAGE_NIX="$ROOT_DIR/nix/package.nix"
CHECK_MODE=false
if [[ "${1:-}" == "--check" ]]; then
CHECK_MODE=true
fi
# 1. Fix lockfile (add resolved/integrity for workspace-local entries)
# Workaround for https://github.com/npm/cli/issues/4460
echo "Fixing lockfile..."
node "$SCRIPT_DIR/fix-lockfile.mjs" "$LOCK_FILE"
# 2. Prefetch deps and compute hash
echo "Prefetching npm dependencies..."
# Resolve prefetch-npm-deps from the same nixpkgs pinned in flake.lock
NIXPKGS_URL="$(node -p "
const l = JSON.parse(require('fs').readFileSync('$ROOT_DIR/flake.lock', 'utf8'));
const n = l.nodes.nixpkgs.locked;
'github:' + n.owner + '/' + n.repo + '/' + n.rev;
")"
STDERR_LOG="$(mktemp)"
trap "rm -f '$STDERR_LOG'" EXIT
if ! NEW_HASH="$(nix shell "${NIXPKGS_URL}#prefetch-npm-deps" -c prefetch-npm-deps "$LOCK_FILE" 2>"$STDERR_LOG")"; then
echo "ERROR: prefetch-npm-deps failed:" >&2
tail -20 "$STDERR_LOG" >&2
exit 1
fi
echo "Computed hash: $NEW_HASH"
# 3. Read current hash
CURRENT_HASH="$(grep 'npmDepsHash' "$PACKAGE_NIX" | sed 's/.*"\(.*\)".*/\1/')"
if [[ "$NEW_HASH" == "$CURRENT_HASH" ]]; then
echo "Hash is already up to date."
else
if $CHECK_MODE; then
echo "ERROR: npmDepsHash is stale."
echo " current: $CURRENT_HASH"
echo " correct: $NEW_HASH"
echo "Run ./scripts/update-nix.sh to fix."
exit 1
fi
echo "Updating npmDepsHash in nix/package.nix..."
sed -i.bak "s|npmDepsHash = \".*\"|npmDepsHash = \"$NEW_HASH\"|" "$PACKAGE_NIX"
rm -f "$PACKAGE_NIX.bak"
echo "Updated: $CURRENT_HASH -> $NEW_HASH"
fi