The surprisingly performant, Nushell-scriptable, cross.stream-powered, Datastar-ready HTTP server that fits in your back pocket.
Install
ยท
Reference
ยท
Discord
- Install
- Reference
- GET: Hello world
- UNIX domain sockets
- Watch Mode
- Reading from stdin
- POST: echo
- Request metadata
- Response metadata
- Content-Type Inference
- TLS & HTTP/2 Support
- Logging
- Trusted Proxies
- Serving Static Files
- Streaming responses
- server-sent events
- Embedded Store
- Reverse Proxy
- Templates
- Syntax Highlighting
- Markdown
- Streaming Input
- Plugins
- Module Paths
- Embedded Modules
- Eval Subcommand
- Building and Releases
- History
Install
eget
Homebrew (macOS)
brew install cablehead/tap/http-nu
cargo
For fast installation using pre-built binaries:
Or build from source:
cargo install http-nu --locked
Nix
http-nu is available in nixpkgs. For packaging and maintenance documentation, see NIXOS_PACKAGING_GUIDE.md.
Reference
GET: Hello world
$ http-nu :3001 -c '{|req| "Hello world"}'
$ curl -s localhost:3001
Hello worldOr from a file:
$ http-nu :3001 ./serve.nu
Try the live examples or run them locally with the examples hub:
$ http-nu --datastar :3001 examples/serve.nu
$ http-nu --datastar --store ./store :3001 examples/serve.nu # enables store-dependent examplesUNIX domain sockets
$ http-nu ./sock -c '{|req| "Hello world"}'
$ curl -s --unix-socket ./sock localhost
Hello worldWatch Mode
Use -w / --watch to automatically reload when files change:
$ http-nu :3001 -w ./serve.nu
This watches the script's directory for any changes (including included files) and hot-reloads the handler. Useful during development. Active SSE connections are aborted on reload to trigger client reconnection.
Reading from stdin
Pass - to read the script from stdin:
$ echo '{|req| "hello"}' | http-nu :3001 -
With -w, send null-terminated scripts to hot-reload the handler:
$ (printf '{|req| "v1"}\0'; sleep 5; printf '{|req| "v2"}') | http-nu :3001 - -w
Each \0-terminated script replaces the handler.
POST: echo
$ http-nu :3001 -c '{|req| $in}'
$ curl -s -d Hai localhost:3001
HaiRequest metadata
The Request metadata is passed as an argument to the closure.
$ http-nu :3001 -c '{|req| $req}' $ curl -s 'localhost:3001/segment?foo=bar&abc=123' # or $ http get 'http://localhost:3001/segment?foo=bar&abc=123' โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ proto โ HTTP/1.1 method โ GET uri โ /segment?foo=bar&abc=123 path โ /segment remote_ip โ 127.0.0.1 remote_port โ 52007 trusted_ip โ 127.0.0.1 โ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ headers โ host โ localhost:3001 โ user-agent โ curl/8.7.1 โ accept โ */* โ โโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ โ โโโโโโฌโโโโโ query โ abc โ 123 โ foo โ bar โ โโโโโโดโโโโโ โโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ $ http-nu :3001 -c '{|req| $"hello: ($req.path)"}' $ http get 'http://localhost:3001/yello' hello: /yello
Response metadata
Set HTTP response status and headers using nushell's pipeline metadata:
"body" | metadata set --merge {'http.response': { status: <number> # Optional, defaults to 204 if body is empty, 200 otherwise headers: { # Optional, HTTP headers <key>: <value> # Single value: "text/plain" <key>: [<value>, <value>] # Multiple values: ["cookie1=a", "cookie2=b"] } }}
Header values can be strings or lists of strings. Multiple values (e.g., Set-Cookie) are sent as separate HTTP headers per RFC 6265.
$ http-nu :3001 -c '{|req| "sorry, eh" | metadata set --merge {"http.response": {status: 404}}}'
$ curl -si localhost:3001
HTTP/1.1 404 Not Found
transfer-encoding: chunked
date: Fri, 31 Jan 2025 08:20:28 GMT
sorry, eh
Multi-value headers:
"cookies set" | metadata set --merge {'http.response': { headers: { "Set-Cookie": ["session=abc; Path=/", "token=xyz; Secure"] } }}
Content-Type Inference
Content-type is determined in the following order of precedence:
-
Headers set via
http.responsemetadata:"body" | metadata set --merge {'http.response': { headers: {"Content-Type": "text/plain"} }}
-
Pipeline metadata content-type (e.g., from
to yamlormetadata set --content-type) -
Inferred from value type:
- Record ->
application/json - List ->
application/json(JSON array) - Stream of records ->
application/x-ndjson(JSONL) - Binary or byte stream ->
application/octet-stream - Empty (null) -> no Content-Type header
- Record ->
-
Default:
text/html; charset=utf-8
Examples:
# 1. Explicit header takes precedence {|req| {foo: "bar"} | metadata set --merge {'http.response': {headers: {"Content-Type": "text/plain"}}} } # 2. Pipeline metadata {|req| ls | to yaml } # Returns as application/x-yaml # 3. Inferred from value type {|req| {foo: "bar"} } # Record -> application/json {|req| [{a: 1}, {b: 2}, {c: 3}] } # List -> application/json (array) {|req| 1..10 | each { {n: $in} } } # Stream of records -> application/x-ndjson {|req| 0x[deadbeef] } # Binary -> application/octet-stream {|req| null } # Empty -> no Content-Type header # 4. Default {|req| "Hello" } # Returns as text/html; charset=utf-8
To consume a JSONL endpoint from Nushell:
http get http://localhost:3001 | from json --objects | each {|row| ... }
TLS Support
Enable TLS by providing a PEM file containing both certificate and private key:
$ http-nu :3001 --tls combined.pem -c '{|req| "Secure Hello"}'
$ curl -k https://localhost:3001
Secure HelloGenerate a self-signed certificate for testing:
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
$ cat cert.pem key.pem > combined.pemHTTP/2 is automatically enabled for TLS connections:
$ curl -k --http2 -si https://localhost:3001 | head -1
HTTP/2 200Logging
Control log output with --log-format:
human(default): Live-updating terminal output with startup banner, per-request progress lines showing timestamp, IP, method, path, status, timing, and bytesjsonl: Structured JSON lines withscru128stamps for log aggregation
Each request emits 3 phases: request (received), response (headers sent), complete (body finished).
Human format
JSONL format
Events share a request_id for correlation:
$ http-nu --log-format jsonl :3001 '{|req| "hello"}' {"stamp":"...","message":"started","address":"http://127.0.0.1:3001","startup_ms":42} {"stamp":"...","message":"request","request_id":"...","method":"GET","path":"/","request":{...}} {"stamp":"...","message":"response","request_id":"...","status":200,"headers":{...},"latency_ms":1} {"stamp":"...","message":"complete","request_id":"...","bytes":5,"duration_ms":2}
Lifecycle events: started, reloaded, stopping, stopped, stop_timed_out
The print command outputs to the logging system (appears as message: "print"
in JSONL).
Trusted Proxies
When behind a reverse proxy, use --trust-proxy to extract client IP from
X-Forwarded-For. Accepts CIDR notation, repeatable:
$ http-nu --trust-proxy 10.0.0.0/8 --trust-proxy 192.168.0.0/16 :3001 '{|req| $req.trusted_ip}'The trusted_ip field is resolved by parsing X-Forwarded-For right-to-left,
stopping at the first IP not in a trusted range. Falls back to remote_ip when:
- No
--trust-proxyflags provided - Remote IP is not in trusted ranges
- No
X-Forwarded-Forheader present
Serving Static Files
You can serve static files from a directory using the .static command. This
command takes two arguments: the root directory path and the request path.
When you call .static, it sets the response to serve the specified file, and
any subsequent output in the closure will be ignored. The content type is
automatically inferred based on the file extension (e.g., text/css for .css
files).
Here's an example:
$ http-nu :3001 -c '{|req| .static "/path/to/static/dir" $req.path}'For single page applications you can provide a fallback file:
$ http-nu :3001 -c '{|req| .static "/path/to/static/dir" $req.path --fallback "index.html"}'Streaming responses
Values returned by streaming pipelines (like generate) are sent to the client
immediately as HTTP chunks. This allows real-time data transmission without
waiting for the entire response to be ready.
$ http-nu :3001 -c '{|req| generate {|_| sleep 1sec {out: (date now | to text | $in + "\n") next: true } } true }' $ curl -s localhost:3001 Fri, 31 Jan 2025 03:47:59 -0500 (now) Fri, 31 Jan 2025 03:48:00 -0500 (now) Fri, 31 Jan 2025 03:48:01 -0500 (now) Fri, 31 Jan 2025 03:48:02 -0500 (now) Fri, 31 Jan 2025 03:48:03 -0500 (now) ...
server-sent events
Use the to sse command to format records for the text/event-stream protocol.
Each input record may contain the optional fields data, id, event, and
retry which will be emitted in the resulting stream.
to sse
Converts {data? id? event? retry?} records into SSE format. Non-string data
values are serialized to JSON.
Auto-sets response headers: content-type: text/event-stream,
cache-control: no-cache, connection: keep-alive.
| input | output |
|---|---|
| record | string |
Examples
> {data: 'hello'} | to sse data: hello > {id: 1 event: greet data: 'hi'} | to sse id: 1 event: greet data: hi > {data: "foo\nbar"} | to sse data: foo data: bar > {data: [1 2 3]} | to sse data: [1,2,3]
# Note: `to sse` automatically sets content-type: text/event-stream $ http-nu :3001 -c '{|req| tail -F source.json | lines | from json | to sse }' # simulate generating events in a seperate process $ loop { {date: (date now)} | to json -r | $in + "\n" | save -a source.json sleep 1sec } $ curl -si localhost:3001/ HTTP/1.1 200 OK content-type: text/event-stream transfer-encoding: chunked date: Fri, 31 Jan 2025 09:01:20 GMT data: {"date":"2025-01-31 04:01:23.371514 -05:00"} data: {"date":"2025-01-31 04:01:24.376864 -05:00"} data: {"date":"2025-01-31 04:01:25.382756 -05:00"} data: {"date":"2025-01-31 04:01:26.385418 -05:00"} data: {"date":"2025-01-31 04:01:27.387723 -05:00"} data: {"date":"2025-01-31 04:01:28.390407 -05:00"} ...
Embedded Store
Embed cross.stream for real-time state and event
streaming. Append-only frames, automatic indexing, content-addressed storage.
Enable with --store <path>. Add --services to enable xs actors, services,
and actions - external clients can register automation via the store's API
(e.g., xs append ./store echo.register ...).
$ http-nu :3001 --store ./store ./serve.nu
Use --topic <name> to load the handler closure from a store topic instead of a
script file. With -w, the server live-reloads whenever the topic is updated:
$ http-nu :3001 --store ./store --topic serve -w # In another terminal: $ '{|req| "hello, world"}' | xs append ./store/sock serve
If the topic doesn't exist yet, the server shows a placeholder page with instructions until a handler is appended.
Templates can also load from the store using .mj --topic and
.mj compile --topic - see Templates.
Commands available in handlers:
| Command | Description |
|---|---|
.cat |
Read frames (-f follow, -n new, -T topic) |
.last |
Get latest frame for topic (--follow stream) |
.append |
Write frame to topic (--meta for metadata) |
.get |
Retrieve frame by ID |
.remove |
Remove frame by ID |
.cas |
Content-addressable storage operations |
.id |
Generate/unpack/pack SCRU128 IDs |
SSE with store:
{|req|
.last quotes --follow
| each {|frame| $frame.meta | to datastar-patch-elements }
| to sse
}See the xs documentation to learn more.
Reverse Proxy
You can proxy HTTP requests to backend servers using the .reverse-proxy
command. This command takes a target URL and an optional configuration record.
When you call .reverse-proxy, it forwards the incoming request to the
specified backend server and returns the response. Any subsequent output in the
closure will be ignored.
What gets forwarded:
- HTTP method (GET, POST, PUT, etc.)
- Request path and query parameters
- All request headers (with Host header handling based on
preserve_host) - Request body (whatever you pipe into the command)
Host header behavior:
- By default: Preserves the original client's Host header
(
preserve_host: true) - With
preserve_host: false: Sets Host header to match the target backend hostname
Basic Usage
# Simple proxy to backend server $ http-nu :3001 -c '{|req| .reverse-proxy "http://localhost:8080"}'
Configuration Options
The optional second parameter allows you to customize the proxy behavior:
.reverse-proxy <target_url> { headers?: {<key>: <value>} # Additional headers to add preserve_host?: bool # Keep original Host header (default: true) strip_prefix?: string # Remove path prefix before forwarding query?: {<key>: <value>} # Replace query parameters (Nu record) }
Examples
Add custom headers:
$ http-nu :3001 -c '{|req| .reverse-proxy "http://api.example.com" { headers: { "X-API-Key": "secret123" "X-Forwarded-Proto": "https" } } }'
API gateway with path stripping:
$ http-nu :3001 -c '{|req| .reverse-proxy "http://localhost:8080" { strip_prefix: "/api/v1" } }' # Request to /api/v1/users becomes /users at the backend
Forward original request body:
$ http-nu :3001 -c '{|req| .reverse-proxy "http://backend:8080"}' # If .reverse-proxy is first in closure, original body is forwarded (implicit $in)
Override request body:
$ http-nu :3001 -c '{|req| "custom body" | .reverse-proxy "http://backend:8080"}' # Whatever you pipe into .reverse-proxy becomes the request body
Modify query parameters:
$ http-nu :3001 -c '{|req| .reverse-proxy "http://backend:8080" { query: ($req.query | upsert "context-id" "smidgeons" | reject "debug") } }' # Force context-id=smidgeons, remove debug param, preserve others
Templates
Render minijinja (Jinja2-compatible) templates. Pipe a record as context.
.mj - Render templates
Three modes: file, inline, and topic (mutually exclusive).
File - renders a template from disk. {% extends %}, {% include %}, and
{% import %} resolve from the template's directory and subdirectories only -
no parent traversal (../) or absolute paths.
$ http-nu :3001 -c '{|req| $req.query | .mj "templates/page.html"}'Inline - renders the given snippet. Self-contained: no {% extends %},
{% include %}, or {% import %} resolution.
$ http-nu :3001 -c '{|req| {name: "world"} | .mj --inline "Hello {{ name }}!"}' $ curl -s localhost:3001 Hello world!
Topic (requires --store) - renders a template stored in a cross.stream
topic. {% extends %} and {% include %} resolve template names as topic names
from the same store.
{|req| $req.query | .mj --topic "page.html"}.mj compile / .mj render - Precompiled templates
Compile once, render many. Syntax errors caught at compile time. Same three
modes as .mj: file, --inline, and --topic.
let tpl = (.mj compile "templates/user.html") # or let tpl = (.mj compile --inline "{{ name }} is {{ age }}") # or (requires --store) let tpl = (.mj compile --topic "user.html") # Render with data {name: "Alice", age: 30} | .mj render $tpl
Useful for repeated rendering:
let tpl = (.mj compile --inline "{% for i in items %}{{ i }}{% endfor %}") [{items: [1,2,3]}, {items: [4,5,6]}] | each { .mj render $tpl }
Compile once at handler load, render per-request:
let page = .mj compile "templates/page.html" {|req| $req.query | .mj render $page}
With HTML DSL (accepts {__html} records directly):
use http-nu/html * let tpl = .mj compile --inline (UL (_for {item: items} (LI (_var "item")))) {items: [a b c]} | .mj render $tpl # <ul><li>a</li><li>b</li><li>c</li></ul>
Syntax Highlighting
Highlight code to HTML with CSS classes.
$ http-nu eval -c 'use http-nu/html *; PRE { "fn main() {}" | .highlight rust } | get __html' <pre><span class="source rust">... $ .highlight lang # list languages $ .highlight theme # list themes $ .highlight theme Dracula # get CSS
Markdown
Convert Markdown to HTML with syntax-highlighted code blocks.
$ http-nu eval -c '"# Hello **world**" | .md | get __html' <h1>Hello <strong>world</strong></h1>
Code blocks use .highlight internally:
$ http-nu eval -c '"```rust fn main() {} ```" | .md | get __html' <pre><code class="language-rust"><span class="source rust">...
Streaming Input
In Nushell, input only streams when received implicitly. Referencing $in
collects the entire input into memory.
# Streams: command receives input implicitly {|req| from json } # Buffers: $in collects before piping {|req| $in | from json }
For routing, dispatch must be first in the closure to receive the body. In
handlers, put body-consuming commands first:
{|req|
dispatch $req [
(route {method: "POST"} {|req ctx|
from json # receives body implicitly
})
]
}Plugins
Load Nushell plugins to extend available commands.
$ http-nu --plugin ~/.cargo/bin/nu_plugin_inc :3001 '{|req| 5 | inc}' $ curl -s localhost:3001 6
Multiple plugins:
$ http-nu --plugin ~/.cargo/bin/nu_plugin_inc --plugin ~/.cargo/bin/nu_plugin_query :3001 '{|req| ...}'
Works with eval:
$ http-nu --plugin ~/.cargo/bin/nu_plugin_inc eval -c '1 | inc' 2
Module Paths
Make module paths available with -I / --include-path:
$ http-nu -I ./lib -I ./vendor :3001 '{|req| use mymod.nu; ...}'Runtime Constants
The $HTTP_NU const is available in all scripts and reflects the CLI options
the server was started with:
$HTTP_NU # => {dev: false, datastar: true, watch: false, store: "./store", topic: null, expose: null, tls: null, services: false} $HTTP_NU.store != null # check if store is available $HTTP_NU.dev # true when --dev was passed
Embedded Modules
Routing
http-nu includes an embedded routing module for declarative request handling.
The request body is available to handlers as $in.
use http-nu/router * {|req| dispatch $req [ # Exact path match (route {path: "/health"} {|req ctx| "OK"}) # Method + path (route {method: "POST", path: "/users"} {|req ctx| "Created" | metadata set --merge {'http.response': {status: 201}} }) # Path parameters (route {path-matches: "/users/:id"} {|req ctx| $"User: ($ctx.id)" }) # Header matching (route {has-header: {accept: "application/json"}} {|req ctx| {status: "ok"} }) # Fallback (always matches) (route true {|req ctx| "Not Found" | metadata set --merge {'http.response': {status: 404}} }) ] }
Routes match in order. First match wins. Closure tests return a record (match,
context passed to handler) or null (no match). If no routes match, returns
501 Not Implemented.
Mounting sub-handlers:
mount serves a handler under a path prefix. Requests to /prefix redirect to
/prefix/, then the prefix is stripped before dispatching to the handler.
Sub-handlers see $req.mount_prefix for absolute URL reconstruction.
let api = source api/serve.nu let docs = source docs/serve.nu {|req| dispatch $req [ (mount "/api" $api) (mount "/docs" $docs) (route true {|req ctx| "Not Found"}) ] }
Mounts compose -- a mounted handler can mount further sub-handlers, and
$req.mount_prefix accumulates the full prefix chain.
HTML DSL
Build HTML with Nushell. Lisp-style nesting with uppercase tags.
use http-nu/html * {|req| (HTML (HEAD (TITLE "Demo")) (BODY (H1 "Hello") (P {class: "intro"} "Built with Nushell") (UL { 1..3 | each {|n| LI $"Item ($n)" } }) ) ) }
HTML automatically prepends
<!DOCTYPE html>.
All HTML5 elements available as uppercase commands (DIV, SPAN, UL, etc.).
Attributes via record, children via args or closure. Lists from each are
automatically joined. Plain strings are auto-escaped for XSS protection;
{__html: "<b>trusted</b>"} bypasses escaping for pre-sanitized content.
style accepts a record; values can be lists for comma-separated CSS (e.g.
font-family): {style: {font-family: [Arial sans-serif] padding: 10px}}
class accepts a list: {class: [card active]}
Boolean attributes:
true renders the attribute, false omits it:
INPUT {type: "checkbox" checked: true disabled: false} # <input type="checkbox" checked>
Jinja2 Template DSL
For hot paths, _var, _for, and _if generate Jinja2 syntax that can be
compiled once and rendered repeatedly (~200x faster than the runtime DSL):
_var "user.name" # {{ user.name }} _for {item: items} (LI (_var "item")) # {% for item in items %}...{% endfor %} _if "show" (DIV "content") # {% if show %}...{% endif %}
let tpl = .mj compile --inline (UL (_for {item: items} (LI (_var "item")))) {items: [a b c]} | .mj render $tpl # <ul><li>a</li><li>b</li><li>c</li></ul>
Datastar SDK
Generate Datastar SSE events for hypermedia interactions. Follows the SDK ADR.
Use --datastar to serve the embedded JS bundle at $DATASTAR_JS_PATH
(/datastar@1.0.0-RC.8.js) with immutable cache headers:
$ http-nu --datastar :3001 ./serve.nu
use http-nu/datastar * use http-nu/html * {|req| HTML ( HEAD ( SCRIPT {type: "module" src: $DATASTAR_JS_PATH} ) ) ( BODY ( DIV {"data-signals": "{count: 0}"} ( SPAN {"data-text": "$count"} "0" ) ( BUTTON {"data-on:click": "$count++"} "+1" ) ) ) }
Commands return records that pipe to to sse for streaming output.
use http-nu/datastar * use http-nu/html * {|req| # Parse signals from request (GET query param or POST body) let signals = from datastar-signals $req [ # Update DOM (DIV {id: "notifications" class: "alert"} "Profile updated!" | to datastar-patch-elements) # Or target by selector (DIV {class: "alert"} "Profile updated!" | to datastar-patch-elements --selector "#notifications") # Update signals ({count: ($signals.count + 1)} | to datastar-patch-signals) # Execute script ("console.log('updated')" | to datastar-execute-script) ] | to sse }
Commands:
to datastar-patch-elements [ --selector: string # CSS selector (omit if element has ID) --mode: string # outer, inner, replace, prepend, append, before, after, remove (default: outer) --namespace: string # Content namespace: html (default) or svg --use-view-transition # Enable CSS View Transitions API --id: string # SSE event ID for replay --retry-duration: int # Reconnection delay in ms ]: string -> record to datastar-patch-signals [ --only-if-missing # Only set signals not present on client --id: string --retry-duration: int ]: record -> record to datastar-execute-script [ --auto-remove: bool # Remove <script> after execution (default: true) --attributes: record # HTML attributes for <script> tag --id: string --retry-duration: int ]: string -> record to datastar-redirect []: string -> record # "/url" | to datastar-redirect from datastar-signals [req: record]: string -> record # $in | from datastar-signals $req
Cookies
Set and parse HTTP cookies with secure defaults.
use http-nu/http * {|req| # Parse cookies from request let cookies = $req | cookie parse # {session: "abc123", theme: "dark"} # Set cookies on response (chainable, accumulates Set-Cookie headers) "OK" | cookie set "session" $id --max-age 86400 | cookie set "theme" "dark" --no-httponly | cookie delete "old_token" }
Defaults (secure by default, opt out explicitly):
| Attribute | Default | Override |
|---|---|---|
Path |
/ |
--path |
HttpOnly |
yes | --no-httponly |
SameSite |
Lax |
--same-site Strict |
Secure |
yes | --no-secure, --dev |
Max-Age |
session | --max-age <seconds> |
Domain |
none | --domain |
Use --dev to omit the Secure flag for local HTTP development:
$ http-nu --dev :3001 ./serve.nu
Commands:
cookie parse []: record -> record # $req | cookie parse cookie set [ name: string value: string --max-age: int # Cookie lifetime in seconds --path: string # Default: "/" --domain: string --no-httponly # Allow JavaScript access --no-secure # Omit Secure flag even in prod --same-site: string # Lax (default), Strict, or None ]: any -> any cookie delete [ name: string --path: string # Must match original (default: "/") --domain: string # Must match original ]: any -> any
Eval Subcommand
Test http-nu commands without running a server.
# From command line $ http-nu eval -c '1 + 2' 3 # From file $ http-nu eval script.nu # From stdin $ echo '1 + 2' | http-nu eval - 3 # Test .mj commands $ http-nu eval -c '.mj compile --inline "Hello, {{ name }}" | describe' CompiledTemplate
Unit Testing Endpoints
source loads a handler script and returns the closure. do invokes it with a
request record. assert checks the response.
# test.nu use std/assert const script_dir = path self | path dirname let handler = source ($script_dir | path join serve.nu) let response = do $handler {method: GET, path: "/", headers: {}} assert ($response | str contains "<h1>State in the Right Place</h1>")
See examples/tao/test.nu.
Building and Releases
This project uses Dagger for cross-platform containerized builds that run identically locally and in CI. This means you can test builds on your machine before pushing tags to trigger releases.
Available Build Targets
- Windows (
windows-build) - macOS ARM64 (
darwin-build) - Linux ARM64 (
linux-arm-64-build) - Linux AMD64 (
linux-amd-64-build)
Examples
Build a Windows binary locally:
dagger call windows-build --src upload --src "." export --path ./dist/
Get a throwaway terminal inside the Windows builder for debugging:
dagger call windows-env --src upload --src "." terminalNote: Requires Docker and the Dagger CLI.
The upload function filters files to avoid uploading everything in your local
directory.
GitHub Releases
The GitHub workflow automatically builds all platforms and creates releases when
you push a version tag (e.g., v1.0.0). Development tags containing -dev. are
marked as prereleases.
History
If you prefer POSIX to Nushell, this project has a cousin called http-sh.