## fs

Filesystem operations. No `require()` needed.

### Reading & Writing

- `fs.read(path)` → string — Read entire file as UTF-8 text
- `fs.read_bytes(path)` → string — Read entire file as raw bytes (binary-safe)
- `fs.write(path, str)` → nil — Write UTF-8 string to file (creates parent dirs)
- `fs.write_bytes(path, data)` → nil — Write raw bytes to file (binary-safe, creates parent dirs)

### File Operations

- `fs.remove(path)` → nil — Remove file or directory (recursive for dirs)
- `fs.copy(src, dst)` → bytes_copied — Copy file
- `fs.rename(src, dst)` → nil — Move/rename file or directory
- `fs.chmod(path, mode)` → nil — Set file permissions (e.g. `"755"`)

### Directory Operations

- `fs.mkdir(path)` → nil — Create directory (and parents)
- `fs.list(path)` → `[{name, type}]` — List directory entries. `type`: `"file"`, `"directory"`, `"symlink"`
- `fs.readdir(path, opts?)` → `[{name, path, type}]` — Recursive directory listing. `opts`: `{depth = N}` for max recursion
- `fs.glob(pattern)` → `[path]` — Glob pattern matching, returns array of path strings
- `fs.tempdir()` → path — Create a temporary directory

### Metadata

- `fs.stat(path)` → `{size, type, modified, created, permissions}` — File metadata
- `fs.exists(path)` → bool — Check if path exists

### Binary I/O

`fs.read_bytes` and `fs.write_bytes` handle files with arbitrary byte content
(images, WASM, protobuf, compressed data). Lua strings can hold any bytes,
so the returned value works with `http.serve()` response bodies, `base64.encode()`,
or any other builtin that accepts strings.

```lua
-- Copy a binary file
local data = fs.read_bytes("image.png")
fs.write_bytes("copy.png", data)

-- Serve binary files via http.serve()
http.serve(8080, {
  GET = {
    ["/*"] = function(req)
      return { status = 200, body = fs.read_bytes("static" .. req.path) }
    end,
  },
})
```
