Packages & Scripts
Add tools to your shell and define custom commands
Stackpanel gives you two ways to extend your development shell: packages add tools to your PATH, and scripts define project-specific commands that your whole team can use.
Packages
Packages are Nix derivations added to your shell environment. When you enter the dev shell, every package's bin/ directory is on your PATH.
stackpanel.devshell.packages = with pkgs; [
nodejs
bun
jq
awscli2
postgresql
];Packages are pinned to exact versions by your flake.lock. Every team member gets the same node, the same jq, the same aws—regardless of what's installed on their system.
You don't need to install packages globally or manage version managers like nvm or fnm. The dev shell provides exactly the versions you specify, isolated from the rest of the system.
Finding Packages
Search for available packages on search.nixos.org or from the command line:
nix search nixpkgs nodejs
nix search nixpkgs python3User Packages
Individual developers can add personal packages that don't affect the rest of the team using stackpanel.userPackages:
# .stack/config.local.nix (gitignored)
{
stackpanel.userPackages = with pkgs; [
lazygit
ripgrep
fd
];
}These are merged into the shell alongside the project packages but don't show up in version control.
Scripts
Scripts are named shell commands available in your dev shell. They're defined in stackpanel.scripts and become executables on your PATH.
You can also opt into Turbo integration so scripts are materialized as a generated workspace package (default: packages/gen/scripts, package name @gen/scripts) and can run through turbo run <script-name> with dependency/caching metadata.
Inline Scripts
For simple commands, use exec to define the script inline:
stackpanel.scripts = {
dev = {
exec = "bun run --filter './apps/*' dev";
description = "Start all development servers";
};
db-seed = {
exec = "bun run seed";
description = "Seed the database with test data";
};
lint = {
exec = "oxlint . && tsc --noEmit";
description = "Run linter and type checker";
};
};After entering your shell, these are available as regular commands:
$ dev # starts dev servers
$ db-seed # seeds the database
$ lint # runs lint + typecheckFile-Based Scripts
For anything longer than a one-liner, point path at a shell script file:
stackpanel.scripts.deploy = {
path = ./.stack/src/scripts/deploy.sh;
description = "Deploy the application";
runtimeInputs = [ pkgs.awscli2 pkgs.jq ];
env.AWS_REGION = "us-west-2";
};File-based scripts get better editor support (syntax highlighting, linting) and are easier to test in isolation. The path and exec options are mutually exclusive—use one or the other.
Runtime Inputs
Use runtimeInputs to add packages to a script's PATH without adding them to the entire shell:
stackpanel.scripts."db:migrate" = {
exec = "drizzle-kit migrate";
description = "Run database migrations";
runtimeInputs = [ pkgs.nodejs ];
};This is useful when a script needs a tool that you don't want globally available—it keeps the shell clean.
Environment Variables
Pass configuration into scripts via env:
stackpanel.scripts."test:e2e" = {
exec = "playwright test";
description = "Run end-to-end tests";
env = {
CI = "true";
BASE_URL = "http://localhost:4200";
};
};Environment variables defined in env are exported before the script runs. This is the recommended way to pass Nix-evaluated values into file-based scripts.
Timeouts
Scripts have a default timeout of 5 minutes to prevent runaway processes. You can adjust this per-script:
stackpanel.scripts = {
"health:check" = {
exec = "curl -sf http://localhost:4200/health";
description = "Quick health check";
timeout = 30; # 30 seconds
};
"db:restore" = {
path = ./.stack/src/scripts/db-restore.sh;
description = "Restore database from backup";
timeout = 1800; # 30 minutes
};
"dev" = {
exec = "bun run dev";
description = "Start dev server";
timeout = 0; # no timeout (long-running)
};
};Common timeout values:
| Preset | Seconds | Use Case |
|---|---|---|
| Quick | 30 | Health checks, simple commands |
| Default | 300 | Most scripts, network calls, builds |
| Build | 900 | Complex compilations |
| Deploy | 1800 | Deployments, migrations |
| Long | 3600 | Data processing |
| None | 0 | Long-running processes (dev servers) |
Namespaced Scripts
Extensions use a colon-separated namespace to avoid name collisions:
stackpanel.scripts = {
"sst:deploy" = {
exec = "sst deploy";
description = "Deploy SST infrastructure";
};
"sst:dev" = {
exec = "sst dev";
description = "Start SST dev mode";
};
"docker:build" = {
exec = "docker compose build";
description = "Build Docker images";
};
};This convention keeps things tidy as your project grows. Builtin extensions follow this pattern automatically—when you enable the SST extension, its scripts appear as sst:deploy, sst:dev, etc.
How Scripts Are Built
Under the hood, every script becomes a Nix derivation via pkgs.writeShellApplication. All scripts are bundled into a single stackpanel-scripts package with executables in bin/. This means:
- Scripts are immutable—they can't be accidentally modified at runtime
- Dependencies declared in
runtimeInputsare pinned to exact Nix store paths - Scripts run with
set -euo pipefailby default (strict mode) - The combined scripts package is cached by Nix like any other derivation
Scripts are also exposed as flake outputs, so you can run them outside the dev shell:
nix run .#scripts.db-seedTurbo Integration
To connect scripts to Turborepo, enable generated package support:
stackpanel.scriptsConfig = {
generateTurboPackage = true;
# Optional overrides:
# turboPackageId = "scripts";
# turboPackageName = "@gen/scripts";
# turboPackagePath = "packages/gen/scripts";
};When enabled, Stackpanel generates/merges a Turbo package declaration for scripts. The resulting workspace package contributes package.json scripts where each script command maps to its script name, so Turbo can execute them directly.
Per-Script Turbo Metadata
Each script can declare Turbo task metadata under turbo:
stackpanel.scripts = {
"write-files" = {
exec = "write-files";
description = "Materialize generated files";
turbo = {
enable = true;
cache = false;
inputs = [ ".stack/**" "nix/stackpanel/**" ];
};
};
"check-files" = {
exec = "check-files";
description = "Verify generated files are current";
turbo = {
enable = true;
dependsOn = [ "write-files" ];
cache = false;
inputs = [ ".stack/**" "nix/stackpanel/**" ];
};
};
"build:ui" = {
exec = "bun run --filter @stackpanel/web build";
description = "Build web UI";
turbo = {
enable = true;
dependsOn = [ "^build" "check-files" ];
outputs = [ "apps/web/.next/**" ];
};
};
};Supported turbo fields:
enable: Register this script as a Turbo taskdependsOn: Task dependencies (supports^tasksyntax)outputs: Output globs for cachinginputs: Input globs for cache keyscache: Override Turbo cache behaviorpersistent: Mark long-running tasksinteractive: Mark tasks that read stdin
Running Through Turbo
After generating files (write-files) and with Turbo configured, run scripts via:
turbo run write-files
turbo run check-files
turbo run build:uiThis gives you Turbo dependency ordering and cache behavior while keeping script definitions in one place (stackpanel.scripts).
Reference
- Options Reference → Devshell for
stackpanel.devshell.packages,stackpanel.scripts, andstackpanel.scriptsConfig - Options Reference → Packages for the full packages configuration
- Options Reference → UserPackages for per-user package overrides