Compare commits

...

7 Commits

Author SHA1 Message Date
Mohamed Boudra
0820ee14f1 ci: trigger nix build on highlight changes 2026-03-25 23:20:48 +07:00
José Albornoz
49ebc26cdb integrate with npm run build:daemon 2026-03-25 22:52:43 +07:00
José Albornoz
6bad956c8f fix hash update script, and update hash 2026-03-25 22:52:43 +07:00
José Albornoz
233b309fd9 better document npm workaround 2026-03-25 22:52:43 +07:00
José Albornoz
ae4123be10 don't block PRs on nix failures 2026-03-25 22:52:43 +07:00
José Albornoz
6b0ff42748 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.
2026-03-25 22:52:43 +07:00
José Albornoz
627dc21736 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.
2026-03-25 22:52:43 +07:00
9 changed files with 716 additions and 25 deletions

41
.github/workflows/fix-nix-hash.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Fix Nix hash
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
permissions:
contents: write
jobs:
fix-nix-hash:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Fix lockfile and update hash
run: ./scripts/update-nix.sh
- name: Commit changes
run: |
git diff --quiet package-lock.json nix/package.nix && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package-lock.json nix/package.nix
git commit -m "fix: update lockfile signatures and Nix hash"
git push

59
.github/workflows/nix-build.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Nix Build
on:
push:
branches: [main]
paths:
- 'nix/**'
- 'flake.nix'
- 'flake.lock'
- 'package.json'
- 'package-lock.json'
- 'packages/highlight/**'
- 'packages/server/**'
- 'packages/relay/**'
- 'packages/cli/**'
pull_request:
branches: [main]
paths:
- 'nix/**'
- 'flake.nix'
- 'flake.lock'
- 'package.json'
- 'package-lock.json'
- 'packages/highlight/**'
- 'packages/server/**'
- 'packages/relay/**'
- 'packages/cli/**'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Build Nix package
run: nix build .#default -o result
- name: Verify lockfile is complete
# npm silently omits resolved/integrity fields in workspace monorepos.
# Nix needs them for offline builds. See https://github.com/npm/cli/issues/4460
run: |
node scripts/fix-lockfile.mjs package-lock.json
git diff --exit-code package-lock.json || {
echo "ERROR: package-lock.json has missing resolved/integrity fields."
echo "This is a known npm bug: https://github.com/npm/cli/issues/4460"
echo "Run 'node scripts/fix-lockfile.mjs' and commit the result."
exit 1
}
- name: Check npmDepsHash is up to date
run: ./scripts/update-nix.sh --check

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

59
flake.nix Normal file
View File

@@ -0,0 +1,59 @@
{
description = "Paseo - self-hosted daemon for AI coding agents";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{
self,
nixpkgs,
}:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
pkgsFor = system: import nixpkgs { inherit system; };
in
{
packages = forAllSystems (
system:
let
pkgs = pkgsFor system;
paseo = pkgs.callPackage ./nix/package.nix { };
in
{
default = paseo;
paseo = paseo;
}
);
nixosModules.default = self.nixosModules.paseo;
nixosModules.paseo =
{ pkgs, lib, ... }:
{
imports = [ ./nix/module.nix ];
services.paseo.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
devShells = forAllSystems (
system:
let
pkgs = pkgsFor system;
in
{
default = pkgs.mkShell {
packages = [
pkgs.nodejs_22
pkgs.python3
];
};
}
);
};
}

172
nix/module.nix Normal file
View File

@@ -0,0 +1,172 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.paseo;
in
{
options.services.paseo = {
enable = lib.mkEnableOption "Paseo, a self-hosted daemon for AI coding agents";
package = lib.mkPackageOption pkgs "paseo" { };
user = lib.mkOption {
type = lib.types.str;
default = "paseo";
description = "User account under which Paseo runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "paseo";
description = "Group under which Paseo runs.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default =
if cfg.user == "paseo"
then "/var/lib/paseo"
else "/home/${cfg.user}/.paseo";
defaultText = lib.literalExpression ''
if cfg.user == "paseo"
then "/var/lib/paseo"
else "/home/''${cfg.user}/.paseo"
'';
description = "Directory for Paseo state (PASEO_HOME). Stores agent data, config, and logs.";
};
port = lib.mkOption {
type = lib.types.port;
default = 6767;
description = "Port for the Paseo daemon to listen on.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address for the Paseo daemon to bind to.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the firewall for the Paseo daemon port.";
};
allowedHosts = lib.mkOption {
type = lib.types.either (lib.types.enum [ true ]) (lib.types.listOf lib.types.str);
default = [ ];
example = [ ".example.com" "myhost.local" ];
description = ''
Hosts allowed to connect to the Paseo daemon (DNS rebinding protection).
Localhost and IP addresses are always allowed by default.
Use a leading dot to match a domain and all its subdomains
(e.g. `".example.com"` matches `example.com` and `foo.example.com`).
Set to `true` to allow any host (not recommended).
'';
};
relay = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable the relay connection for remote access via app.paseo.sh.";
};
};
inheritUserEnvironment = lib.mkOption {
type = lib.types.bool;
default = cfg.user != "paseo";
defaultText = lib.literalExpression ''cfg.user != "paseo"'';
description = ''
Whether to include the user's profile PATH in the service environment.
When Paseo runs as a real user (not the default system user), AI agents
need access to the user's tools (git, ssh, etc.). This adds the user's
NixOS profile and system paths so agents can use them without manually
setting PATH.
Enabled by default when `user` is set to a non-default value.
'';
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''
{
PASEO_RELAY_ENDPOINT = "relay.paseo.sh:443";
}
'';
description = "Extra environment variables for the Paseo daemon.";
};
};
config = lib.mkIf cfg.enable {
users.users.${cfg.user} = lib.mkIf (cfg.user == "paseo") {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
};
users.groups.${cfg.group} = lib.mkIf (cfg.group == "paseo") { };
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0700 ${cfg.user} ${cfg.group} - -"
];
systemd.services.paseo = {
description = "Paseo - self-hosted daemon for AI coding agents";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
NODE_ENV = "production";
PASEO_HOME = cfg.dataDir;
PASEO_LISTEN = "${cfg.listenAddress}:${toString cfg.port}";
} // lib.optionalAttrs cfg.inheritUserEnvironment {
# mkForce overrides the default PATH from NixOS's systemd module (which
# only includes store paths for coreutils/grep/sed/systemd). Our PATH
# includes /run/current-system/sw/bin which is a superset of those.
PATH = lib.mkForce (lib.concatStringsSep ":" [
"/etc/profiles/per-user/${cfg.user}/bin"
"/run/current-system/sw/bin"
"/run/wrappers/bin"
"/nix/var/nix/profiles/default/bin"
]);
} // lib.optionalAttrs (cfg.allowedHosts == true) {
PASEO_ALLOWED_HOSTS = "true";
} // lib.optionalAttrs (lib.isList cfg.allowedHosts && cfg.allowedHosts != [ ]) {
PASEO_ALLOWED_HOSTS = lib.concatStringsSep "," cfg.allowedHosts;
} // cfg.environment;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart =
"${cfg.package}/bin/paseo-server"
+ lib.optionalString (!cfg.relay.enable) " --no-relay";
Restart = "on-failure";
RestartSec = 5;
# Graceful shutdown (server handles SIGTERM with a 10s timeout)
KillSignal = "SIGTERM";
TimeoutStopSec = 15;
};
};
environment.systemPackages = [ cfg.package ];
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}

144
nix/package.nix Normal file
View File

@@ -0,0 +1,144 @@
{
lib,
stdenv,
buildNpmPackage,
nodejs_22,
python3,
makeWrapper,
# node-pty needs libuv headers on Linux
libuv,
}:
buildNpmPackage rec {
pname = "paseo";
version = (builtins.fromJSON (builtins.readFile ../package.json)).version;
src = lib.cleanSourceWith {
src = ./..;
filter = path: type:
let
baseName = builtins.baseNameOf path;
relPath = lib.removePrefix (toString ./..) path;
in
# Exclude non-daemon workspace contents (keep package.json for workspace resolution)
!(lib.hasPrefix "/packages/app/src" relPath)
&& !(lib.hasPrefix "/packages/app/assets" relPath)
&& !(lib.hasPrefix "/packages/app/android" relPath)
&& !(lib.hasPrefix "/packages/app/ios" relPath)
&& !(lib.hasPrefix "/packages/website/src" relPath)
&& !(lib.hasPrefix "/packages/website/public" relPath)
&& !(lib.hasPrefix "/packages/desktop/src" relPath)
&& !(lib.hasPrefix "/packages/desktop/src-tauri" relPath)
# Exclude test fixtures and debug files
&& !(lib.hasSuffix ".test.ts" baseName)
&& !(lib.hasSuffix ".e2e.test.ts" baseName)
&& baseName != "node_modules"
&& baseName != ".git"
&& baseName != ".paseo"
&& baseName != ".DS_Store";
};
nodejs = nodejs_22;
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
npmDepsHash = "sha256-qRknrMt2VBCCRGGedyTrPEBs3u+Z8lmDZuuvX7mE2Cg=";
# Prevent onnxruntime-node's install script from running during automatic
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
# We manually rebuild only node-pty in buildPhase.
npmRebuildFlags = [ "--ignore-scripts" ];
nativeBuildInputs = [
python3 # for node-gyp (node-pty compilation)
makeWrapper
];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
libuv
];
# Don't use the default npm build hook — we need a custom build sequence
dontNpmBuild = true;
buildPhase = ''
runHook preBuild
# Rebuild only node-pty (native addon for terminal emulation).
# Speech-related native modules (sherpa-onnx, onnxruntime-node) are
# intentionally left unbuilt they're lazily loaded and gracefully
# degrade when unavailable.
npm rebuild node-pty
# Build all daemon packages in dependency order (defined in package.json)
npm run build:daemon
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib/paseo
# Copy root package metadata
cp package.json $out/lib/paseo/
# Copy node_modules (preserving workspace symlinks)
cp -a node_modules $out/lib/paseo/
# Auto-detect which @getpaseo/* packages were built by build:daemon
# (they'll have a dist/ directory). Copy those and remove the rest.
for link in $out/lib/paseo/node_modules/@getpaseo/*; do
name=$(basename "$link")
if [ -d "packages/$name/dist" ]; then
mkdir -p "$out/lib/paseo/packages/$name"
cp "packages/$name/package.json" "$out/lib/paseo/packages/$name/"
cp -a "packages/$name/dist" "$out/lib/paseo/packages/$name/"
if [ -d "packages/$name/node_modules" ]; then
cp -a "packages/$name/node_modules" "$out/lib/paseo/packages/$name/"
fi
else
rm -f "$link"
fi
done
# Copy CLI bin entry
mkdir -p $out/lib/paseo/packages/cli/bin
cp packages/cli/bin/paseo $out/lib/paseo/packages/cli/bin/
# Copy extra server files referenced at runtime
for f in agent-prompt.md .env.example; do
if [ -f packages/server/$f ]; then
cp packages/server/$f $out/lib/paseo/packages/server/
fi
done
# Copy server scripts (daemon-runner, supervisor) needed by CLI
if [ -d packages/server/dist/scripts ]; then
mkdir -p $out/lib/paseo/packages/server/dist/scripts
cp -a packages/server/dist/scripts/* $out/lib/paseo/packages/server/dist/scripts/
fi
# Create wrapper for the server entry point (for systemd / direct use)
mkdir -p $out/bin
makeWrapper ${nodejs}/bin/node $out/bin/paseo-server \
--add-flags "$out/lib/paseo/packages/server/dist/server/server/index.js" \
--set NODE_ENV production
# Create wrapper for the CLI
makeWrapper ${nodejs}/bin/node $out/bin/paseo \
--add-flags "$out/lib/paseo/packages/cli/dist/index.js" \
--set NODE_PATH "$out/lib/paseo/node_modules"
runHook postInstall
'';
meta = {
description = "Self-hosted daemon for Claude Code, Codex, and OpenCode";
homepage = "https://github.com/getpaseo/paseo";
license = lib.licenses.agpl3Plus;
mainProgram = "paseo";
platforms = lib.platforms.linux ++ lib.platforms.darwin;
};
}

100
package-lock.json generated
View File

@@ -34942,7 +34942,9 @@
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz",
"integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q=="
},
"packages/app/node_modules/react-native-nitro-modules": {
"version": "0.33.8",
@@ -34959,7 +34961,9 @@
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
},
"packages/cli": {
"name": "@getpaseo/cli",
@@ -34993,14 +34997,18 @@
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"packages/cli/node_modules/commander": {
"version": "12.1.0",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="
},
"packages/desktop": {
"name": "@getpaseo/desktop",
@@ -35351,7 +35359,9 @@
},
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
"integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
"version": "6.12.6",
@@ -35365,7 +35375,9 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/express": {
"version": "5.1.0",
@@ -35405,7 +35417,9 @@
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -35422,7 +35436,9 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="
},
"packages/server/node_modules/ajv": {
"version": "8.17.1",
@@ -35436,7 +35452,9 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="
},
"packages/server/node_modules/ansi-regex": {
"version": "6.2.2",
@@ -35446,7 +35464,9 @@
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="
},
"packages/server/node_modules/body-parser": {
"version": "2.2.0",
@@ -35464,7 +35484,9 @@
},
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="
},
"packages/server/node_modules/content-disposition": {
"version": "1.0.0",
@@ -35474,14 +35496,18 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="
},
"packages/server/node_modules/cookie-signature": {
"version": "1.2.2",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
},
"packages/server/node_modules/finalhandler": {
"version": "2.1.0",
@@ -35496,21 +35522,27 @@
},
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="
},
"packages/server/node_modules/fresh": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="
},
"packages/server/node_modules/media-typer": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
},
"packages/server/node_modules/merge-descriptors": {
"version": "2.0.0",
@@ -35520,7 +35552,9 @@
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="
},
"packages/server/node_modules/mime-types": {
"version": "3.0.1",
@@ -35530,14 +35564,18 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="
},
"packages/server/node_modules/negotiator": {
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="
},
"packages/server/node_modules/raw-body": {
"version": "3.0.2",
@@ -35588,7 +35626,9 @@
},
"engines": {
"node": ">= 18"
}
},
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="
},
"packages/server/node_modules/serve-static": {
"version": "2.2.0",
@@ -35601,7 +35641,9 @@
},
"engines": {
"node": ">= 18"
}
},
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="
},
"packages/server/node_modules/strip-ansi": {
"version": "7.1.2",
@@ -35614,7 +35656,9 @@
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="
},
"packages/server/node_modules/type-is": {
"version": "2.0.1",
@@ -35626,14 +35670,18 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="
},
"packages/server/node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
},
"packages/website": {
"name": "@getpaseo/website",
@@ -35667,7 +35715,9 @@
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
"integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ=="
},
"packages/website/node_modules/react": {
"version": "19.2.4",

76
scripts/fix-lockfile.mjs Normal file
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
scripts/update-nix.sh Executable file
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