Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
1# Wisp CLI
2
3A command-line tool for deploying static sites to your AT Protocol repo to be served on [wisp.place](https://wisp.place), an AT indexer to serve such sites.
4
5## Why?
6
7The PDS serves as a way to verfiably, cryptographically prove that you own your site. That it was you (or at least someone who controls your account) who uploaded it. It is also a manifest of each file in the site to ensure file integrity. Keeping hosting seperate ensures that you could move your site across other servers or even serverless solutions to ensure speedy delievery while keeping it backed by an absolute source of truth being the manifest record and the blobs of each file in your repo.
8
9## Features
10
11- Deploy static sites directly to your AT Protocol repo
12- Supports both OAuth and app password authentication
13- Preserves directory structure and file integrity
14
15## Soon
16
17-- Host sites
18-- Manage and delete sites
19-- Metrics and logs for self hosting.
20
21## Installation
22
23### From Source
24
25```bash
26cargo build --release
27```
28
29Check out the build scripts for cross complation using nix-shell.
30
31The binary will be available at `target/release/wisp-cli`.
32
33## Usage
34
35### Commands
36
37The CLI supports three main commands:
38- **deploy**: Upload a site to your PDS (default command)
39- **pull**: Download a site from a PDS to a local directory
40- **serve**: Serve a site locally with real-time firehose updates
41
42### Basic Deployment
43
44Deploy the current directory:
45
46```bash
47wisp-cli nekomimi.pet --path . --site my-site
48```
49
50Deploy a specific directory:
51
52```bash
53wisp-cli alice.bsky.social --path ./dist/ --site my-site
54```
55
56Or use the explicit `deploy` subcommand:
57
58```bash
59wisp-cli deploy alice.bsky.social --path ./dist/ --site my-site
60```
61
62### Pull a Site
63
64Download a site from a PDS to a local directory:
65
66```bash
67wisp-cli pull alice.bsky.social --site my-site --path ./downloaded-site
68```
69
70This will download all files from the site to the specified directory.
71
72### Serve a Site Locally
73
74Serve a site locally with real-time updates from the firehose:
75
76```bash
77wisp-cli serve alice.bsky.social --site my-site --path ./site --port 8080
78```
79
80This will:
811. Download the site to the specified path
822. Start a local server on the specified port (default: 8080)
833. Watch the firehose for updates and automatically reload files when changed
84
85### Authentication Methods
86
87#### OAuth (Recommended)
88
89By default, the CLI uses OAuth authentication with a local loopback server:
90
91```bash
92wisp-cli alice.bsky.social --path ./my-site --site my-site
93```
94
95This will:
961. Open your browser for authentication
972. Save the session to a file (default: `/tmp/wisp-oauth-session.json`)
983. Reuse the session for future deployments
99
100Specify a custom session file location:
101
102```bash
103wisp-cli alice.bsky.social --path ./my-site --site my-site --store ~/.wisp-session.json
104```
105
106#### App Password
107
108For headless environments or CI/CD, use an app password:
109
110```bash
111wisp-cli alice.bsky.social --path ./my-site --site my-site --password YOUR_APP_PASSWORD
112```
113
114**Note:** When using `--password`, the `--store` option is ignored.
115
116## Command-Line Options
117
118### Deploy Command
119
120```
121wisp-cli [deploy] [OPTIONS] <INPUT>
122
123Arguments:
124 <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL
125
126Options:
127 -p, --path <PATH> Path to the directory containing your static site [default: .]
128 -s, --site <SITE> Site name (defaults to directory name)
129 --store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json]
130 --password <PASSWORD> App Password for authentication (alternative to OAuth)
131 --directory Enable directory listing mode for paths without index files
132 --spa Enable SPA mode (serve index.html for all routes)
133 -y, --yes Skip confirmation prompts (automatically accept warnings)
134 -h, --help Print help
135 -V, --version Print version
136```
137
138### Pull Command
139
140```
141wisp-cli pull [OPTIONS] --site <SITE> <INPUT>
142
143Arguments:
144 <INPUT> Handle (e.g., alice.bsky.social) or DID
145
146Options:
147 -s, --site <SITE> Site name (record key)
148 -p, --path <PATH> Output directory for the downloaded site [default: .]
149 -h, --help Print help
150```
151
152### Serve Command
153
154```
155wisp-cli serve [OPTIONS] --site <SITE> <INPUT>
156
157Arguments:
158 <INPUT> Handle (e.g., alice.bsky.social) or DID
159
160Options:
161 -s, --site <SITE> Site name (record key)
162 -p, --path <PATH> Output directory for the site files [default: .]
163 -P, --port <PORT> Port to serve on [default: 8080]
164 -h, --help Print help
165```
166
167## How It Works
168
1691. **Authentication**: Authenticates using OAuth or app password
1702. **File Processing**:
171 - Recursively walks the directory tree
172 - Skips hidden files (starting with `.`)
173 - Detects MIME types automatically
174 - Compresses files with gzip
175 - Base64 encodes compressed content
1763. **Upload**:
177 - Uploads files as blobs to your PDS
178 - Processes up to 5 files concurrently
179 - Creates a `place.wisp.fs` record with the site manifest
1804. **Deployment**: Site is immediately available at `https://sites.wisp.place/{did}/{site-name}`
181
182## File Processing
183
184All files are automatically:
185
186- **Compressed** with gzip (level 9)
187- **Base64 encoded** to bypass PDS content sniffing
188- **Uploaded** as `application/octet-stream` blobs
189- **Stored** with original MIME type metadata
190
191The hosting service automatically decompresses non HTML/CSS/JS files when serving them.
192
193## Limitations
194
195- **Max file size**: 100MB per file (after compression) (this is a PDS limit, but not enforced by the CLI in case yours is higher)
196- **Max file count**: 2000 files
197- **Site name** must follow AT Protocol rkey format rules (alphanumeric, hyphens, underscores)
198
199## Deploy with CI/CD
200
201### GitHub Actions
202
203```yaml
204name: Deploy to Wisp
205on:
206 push:
207 branches: [main]
208
209jobs:
210 deploy:
211 runs-on: ubuntu-latest
212 steps:
213 - uses: actions/checkout@v3
214
215 - name: Setup Node
216 uses: actions/setup-node@v3
217 with:
218 node-version: '25'
219
220 - name: Install dependencies
221 run: npm install
222
223 - name: Build site
224 run: npm run build
225
226 - name: Download Wisp CLI
227 run: |
228 curl -L https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
229 chmod +x wisp-cli
230
231 - name: Deploy to Wisp
232 env:
233 WISP_APP_PASSWORD: ${{ secrets.WISP_APP_PASSWORD }}
234 run: |
235 ./wisp-cli alice.bsky.social \
236 --path ./dist \
237 --site my-site \
238 --password "$WISP_APP_PASSWORD"
239```
240
241### Tangled.org
242
243```yaml
244when:
245 - event: ['push']
246 branch: ['main']
247 - event: ['manual']
248
249engine: 'nixery'
250
251clone:
252 skip: false
253 depth: 1
254 submodules: false
255
256dependencies:
257 nixpkgs:
258 - nodejs
259 - coreutils
260 - curl
261 github:NixOS/nixpkgs/nixpkgs-unstable:
262 - bun
263
264environment:
265 SITE_PATH: 'dist'
266 SITE_NAME: 'my-site'
267 WISP_HANDLE: 'your-handle.bsky.social'
268
269steps:
270 - name: build site
271 command: |
272 export PATH="$HOME/.nix-profile/bin:$PATH"
273
274 # regenerate lockfile
275 rm package-lock.json bun.lock
276 bun install @rolldown/binding-linux-arm64-gnu --save-optional
277 bun install
278
279 # build with vite
280 bun node_modules/.bin/vite build
281
282 - name: deploy to wisp
283 command: |
284 # Download Wisp CLI
285 curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
286 chmod +x wisp-cli
287
288 # Deploy to Wisp
289 ./wisp-cli \
290 "$WISP_HANDLE" \
291 --path "$SITE_PATH" \
292 --site "$SITE_NAME" \
293 --password "$WISP_APP_PASSWORD"
294```
295
296### Generic Shell Script
297
298```bash
299# Use app password from environment variable
300wisp-cli alice.bsky.social --path ./dist --site my-site --password "$WISP_APP_PASSWORD"
301```
302
303## Output
304
305Upon successful deployment, you'll see:
306
307```
308Deployed site 'my-site': at://did:plc:abc123xyz/place.wisp.fs/my-site
309Available at: https://sites.wisp.place/did:plc:abc123xyz/my-site
310```
311
312### Dependencies
313
314- **jacquard**: AT Protocol client library
315- **clap**: Command-line argument parsing
316- **tokio**: Async runtime
317- **flate2**: Gzip compression
318- **base64**: Base64 encoding
319- **walkdir**: Directory traversal
320- **mime_guess**: MIME type detection
321
322## License
323
324MIT License
325
326## Contributing
327
328Just don't give me entirely claude slop especailly not in the PR description itself. You should be responsible for code you submit and aware of what it even is you're submitting.
329
330## Links
331
332- **Website**: https://wisp.place
333- **Main Repository**: https://tangled.org/@nekomimi.pet/wisp.place-monorepo
334- **AT Protocol**: https://atproto.com
335- **Jacquard Library**: https://tangled.org/@nonbinary.computer/jacquard
336
337## Support
338
339For issues and questions:
340- Check the main wisp.place documentation
341- Open an issue in the main repository