Metadata-Version: 2.4
Name: vsnap
Version: 0.1.0
Summary: Build Vercel Sandbox snapshots from declarative .snap files
Project-URL: Repository, https://github.com/gscho/vsnap
Project-URL: Issues, https://github.com/gscho/vsnap/issues
Author: Greg Schofield
License-Expression: MIT
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: vercel>=0.5.0
Provides-Extra: dev
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.9.0; extra == 'dev'
Description-Content-Type: text/markdown

# vsnap

Build Vercel Sandbox snapshots from declarative `.snap` files.

`.snap` files are declarative build recipes -- like a Dockerfile, but for Vercel Sandbox environments. Each directive is a single line that says what to do: install packages, run commands, copy files, write configs. No programming language, no boilerplate. Just a clean, sequential recipe.

## Installation

```bash
uv add vsnap
# or: pip install vsnap
```

Requires Python 3.10+ and a Vercel project with sandbox access.

## Quick Start

Create a `.snap` file:

```
# my-app.snap
runtime node22 {
    env NODE_ENV development
    expose 3000
}

workdir /vercel/sandbox/my-app

install git jq

run "npm install"
run "npm run build"
```

Build a snapshot:

```bash
vsnap build my-app.snap
```

The CLI auto-loads `.env.local` for Vercel OIDC credentials. Run `vercel env pull .env.local` once to set up.

## CLI

```bash
vsnap build <file.snap>                    # Build and snapshot (streams output by default)
vsnap build <file.snap> -q                 # Quiet mode (suppress streaming output)
vsnap build <file.snap> --dry-run          # Parse and validate only, print the build plan
vsnap build <file.snap> --dry-run --output json  # Machine-readable build plan
vsnap build <file.snap> --output json      # JSON build result (agent-friendly)
vsnap build <file.snap> --env KEY=VALUE    # Extra env vars (repeatable)
vsnap build <file.snap> --token <token>    # Vercel API token (overrides .env.local)
```

## DSL Reference

### Source Directives

Every `.snap` file starts with a source directive that defines where the sandbox comes from. Exactly one is required.

#### `runtime` -- From a runtime image

```
runtime node22

runtime node22 {
    vcpus 2
    timeout 600000
    env NODE_ENV production
    env CI true
    expose 3000 9229
    network allow-all
}
```

Valid runtimes: `node22`, `node24`, `python3.13`

#### `snapshot` -- From an existing snapshot

```
snapshot snap_abc123

snapshot snap_abc123 {
    runtime node22
    vcpus 2
}
```

#### `git` -- From a git repository

```
git https://github.com/user/repo.git {
    runtime node22
    depth 1
    revision main
}
```

#### `tarball` -- From an archive

```
tarball https://example.com/archive.tar.gz {
    runtime node22
}
```

#### Source block sub-directives

| Sub-directive | Description |
|---|---|
| `vcpus <n>` | Number of vCPUs |
| `timeout <ms>` | Sandbox lifetime in ms (default: 30 min) |
| `env <KEY> <VALUE>` | Creation-time environment variable (repeatable) |
| `expose <port> [<port> ...]` | Ports to expose |
| `network allow-all \| deny-all` | Network policy |
| `network { allow ...; subnet_allow ...; subnet_deny ... }` | Custom network policy |
| `runtime <name>` | Runtime override (snapshot/git/tarball only) |
| `depth <n>` | Git clone depth (git only) |
| `revision <ref>` | Git branch/tag/commit (git only) |
| `username <str>` | Git auth username (git only) |
| `password <str>` | Git auth password (git only) |

### Step Directives

Steps execute in order, top to bottom. They appear after the source directive.

#### `run` -- Execute a shell command

```
run "npm install"

run "systemctl start nginx" {
    sudo
}

run "npm run build" {
    env NODE_ENV production
    cwd /opt/build
}

# Multi-command block (each line = separate step)
run {
    npm install
    npm run build
    npm prune --production
}
```

| Sub-directive | Description |
|---|---|
| `sudo` | Run with sudo |
| `cwd <path>` | Working directory override (this command only) |
| `env <KEY> <VALUE>` | Per-command env var (repeatable) |

#### `script` -- Execute a multi-line script

```
# From a local file
script ./scripts/setup.sh

# Inline (escape sequences in double quotes)
script "echo hello\necho world"

# Heredoc
script <<EOF
set -e
curl -fsSL https://example.com/install.sh | bash
ln -sf /usr/local/lib/tool /usr/local/bin/tool
EOF

# With options
script ./scripts/deploy.sh {
    sudo
    shell python3
    cwd /opt/build
    env CI true
}
```

| Sub-directive | Description |
|---|---|
| `sudo` | Run with sudo |
| `shell <name>` | Shell interpreter (default: `bash`) |
| `cwd <path>` | Working directory override |
| `env <KEY> <VALUE>` | Per-script env var (repeatable) |

#### `install` / `remove` / `update` -- System packages

```
install git curl jq make
remove nano
update curl
```

Uses `dnf` on Amazon Linux 2023 (the sandbox OS). Runs with sudo automatically.

#### `copy` -- Upload local files into the sandbox

```
copy ./assets/config.json /vercel/share/config.json
copy ./scripts/start.sh /usr/local/bin/start.sh {
    mode 0755
}
copy ./assets/config/ /vercel/share/.config/
```

Paths are relative to the `.snap` file's directory.

#### `file` -- Write inline content to a file

```
file /etc/config.json '{"port": 3000}'

file /vercel/sandbox/config/app.json <<EOF
{
    "port": 3000,
    "debug": false
}
EOF

file /opt/script.sh "#!/bin/bash\nexec node server.js" {
    mode 0755
}
```

Default mode is `0644`.

#### `download` -- Download a remote file

```
download https://example.com/tool.tar.gz {
    dest /usr/local/bin
    extract
    checksum sha256:abc123...
}

download https://example.com/config.json {
    dest /etc/app/config.json
}
```

| Sub-directive | Description |
|---|---|
| `dest <path>` | Destination path in sandbox (required) |
| `extract` | Extract archive (tar/zip) after download |
| `checksum <algo>:<hash>` | Verify checksum |

#### `workdir` -- Set the working directory

```
workdir /vercel/sandbox/my-app
run "npm install"               # runs in /vercel/sandbox/my-app
```

Creates the directory implicitly.

#### `mkdir` -- Create a directory

```
mkdir /vercel/sandbox/my-app/src
```

#### `env` -- Persist an environment variable

```
env CUSTOM_VAR value
env NODE_ENV production
```

Writes to `/etc/environment` so the variable survives snapshot restore. This is different from `env` inside a source block, which sets creation-time env vars.

## Syntax

`.snap` files use a Caddyfile-inspired syntax:

- **One directive per line.** Each line starts with a directive name followed by arguments.
- **Bare words.** No quotes needed for simple values. Use double or single quotes for values with spaces.
- **Blocks.** Use `{ }` for sub-directives (options) or multi-line content.
- **Heredocs.** Use `<<MARKER ... MARKER` for multi-line file content and scripts.
- **Comments.** `#` to end of line.
- **Booleans by presence.** `sudo` means true. Absence means false.

### Quoting

```
run "echo hello world"          # double quotes for spaces
run 'echo hello world'          # single quotes also work
run "echo \"quoted\""           # escape quotes in double-quoted strings
script "line1\nline2"           # \n = newline, \t = tab, \\ = backslash
install git jq                  # bare words when no spaces needed
```

### Escape sequences (double quotes only)

| Escape | Result |
|---|---|
| `\n` | Newline |
| `\t` | Tab |
| `\\` | Backslash |
| `\"` | Double quote |
