@zerodeploy/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +248 -292
  2. package/dist/cli.js +242 -29
  3. package/package.json +1 -6
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # ZeroDeploy CLI
2
2
 
3
- Command-line interface for deploying static sites to ZeroDeploy.
3
+ Command-line interface for deploying static sites and SPAs to [ZeroDeploy](https://zerodeploy.dev).
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- # From the monorepo root
9
- bun install
10
-
11
- # Link CLI globally
12
- cd apps/cli && bun link
8
+ npm install -g @zerodeploy/cli
13
9
  ```
14
10
 
15
- After linking, the `zerodeploy` command is available globally.
11
+ Or use directly with npx:
12
+
13
+ ```bash
14
+ npx @zerodeploy/cli deploy
15
+ ```
16
16
 
17
17
  ## Quick Start
18
18
 
@@ -24,26 +24,26 @@ zerodeploy login
24
24
  zerodeploy org create "My Company"
25
25
 
26
26
  # 3. Create a site
27
- zerodeploy site create my-company "My Website"
27
+ zerodeploy site create my-company "My Website" --subdomain my-website
28
28
 
29
29
  # 4. Deploy your site
30
30
  zerodeploy deploy my-website --org my-company --dir ./dist
31
31
  ```
32
32
 
33
+ Your site will be live at `https://my-website.zerodeploy.app`
34
+
33
35
  ## Commands
34
36
 
35
37
  ### Authentication
36
38
 
37
39
  #### `zerodeploy login`
38
40
 
39
- Authenticate with GitHub OAuth. Opens browser for authentication and stores JWT token locally.
41
+ Authenticate with GitHub OAuth. Opens your browser for authentication.
40
42
 
41
43
  ```bash
42
44
  zerodeploy login
43
45
  ```
44
46
 
45
- Token is stored at `~/.zerodeploy/token`.
46
-
47
47
  #### `zerodeploy logout`
48
48
 
49
49
  Clear stored authentication token.
@@ -60,449 +60,397 @@ Display current logged-in user information.
60
60
  zerodeploy whoami
61
61
  ```
62
62
 
63
- Output:
64
- ```
65
- Logged in as:
66
- Username: johndoe
67
- User ID: 019b1234-5678-...
68
- Admin: No
69
- ```
70
-
71
63
  ### Organizations
72
64
 
73
65
  #### `zerodeploy org list`
74
66
 
75
- List all organizations you own.
67
+ List all organizations you have access to.
76
68
 
77
69
  ```bash
78
70
  zerodeploy org list
79
71
  ```
80
72
 
81
- Output:
82
- ```
83
- Organizations:
84
- - My Company [paid] (id: 019b1234-...)
85
- - Personal [paid] (id: 019b5678-...)
86
- ```
87
-
88
73
  #### `zerodeploy org create <name>`
89
74
 
90
- Create a new organization. Slug is auto-generated from the name.
75
+ Create a new organization.
91
76
 
92
77
  ```bash
93
78
  zerodeploy org create "My Company"
94
79
  ```
95
80
 
96
- Output:
97
- ```
98
- Organization created: My Company
99
- id: 019b1234-5678-...
81
+ #### `zerodeploy org delete <orgSlug>`
82
+
83
+ Delete an organization (must have no sites).
84
+
85
+ ```bash
86
+ zerodeploy org delete my-company
100
87
  ```
101
88
 
102
89
  ### Sites
103
90
 
104
91
  #### `zerodeploy site list <orgSlug>`
105
92
 
106
- List all sites in an organization. Shows linked GitHub repositories if configured.
93
+ List all sites in an organization.
107
94
 
108
95
  ```bash
109
96
  zerodeploy site list my-company
110
97
  ```
111
98
 
112
- Output:
113
- ```
114
- Sites:
115
- my-website My Website -> company/frontend
116
- landing-page Landing Page
117
- docs Documentation -> company/monorepo
118
- ```
119
-
120
- #### `zerodeploy site create <orgSlug> <name> [options]`
99
+ #### `zerodeploy site create <orgSlug> <name> --subdomain <subdomain>`
121
100
 
122
- Create a new site in an organization. Slug is auto-generated from the name.
101
+ Create a new site in an organization.
123
102
 
124
103
  **Options:**
104
+ - `--subdomain <subdomain>` - Subdomain for the site (required, globally unique)
125
105
  - `--repo <owner/repo>` - Link to a GitHub repository
126
106
 
127
107
  ```bash
128
108
  # Create a site
129
- zerodeploy site create my-company "My Website"
109
+ zerodeploy site create my-company "My Website" --subdomain my-website
130
110
 
131
111
  # Create a site linked to a GitHub repo
132
- zerodeploy site create my-company "Dashboard" --repo company/monorepo
112
+ zerodeploy site create my-company "Dashboard" --subdomain dashboard --repo company/monorepo
133
113
  ```
134
114
 
135
- Output:
136
- ```
137
- Site created: Dashboard
138
- ID: 019b1234-5678-...
139
- Repo: company/monorepo
140
- ```
141
-
142
- #### `zerodeploy site link <orgSlug> <siteSlug> <repo>`
143
-
144
- Link an existing site to a GitHub repository. One repository can be linked to multiple sites (monorepo support).
145
-
146
- ```bash
147
- zerodeploy site link my-company dashboard company/monorepo
148
- ```
149
-
150
- Output:
151
- ```
152
- Site "Dashboard" linked to company/monorepo
153
- ```
115
+ Your site will be available at `https://<subdomain>.zerodeploy.app`
154
116
 
155
- #### `zerodeploy site unlink <orgSlug> <siteSlug>`
117
+ #### `zerodeploy site delete <siteSlug> --org <orgSlug>`
156
118
 
157
- Remove the GitHub repository link from a site.
119
+ Delete a site and all its deployments.
158
120
 
159
121
  ```bash
160
- zerodeploy site unlink my-company dashboard
161
- ```
162
-
163
- Output:
164
- ```
165
- Site "Dashboard" unlinked from GitHub repository
166
- ```
167
-
168
- #### GitHub Repository Linking
169
-
170
- Sites can be linked to GitHub repositories to enable:
171
- - Tracking which repo deploys to which site
172
- - Future: Automatic PR preview comments
173
- - Future: GitHub webhook integration
174
-
175
- **Monorepo support:** One repository can be linked to multiple sites:
176
-
177
- ```
178
- company/monorepo
179
- ├── apps/marketing → site: marketing
180
- ├── apps/dashboard → site: dashboard
181
- └── apps/docs → site: docs
122
+ zerodeploy site delete my-website --org my-company
182
123
  ```
183
124
 
184
125
  ### Deployment
185
126
 
186
127
  #### `zerodeploy deploy <siteSlug> [options]`
187
128
 
188
- Deploy a directory to a site. Uploads all files and makes the deployment live.
189
-
190
- **Arguments:**
191
- - `siteSlug` - The site slug to deploy to
129
+ Deploy a directory to a site.
192
130
 
193
131
  **Options:**
194
132
  - `--org <orgSlug>` - Organization slug (required)
195
133
  - `--dir <directory>` - Directory to deploy (default: auto-detect)
196
134
  - `--build` - Run build command before deploying
197
- - `--no-build` - Skip build step (even if output dir doesn't exist)
198
- - `--build-command <cmd>` - Override the build command
135
+ - `--build-command <cmd>` - Custom build command
199
136
  - `--install` - Run install command before building
200
137
 
201
- **CI/CD Options:**
202
- - `--pr <number>` - PR number (for GitHub Actions)
203
- - `--pr-title <title>` - PR title
204
- - `--commit <sha>` - Commit SHA
205
- - `--commit-message <message>` - Commit message
206
- - `--branch <branch>` - Branch name
207
- - `--github-output` - Output deployment info in GitHub Actions format
208
-
209
138
  ```bash
210
139
  # Deploy specific directory
211
140
  zerodeploy deploy my-website --org my-company --dir ./dist
212
141
 
213
- # Auto-detect build directory (looks for dist/, build/, out/, public/)
142
+ # Auto-detect build directory
214
143
  zerodeploy deploy my-website --org my-company
215
144
 
216
- # Build and deploy (auto-detects framework)
145
+ # Build and deploy
217
146
  zerodeploy deploy my-website --org my-company --build
218
147
 
219
- # Install, build, and deploy
148
+ # Install dependencies, build, and deploy
220
149
  zerodeploy deploy my-website --org my-company --install --build
221
-
222
- # Custom build command
223
- zerodeploy deploy my-website --org my-company --build-command "npm run build:prod"
224
150
  ```
225
151
 
226
152
  Output:
227
153
  ```
228
- Detected: Vite
229
-
230
- Building...
231
- > npm run build
232
-
233
154
  Deploying: ./dist
234
155
  Found 42 files (1.2 MB)
235
- Created deployment: 019b1234-5678-...
236
156
  Uploading files...
237
157
  [100%] 42/42 files
238
158
 
239
159
  Deployment successful!
240
- URL: https://my-website_my-company.zerodeploy.app
241
- Preview: https://019b1234_my-website_my-company.zerodeploy.app
160
+ URL: https://my-website.zerodeploy.app
161
+ Preview: https://019b1234-my-website.zerodeploy.app
242
162
  ```
243
163
 
244
- #### Preview Deployments
245
-
246
- Each deployment gets a unique preview URL that remains accessible even after new deployments:
164
+ #### `zerodeploy deployments list <siteSlug> --org <orgSlug>`
247
165
 
248
- - **Production URL**: `https://site_org.zerodeploy.app` - Always serves the current deployment
249
- - **Preview URL**: `https://{shortId}_site_org.zerodeploy.app` - Serves a specific deployment
166
+ List deployment history for a site.
250
167
 
251
- Preview URLs use the first 8 characters of the deployment ID. This is useful for:
252
- - Reviewing changes before promoting to production
253
- - Sharing specific versions with stakeholders
254
- - Rolling back by comparing different deployments
255
- - PR preview deployments
168
+ ```bash
169
+ zerodeploy deployments list my-website --org my-company
170
+ ```
256
171
 
257
- #### GitHub Actions Integration
172
+ **Options:**
173
+ - `--limit <number>` - Number of deployments to show (default: 10)
258
174
 
259
- ZeroDeploy integrates with GitHub Actions for automated deployments and PR previews.
175
+ #### `zerodeploy rollback <siteSlug> --org <orgSlug>`
260
176
 
261
- **Setup:**
262
- 1. Generate a token: Run `zerodeploy login` and copy the token from `~/.zerodeploy/token`
263
- 2. Add the token to your repository secrets as `ZERODEPLOY_TOKEN`
264
- 3. Copy the workflow template to `.github/workflows/deploy.yml`
177
+ Rollback to a previous deployment.
265
178
 
266
- **Minimal workflow:**
267
- ```yaml
268
- name: Deploy
269
- on:
270
- push:
271
- branches: [main]
179
+ ```bash
180
+ # Rollback to the previous deployment
181
+ zerodeploy rollback my-website --org my-company
272
182
 
273
- jobs:
274
- deploy:
275
- runs-on: ubuntu-latest
276
- steps:
277
- - uses: actions/checkout@v4
278
- - uses: oven-sh/setup-bun@v2
279
- - run: bun install
280
- - run: bun run build
281
- - name: Deploy
282
- env:
283
- ZERODEPLOY_TOKEN: ${{ secrets.ZERODEPLOY_TOKEN }}
284
- run: bunx zerodeploy deploy my-site --org my-org
183
+ # Rollback to a specific deployment
184
+ zerodeploy rollback my-website --org my-company --to 019b1230
285
185
  ```
286
186
 
287
- **PR Preview workflow:**
288
- ```yaml
289
- - name: Deploy PR Preview
290
- if: github.event_name == 'pull_request'
291
- env:
292
- ZERODEPLOY_TOKEN: ${{ secrets.ZERODEPLOY_TOKEN }}
293
- run: |
294
- zerodeploy deploy my-site \
295
- --org my-org \
296
- --pr ${{ github.event.pull_request.number }} \
297
- --pr-title "${{ github.event.pull_request.title }}" \
298
- --commit ${{ github.sha }} \
299
- --branch ${{ github.head_ref }} \
300
- --github-output
301
- ```
187
+ ### Deploy Tokens
302
188
 
303
- The `--github-output` flag exports deployment info as GitHub Actions outputs:
304
- - `deployment_id` - The deployment ID
305
- - `deployment_url` - The production URL
306
- - `preview_url` - The unique preview URL
189
+ Deploy tokens allow CI/CD systems to authenticate without using your personal credentials.
307
190
 
308
- See `apps/cli/examples/github-actions/` for full workflow templates including PR comment automation
191
+ #### `zerodeploy token create <name> --org <org> --site <site>`
309
192
 
310
- #### Framework Auto-Detection
193
+ Create a deploy token for CI/CD.
311
194
 
312
- The CLI automatically detects your framework from `package.json` and uses the appropriate build command and output directory:
195
+ ```bash
196
+ zerodeploy token create "GitHub Actions" --org my-company --site my-website
197
+ ```
313
198
 
314
- | Framework | Detection | Build Command | Output Dir |
315
- |-----------|-----------|---------------|------------|
316
- | Vite | `vite` in deps | `npm run build` | `dist/` |
317
- | Next.js | `next` in deps | `npm run build` | `out/` |
318
- | Create React App | `react-scripts` in deps | `npm run build` | `build/` |
319
- | Vue CLI | `@vue/cli-service` in deps | `npm run build` | `dist/` |
320
- | Nuxt | `nuxt` in deps | `npm run build` | `dist/` |
321
- | Astro | `astro` in deps | `npm run build` | `dist/` |
322
- | SvelteKit | `@sveltejs/kit` in deps | `npm run build` | `build/` |
323
- | Gatsby | `gatsby` in deps | `npm run build` | `public/` |
324
- | Remix | `@remix-run/dev` in deps | `npm run build` | `public/build/` |
325
- | Parcel | `parcel` in deps | `npm run build` | `dist/` |
199
+ Save the token securely - it will only be shown once.
326
200
 
327
- **Auto-build behavior:**
328
- - If a framework is detected but the output directory doesn't exist, the CLI automatically runs the build
329
- - Use `--no-build` to skip the build step
330
- - Use `--build-command` to override the detected build command
201
+ #### `zerodeploy token list --org <org> --site <site>`
331
202
 
332
- **Auto-detected directories (fallback):**
333
- If no framework is detected, the CLI looks for common build output directories:
334
- 1. `dist/` (Vite, Rollup)
335
- 2. `build/` (Create React App)
336
- 3. `out/` (Next.js static export)
337
- 4. `public/` (static sites)
203
+ List deploy tokens for a site.
338
204
 
339
- **Ignored files:**
340
- The following are automatically excluded from deployments:
341
- - `node_modules/`
342
- - `.git/`
343
- - `.env`, `.env.*` files
344
- - Hidden files starting with `.`
205
+ ```bash
206
+ zerodeploy token list --org my-company --site my-website
207
+ ```
345
208
 
346
- #### `zerodeploy deploy list <siteSlug> --org <orgSlug>`
209
+ #### `zerodeploy token delete <tokenId> --org <org> --site <site>`
347
210
 
348
- List all deployments for a site.
211
+ Delete a deploy token.
349
212
 
350
213
  ```bash
351
- zerodeploy deploy list my-website --org my-company
352
- ```
353
-
354
- Output:
214
+ zerodeploy token delete 019b1234 --org my-company --site my-website
355
215
  ```
356
- Deployments for my-company/my-website:
357
216
 
358
- 019b1234 ready 12/14/2025, 9:00:00 PM <- current
359
- 019b1230 ready 12/14/2025, 8:30:00 PM
360
- 019b1220 ready 12/14/2025, 8:00:00 PM
361
- ```
217
+ ### Custom Domains
362
218
 
363
- **Options:**
364
- - `--org <orgSlug>` - Organization slug (required)
365
- - `--limit <number>` - Number of deployments to show (default: 10)
219
+ Connect your own domain to any ZeroDeploy site with automatic SSL.
366
220
 
367
- #### `zerodeploy deploy rollback <deploymentId>`
221
+ #### `zerodeploy domain add <domain> --org <org> --site <site>`
368
222
 
369
- Rollback to a previous deployment. Sets the specified deployment as the current live deployment.
223
+ Add a custom domain to a site. Returns DNS verification instructions.
370
224
 
371
225
  ```bash
372
- zerodeploy deploy rollback 019b1230-5678-...
226
+ zerodeploy domain add www.example.com --org my-company --site my-website
373
227
  ```
374
228
 
375
- Output:
376
- ```
377
- Rolled back successfully
378
- Deployment: 019b1230-5678-...
379
- URL: https://my-website_my-company.zerodeploy.app
380
- ```
229
+ #### `zerodeploy domain verify <domain> --org <org> --site <site>`
381
230
 
382
- ### Deploy Tokens
231
+ Verify domain ownership after adding the TXT record to your DNS.
383
232
 
384
- Deploy tokens allow CI/CD systems to authenticate with ZeroDeploy without using your personal JWT.
233
+ ```bash
234
+ zerodeploy domain verify www.example.com --org my-company --site my-website
235
+ ```
385
236
 
386
- #### `zerodeploy token create <name> --org <org> --site <site>`
237
+ #### `zerodeploy domain list --org <org> --site <site>`
387
238
 
388
- Create a new deploy token for a site.
239
+ List all custom domains for a site.
389
240
 
390
241
  ```bash
391
- zerodeploy token create "GitHub Actions" --org my-company --site my-website
242
+ zerodeploy domain list --org my-company --site my-website
392
243
  ```
393
244
 
394
- Output:
245
+ #### `zerodeploy domain remove <domain> --org <org> --site <site>`
246
+
247
+ Remove a custom domain from a site.
248
+
249
+ ```bash
250
+ zerodeploy domain remove www.example.com --org my-company --site my-website
395
251
  ```
396
- Deploy token created successfully!
397
252
 
398
- Name: GitHub Actions
399
- ID: 019b1234-5678-...
253
+ #### `zerodeploy domain redirect <domain> --org <org> --site <site> --mode <mode>`
254
+
255
+ Set redirect mode for a custom domain. This allows automatic redirects between www and apex (non-www) domains.
256
+
257
+ **Options:**
258
+ - `--mode <mode>` - Redirect mode: `none`, `www_to_apex`, or `apex_to_www`
400
259
 
401
- Token (save this - it will not be shown again):
260
+ ```bash
261
+ # Redirect www.example.com to example.com
262
+ zerodeploy domain redirect example.com --org my-company --site my-website --mode www_to_apex
402
263
 
403
- abc123def456...
264
+ # Redirect example.com to www.example.com
265
+ zerodeploy domain redirect www.example.com --org my-company --site my-website --mode apex_to_www
404
266
 
405
- Usage in GitHub Actions:
406
- Add this token as a repository secret named ZERODEPLOY_TOKEN
267
+ # Disable redirects
268
+ zerodeploy domain redirect example.com --org my-company --site my-website --mode none
407
269
  ```
408
270
 
409
- **Options:**
410
- - `--org <orgSlug>` - Organization slug (required)
411
- - `--site <siteSlug>` - Site slug (required)
271
+ **Custom Domain Setup:**
412
272
 
413
- #### `zerodeploy token list --org <org> --site <site>`
273
+ 1. Add the domain: `zerodeploy domain add www.example.com --org my-org --site my-site`
274
+ 2. Add the TXT record to your DNS (shown in output)
275
+ 3. Verify ownership: `zerodeploy domain verify www.example.com --org my-org --site my-site`
276
+ 4. Add the CNAME record to your DNS (shown in output)
277
+ 5. Your site is now live at `https://www.example.com`
278
+
279
+ ## Configuration File
414
280
 
415
- List all deploy tokens for a site.
281
+ Create a `zerodeploy.json` in your project root:
416
282
 
417
283
  ```bash
418
- zerodeploy token list --org my-company --site my-website
284
+ zerodeploy init --org my-company --site my-website
419
285
  ```
420
286
 
421
- Output:
422
- ```
423
- Deploy tokens for my-company/my-website:
287
+ This creates:
424
288
 
425
- 019b1234 GitHub Actions Created: 12/14/2025 Last used: 12/14/2025
426
- 019b5678 CircleCI Created: 12/10/2025 Last used: Never
289
+ ```json
290
+ {
291
+ "org": "my-company",
292
+ "site": "my-website"
293
+ }
427
294
  ```
428
295
 
429
- #### `zerodeploy token delete <tokenId> --org <org> --site <site>`
430
-
431
- Delete a deploy token. You can use the full ID or the first 8 characters.
296
+ Then deploy with just:
432
297
 
433
298
  ```bash
434
- zerodeploy token delete 019b1234 --org my-company --site my-website
299
+ zerodeploy deploy
435
300
  ```
436
301
 
437
- ## Deployment Flow
302
+ ## Deployed Sites
438
303
 
439
- 1. **Create deployment** - API creates a new deployment record with `uploading` status
440
- 2. **Upload files** - CLI scans directory and uploads each file as base64 to R2
441
- 3. **Finalize** - API marks deployment as `ready` and sets it as the current deployment
442
- 4. **Live** - Site is immediately accessible at the deployment URL
304
+ ### URLs
443
305
 
444
- ## Accessing Deployed Sites
306
+ Each site gets a production URL based on its subdomain:
445
307
 
446
- After deployment, sites are accessible at:
447
-
448
- **Local development:**
449
308
  ```
450
- http://localhost:8787/serve/<orgSlug>/<siteSlug>/
309
+ https://<subdomain>.zerodeploy.app
451
310
  ```
452
311
 
453
- **Production:**
312
+ ### Preview URLs
313
+
314
+ Every deployment also gets a unique preview URL that remains accessible even after new deployments:
315
+
454
316
  ```
455
- https://<siteSlug>_<orgSlug>.zerodeploy.app
317
+ https://<deploymentId>-<subdomain>.zerodeploy.app
456
318
  ```
457
319
 
320
+ Preview URLs use the first 8 characters of the deployment ID. Useful for:
321
+ - Reviewing changes before promoting to production
322
+ - Sharing specific versions with stakeholders
323
+ - Comparing different deployments
324
+
458
325
  ### SPA Support
459
326
 
460
327
  ZeroDeploy automatically handles SPA (Single Page Application) routing:
461
328
  - Requests without file extensions fall back to `index.html`
462
- - Enables client-side routing for React, Vue, Angular, etc.
329
+ - Works with React Router, Vue Router, and other client-side routers
463
330
 
464
331
  ### Caching
465
332
 
466
- Appropriate cache headers are set automatically:
467
- - **HTML files**: `max-age=0, must-revalidate` (always check for updates)
468
- - **Hashed assets** (e.g., `main.abc123.js`): `max-age=31536000, immutable` (1 year)
469
- - **Other assets**: `max-age=3600, stale-while-revalidate=86400` (1 hour)
333
+ Cache headers are set automatically:
334
+ - **HTML files**: Always revalidate for fresh content
335
+ - **Hashed assets** (e.g., `main.abc123.js`): Cached for 1 year
336
+ - **Other assets**: Cached for 1 hour with background revalidation
470
337
 
471
- ## Configuration
338
+ ## CI/CD Integration
472
339
 
473
- ### Token Storage
340
+ ### GitHub Actions
474
341
 
475
- Authentication token is stored at `~/.zerodeploy/token`.
342
+ **1. Create a deploy token:**
476
343
 
477
- For CI/CD, set the `ZERODEPLOY_TOKEN` environment variable instead. The environment variable takes precedence over the file-based token.
344
+ ```bash
345
+ zerodeploy token create "GitHub Actions" --org my-company --site my-website
346
+ ```
478
347
 
479
- ### API URL
348
+ **2. Add the token to your repository secrets** as `ZERODEPLOY_TOKEN`
480
349
 
481
- By default, CLI connects to production (`https://api.zerodeploy.dev`). To use a local development server, set the `ZERODEPLOY_API_URL` environment variable:
350
+ **3. Create `.github/workflows/deploy.yml`:**
482
351
 
483
- ```bash
484
- # Point to local dev server
485
- export ZERODEPLOY_API_URL=http://localhost:8787
352
+ ```yaml
353
+ name: Deploy
354
+ on:
355
+ push:
356
+ branches: [main]
486
357
 
487
- # Or prefix individual commands
488
- ZERODEPLOY_API_URL=http://localhost:8787 zerodeploy whoami
358
+ jobs:
359
+ deploy:
360
+ runs-on: ubuntu-latest
361
+ steps:
362
+ - uses: actions/checkout@v4
363
+ - uses: actions/setup-node@v4
364
+ with:
365
+ node-version: '20'
366
+ - run: npm ci
367
+ - run: npm run build
368
+ - name: Deploy
369
+ env:
370
+ ZERODEPLOY_TOKEN: ${{ secrets.ZERODEPLOY_TOKEN }}
371
+ run: npx @zerodeploy/cli deploy my-site --org my-org
489
372
  ```
490
373
 
491
- Make sure your local API is running with `cd apps/api && bun run dev`.
374
+ **PR Preview workflow:**
492
375
 
493
- ## Development
376
+ ```yaml
377
+ name: PR Preview
378
+ on:
379
+ pull_request:
380
+
381
+ jobs:
382
+ preview:
383
+ runs-on: ubuntu-latest
384
+ steps:
385
+ - uses: actions/checkout@v4
386
+ - uses: actions/setup-node@v4
387
+ with:
388
+ node-version: '20'
389
+ - run: npm ci
390
+ - run: npm run build
391
+ - name: Deploy Preview
392
+ env:
393
+ ZERODEPLOY_TOKEN: ${{ secrets.ZERODEPLOY_TOKEN }}
394
+ run: |
395
+ npx @zerodeploy/cli deploy my-site \
396
+ --org my-org \
397
+ --pr ${{ github.event.pull_request.number }} \
398
+ --pr-title "${{ github.event.pull_request.title }}" \
399
+ --commit ${{ github.sha }} \
400
+ --branch ${{ github.head_ref }} \
401
+ --github-output
402
+ ```
403
+
404
+ The `--github-output` flag exports deployment info as GitHub Actions outputs:
405
+ - `deployment_id` - The deployment ID
406
+ - `deployment_url` - The production URL
407
+ - `preview_url` - The unique preview URL
408
+
409
+ ### Other CI/CD Systems
410
+
411
+ ZeroDeploy works with any CI/CD system. Set the `ZERODEPLOY_TOKEN` environment variable and run the deploy command:
494
412
 
495
413
  ```bash
496
- # Run CLI in development
497
- cd apps/cli
498
- bun run src/index.ts <command>
414
+ # Works with npm, yarn, pnpm, or bun
415
+ npx @zerodeploy/cli deploy my-site --org my-org
416
+ yarn dlx @zerodeploy/cli deploy my-site --org my-org
417
+ pnpm dlx @zerodeploy/cli deploy my-site --org my-org
418
+ bunx @zerodeploy/cli deploy my-site --org my-org
419
+ ```
420
+
421
+ ## Framework Auto-Detection
422
+
423
+ The CLI automatically detects your framework and uses the appropriate build command and output directory:
424
+
425
+ | Framework | Build Command | Output Dir |
426
+ |-----------|---------------|------------|
427
+ | Vite | `npm run build` | `dist/` |
428
+ | Next.js | `npm run build` | `out/` |
429
+ | Create React App | `npm run build` | `build/` |
430
+ | Vue CLI | `npm run build` | `dist/` |
431
+ | Nuxt | `npm run build` | `dist/` |
432
+ | Astro | `npm run build` | `dist/` |
433
+ | SvelteKit | `npm run build` | `build/` |
434
+ | Gatsby | `npm run build` | `public/` |
435
+ | Remix | `npm run build` | `public/build/` |
436
+ | Parcel | `npm run build` | `dist/` |
437
+
438
+ **Auto-detected directories (fallback):**
439
+ If no framework is detected, the CLI looks for: `dist/`, `build/`, `out/`, `public/`
499
440
 
500
- # Run tests
501
- bun test
441
+ **Ignored files:**
442
+ The following are automatically excluded from deployments:
443
+ - `node_modules/`
444
+ - `.git/`
445
+ - `.env`, `.env.*` files
446
+ - Hidden files starting with `.`
502
447
 
503
- # Run e2e tests (requires API running)
504
- bun test test/e2e.test.ts
505
- ```
448
+ ## Environment Variables
449
+
450
+ | Variable | Description |
451
+ |----------|-------------|
452
+ | `ZERODEPLOY_TOKEN` | Authentication token (for CI/CD) |
453
+ | `ZERODEPLOY_API_URL` | Custom API URL (defaults to production) |
506
454
 
507
455
  ## Troubleshooting
508
456
 
@@ -512,12 +460,20 @@ Run `zerodeploy login` to authenticate.
512
460
 
513
461
  ### "Org not found"
514
462
 
515
- Ensure you're using the correct org slug (lowercase, hyphenated). Use `zerodeploy org list` to see your organizations.
463
+ Use the correct org slug (lowercase, hyphenated). Check with `zerodeploy org list`.
516
464
 
517
465
  ### "Site not found"
518
466
 
519
- Ensure you're using the correct site slug and org slug. Use `zerodeploy site list <orgSlug>` to see sites.
467
+ Use the correct site and org slugs. Check with `zerodeploy site list <orgSlug>`.
520
468
 
521
469
  ### "No build directory found"
522
470
 
523
- Specify the directory explicitly with `--dir ./path/to/build` or ensure your project has a `dist/`, `build/`, `out/`, or `public/` directory.
471
+ Either:
472
+ - Specify the directory with `--dir ./path/to/build`
473
+ - Run with `--build` to build first
474
+ - Ensure your project has a `dist/`, `build/`, `out/`, or `public/` directory
475
+
476
+ ## Links
477
+
478
+ - [Documentation](https://zerodeploy.dev/docs)
479
+ - [Support](https://zerodeploy.dev/support)
package/dist/cli.js CHANGED
@@ -3453,7 +3453,7 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3453
3453
  console.log(` Value: ${data.verification.recordValue}`);
3454
3454
  console.log();
3455
3455
  console.log("After adding the record, run:");
3456
- console.log(` zerodeploy domain verify ${data.id} --org ${options.org} --site ${options.site}`);
3456
+ console.log(` zerodeploy domain verify ${data.domain} --org ${options.org} --site ${options.site}`);
3457
3457
  console.log();
3458
3458
  } catch (err) {
3459
3459
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -3474,6 +3474,16 @@ function formatStatus(status) {
3474
3474
  return status;
3475
3475
  }
3476
3476
  }
3477
+ function formatRedirect(mode) {
3478
+ switch (mode) {
3479
+ case "www_to_apex":
3480
+ return "www→apex";
3481
+ case "apex_to_www":
3482
+ return "apex→www";
3483
+ default:
3484
+ return "";
3485
+ }
3486
+ }
3477
3487
  var domainListCommand = new Command2("list").description("List custom domains for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
3478
3488
  const token = loadToken();
3479
3489
  if (!token) {
@@ -3501,7 +3511,9 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3501
3511
  console.log("Custom Domains:");
3502
3512
  console.log();
3503
3513
  for (const d of domains) {
3504
- console.log(` ${d.domain.padEnd(30)} ${formatStatus(d.verification_status).padEnd(15)} ${d.id}`);
3514
+ const redirect = formatRedirect(d.redirect_mode);
3515
+ const redirectStr = redirect ? ` [${redirect}]` : "";
3516
+ console.log(` ${d.domain.padEnd(30)} ${formatStatus(d.verification_status).padEnd(15)}${redirectStr}`);
3505
3517
  }
3506
3518
  console.log();
3507
3519
  } catch (err) {
@@ -3511,14 +3523,33 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3511
3523
  });
3512
3524
 
3513
3525
  // src/commands/domain/verify.ts
3514
- var domainVerifyCommand = new Command2("verify").description("Verify ownership of a custom domain").argument("<domainId>", "Domain ID to verify").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainId, options) => {
3526
+ var domainVerifyCommand = new Command2("verify").description("Verify ownership of a custom domain").argument("<domain>", "Domain to verify (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainName, options) => {
3515
3527
  const token = loadToken();
3516
3528
  if (!token) {
3517
3529
  console.log("Not logged in. Run: zerodeploy login");
3518
3530
  return;
3519
3531
  }
3520
3532
  try {
3521
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domainId}/verify`, {
3533
+ const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3534
+ headers: {
3535
+ Authorization: `Bearer ${token}`
3536
+ }
3537
+ });
3538
+ if (!listRes.ok) {
3539
+ const error = await listRes.json();
3540
+ throw new Error(error.error || `API Error ${listRes.status}`);
3541
+ }
3542
+ const domains = await listRes.json();
3543
+ const domain = domains.find((d) => d.domain === domainName);
3544
+ if (!domain) {
3545
+ console.error(`
3546
+ ❌ Domain not found: ${domainName}`);
3547
+ console.log();
3548
+ console.log("Add it first with:");
3549
+ console.log(` zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`);
3550
+ return;
3551
+ }
3552
+ const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}/verify`, {
3522
3553
  method: "POST",
3523
3554
  headers: {
3524
3555
  Authorization: `Bearer ${token}`,
@@ -3528,7 +3559,7 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3528
3559
  const data = await res.json();
3529
3560
  if (!res.ok) {
3530
3561
  console.error(`
3531
- ❌ Verification failed for domain`);
3562
+ ❌ Verification failed for ${domainName}`);
3532
3563
  if (data.message) {
3533
3564
  console.log();
3534
3565
  console.log(data.message);
@@ -3536,7 +3567,7 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3536
3567
  console.log();
3537
3568
  console.log("Tips:");
3538
3569
  console.log(" • DNS changes can take up to 48 hours to propagate");
3539
- console.log(" • Verify the TXT record is set correctly using: dig TXT _zerodeploy.<domain>");
3570
+ console.log(` • Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}`);
3540
3571
  console.log(" • Try again in a few minutes");
3541
3572
  return;
3542
3573
  }
@@ -3587,14 +3618,33 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3587
3618
  });
3588
3619
 
3589
3620
  // src/commands/domain/remove.ts
3590
- var domainRemoveCommand = new Command2("remove").description("Remove a custom domain from a site").argument("<domainId>", "Domain ID to remove").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainId, options) => {
3621
+ var domainRemoveCommand = new Command2("remove").description("Remove a custom domain from a site").argument("<domain>", "Domain to remove (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainName, options) => {
3591
3622
  const token = loadToken();
3592
3623
  if (!token) {
3593
3624
  console.log("Not logged in. Run: zerodeploy login");
3594
3625
  return;
3595
3626
  }
3596
3627
  try {
3597
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domainId}`, {
3628
+ const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3629
+ headers: {
3630
+ Authorization: `Bearer ${token}`
3631
+ }
3632
+ });
3633
+ if (!listRes.ok) {
3634
+ const error = await listRes.json();
3635
+ throw new Error(error.error || `API Error ${listRes.status}`);
3636
+ }
3637
+ const domains = await listRes.json();
3638
+ const domain = domains.find((d) => d.domain === domainName);
3639
+ if (!domain) {
3640
+ console.error(`
3641
+ ❌ Domain not found: ${domainName}`);
3642
+ console.log();
3643
+ console.log("List domains with:");
3644
+ console.log(` zerodeploy domain list --org ${options.org} --site ${options.site}`);
3645
+ return;
3646
+ }
3647
+ const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}`, {
3598
3648
  method: "DELETE",
3599
3649
  headers: {
3600
3650
  Authorization: `Bearer ${token}`
@@ -3612,12 +3662,77 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
3612
3662
  }
3613
3663
  });
3614
3664
 
3665
+ // src/commands/domain/redirect.ts
3666
+ function formatRedirectMode(mode) {
3667
+ switch (mode) {
3668
+ case "none":
3669
+ return "No redirect";
3670
+ case "www_to_apex":
3671
+ return "www → apex (e.g., www.example.com → example.com)";
3672
+ case "apex_to_www":
3673
+ return "apex → www (e.g., example.com → www.example.com)";
3674
+ default:
3675
+ return mode;
3676
+ }
3677
+ }
3678
+ var domainRedirectCommand = new Command2("redirect").description("Set redirect mode for a custom domain").argument("<domain>", "Domain name (e.g., example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").requiredOption("--mode <mode>", "Redirect mode: none, www_to_apex, or apex_to_www").action(async (domain, options) => {
3679
+ const token = loadToken();
3680
+ if (!token) {
3681
+ console.log("Not logged in. Run: zerodeploy login");
3682
+ return;
3683
+ }
3684
+ const validModes = ["none", "www_to_apex", "apex_to_www"];
3685
+ if (!validModes.includes(options.mode)) {
3686
+ console.error(`Invalid mode: ${options.mode}`);
3687
+ console.error("Valid modes: none, www_to_apex, apex_to_www");
3688
+ return;
3689
+ }
3690
+ try {
3691
+ const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3692
+ headers: {
3693
+ Authorization: `Bearer ${token}`
3694
+ }
3695
+ });
3696
+ if (!listRes.ok) {
3697
+ const error = await listRes.json();
3698
+ throw new Error(error.error || `API Error ${listRes.status}`);
3699
+ }
3700
+ const domains = await listRes.json();
3701
+ const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
3702
+ if (!targetDomain) {
3703
+ console.error(`Domain not found: ${domain}`);
3704
+ console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
3705
+ return;
3706
+ }
3707
+ const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${targetDomain.id}/redirect`, {
3708
+ method: "PATCH",
3709
+ headers: {
3710
+ Authorization: `Bearer ${token}`,
3711
+ "Content-Type": "application/json"
3712
+ },
3713
+ body: JSON.stringify({ redirectMode: options.mode })
3714
+ });
3715
+ if (!res.ok) {
3716
+ const error = await res.json();
3717
+ throw new Error(error.error || `API Error ${res.status}`);
3718
+ }
3719
+ const data = await res.json();
3720
+ console.log(`
3721
+ ✅ Redirect mode updated for ${data.domain}`);
3722
+ console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
3723
+ console.log();
3724
+ } catch (err) {
3725
+ const message = err instanceof Error ? err.message : "Unknown error";
3726
+ console.error("Failed to update redirect mode:", message);
3727
+ }
3728
+ });
3729
+
3615
3730
  // src/commands/domain/index.ts
3616
- var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand);
3731
+ var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
3617
3732
 
3618
3733
  // src/commands/deploy/index.ts
3619
- import { resolve as resolve3 } from "node:path";
3620
- import { stat as stat2 } from "node:fs/promises";
3734
+ import { resolve as resolve3, basename } from "node:path";
3735
+ import { stat as stat2, writeFile } from "node:fs/promises";
3621
3736
  import { spawn } from "node:child_process";
3622
3737
 
3623
3738
  // src/utils/files.ts
@@ -4335,6 +4450,29 @@ function getConfigPath(cwd = process.cwd()) {
4335
4450
  return resolve2(cwd, CONFIG_FILENAME);
4336
4451
  }
4337
4452
 
4453
+ // src/utils/prompt.ts
4454
+ import * as readline3 from "node:readline";
4455
+ async function confirm(message, defaultValue = true) {
4456
+ const rl = readline3.createInterface({
4457
+ input: process.stdin,
4458
+ output: process.stdout
4459
+ });
4460
+ const hint = defaultValue ? "[Y/n]" : "[y/N]";
4461
+ return new Promise((resolve3) => {
4462
+ rl.question(`${message} ${hint} `, (answer) => {
4463
+ rl.close();
4464
+ const normalized = answer.trim().toLowerCase();
4465
+ if (normalized === "") {
4466
+ resolve3(defaultValue);
4467
+ } else if (normalized === "y" || normalized === "yes") {
4468
+ resolve3(true);
4469
+ } else {
4470
+ resolve3(false);
4471
+ }
4472
+ });
4473
+ });
4474
+ }
4475
+
4338
4476
  // src/commands/deploy/list.ts
4339
4477
  var deployListCommand = new Command2("list").description("List deployments for a site").argument("<siteSlug>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--limit <number>", "Number of deployments to show", "10").action(async (siteSlug, options) => {
4340
4478
  const token = loadToken();
@@ -4414,7 +4552,37 @@ var deployRollbackCommand = new Command2("rollback").description("Rollback to a
4414
4552
  }
4415
4553
  });
4416
4554
 
4555
+ // src/commands/deploy/promote.ts
4556
+ var deployPromoteCommand = new Command2("promote").description("Promote a preview deployment to production").argument("<deploymentId>", "Deployment ID (or first 8 chars) to promote").action(async (deploymentId) => {
4557
+ const token = loadToken();
4558
+ if (!token) {
4559
+ console.log("Not logged in. Run: zerodeploy login");
4560
+ return;
4561
+ }
4562
+ try {
4563
+ const client = getClient(token);
4564
+ const res = await client.deployments[":id"].rollback.$post({
4565
+ param: { id: deploymentId }
4566
+ });
4567
+ if (!res.ok) {
4568
+ const error = await res.json();
4569
+ console.error(`Error: ${error.error}`);
4570
+ return;
4571
+ }
4572
+ const result = await res.json();
4573
+ console.log("Deployment promoted to production!");
4574
+ console.log(` Deployment: ${result.deployment.id}`);
4575
+ console.log(` URL: ${result.deployment.url}`);
4576
+ } catch (err) {
4577
+ const message = err instanceof Error ? err.message : "Unknown error";
4578
+ console.error("Failed to promote deployment:", message);
4579
+ }
4580
+ });
4581
+
4417
4582
  // src/commands/deploy/index.ts
4583
+ function slugify(input) {
4584
+ return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
4585
+ }
4418
4586
  async function runCommand(command, cwd) {
4419
4587
  return new Promise((promiseResolve) => {
4420
4588
  const [cmd, ...args] = command.split(" ");
@@ -4473,26 +4641,63 @@ async function uploadArchive(token, uploadUrl, archive) {
4473
4641
  });
4474
4642
  return res.ok;
4475
4643
  }
4476
- var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).action(async (siteSlugArg, options) => {
4644
+ var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
4477
4645
  const cwd = process.cwd();
4478
- const config = loadProjectConfig(cwd);
4479
- const siteSlug = siteSlugArg || config.site;
4480
- const orgSlug = options.org || config.org;
4481
- const dirOption = options.dir || config.dir;
4482
- if (!siteSlug) {
4483
- console.log("Error: Site is required. Provide as argument or in zerodeploy.json");
4484
- deployCommand.help();
4485
- return;
4486
- }
4487
- if (!orgSlug) {
4488
- console.log('Error: --org is required (or set "org" in zerodeploy.json)');
4489
- return;
4490
- }
4491
4646
  const token = loadToken();
4492
4647
  if (!token) {
4493
4648
  console.log("Not logged in. Run: zerodeploy login");
4494
4649
  return;
4495
4650
  }
4651
+ const config = loadProjectConfig(cwd);
4652
+ let siteSlug = siteSlugArg || config.site;
4653
+ let orgSlug = options.org || config.org;
4654
+ const dirOption = options.dir || config.dir;
4655
+ if (!siteSlug || !orgSlug) {
4656
+ const client = getClient(token);
4657
+ const meRes = await client.auth.me.$get();
4658
+ if (!meRes.ok) {
4659
+ console.log("Error: Failed to fetch user info");
4660
+ return;
4661
+ }
4662
+ const userInfo = await meRes.json();
4663
+ if (!orgSlug) {
4664
+ if (!userInfo.personalOrg) {
4665
+ console.log("Error: No personal org found. Please create one with: zerodeploy org create <name>");
4666
+ return;
4667
+ }
4668
+ orgSlug = userInfo.personalOrg.slug;
4669
+ }
4670
+ if (!siteSlug) {
4671
+ const folderName = basename(cwd);
4672
+ const suggestedName = slugify(folderName) || "my-site";
4673
+ console.log("");
4674
+ const shouldCreate = await confirm(`No site configured. Create site "${suggestedName}"?`, true);
4675
+ if (!shouldCreate) {
4676
+ console.log("Deploy cancelled. Create a site first with: zerodeploy site create");
4677
+ return;
4678
+ }
4679
+ console.log("");
4680
+ console.log(`Creating site "${suggestedName}"...`);
4681
+ const createRes = await client.orgs[":orgSlug"].sites.$post({
4682
+ param: { orgSlug },
4683
+ json: { name: suggestedName, subdomain: suggestedName }
4684
+ });
4685
+ if (!createRes.ok) {
4686
+ const error = await createRes.json();
4687
+ console.log(`Error: ${error.error || "Failed to create site"}`);
4688
+ return;
4689
+ }
4690
+ const site = await createRes.json();
4691
+ siteSlug = site.slug;
4692
+ console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
4693
+ const configPath = getConfigPath(cwd);
4694
+ const newConfig = { org: orgSlug, site: siteSlug, dir: dirOption || config.dir };
4695
+ await writeFile(configPath, JSON.stringify(newConfig, null, 2) + `
4696
+ `);
4697
+ console.log(`Saved config to zerodeploy.json`);
4698
+ console.log("");
4699
+ }
4700
+ }
4496
4701
  const framework = await detectFramework(cwd);
4497
4702
  if (framework) {
4498
4703
  console.log(`Detected: ${framework.name}`);
@@ -4635,16 +4840,24 @@ Error: Build failed`);
4635
4840
  "Content-Type": "application/json",
4636
4841
  Authorization: `Bearer ${token}`
4637
4842
  },
4638
- body: JSON.stringify({})
4843
+ body: JSON.stringify({ preview: options.preview || false })
4639
4844
  });
4640
4845
  if (!finalizeRes.ok) {
4641
4846
  console.log("Error: Failed to finalize deployment");
4642
4847
  return;
4643
4848
  }
4644
4849
  console.log("");
4645
- console.log("Deployment successful!");
4646
- console.log(`URL: ${deployment.url}`);
4647
- console.log(`Preview: ${deployment.previewUrl}`);
4850
+ if (options.preview) {
4851
+ console.log("Preview deployment created!");
4852
+ console.log(`Preview: ${deployment.previewUrl}`);
4853
+ console.log("");
4854
+ console.log(`To make this deployment live, run:`);
4855
+ console.log(` zerodeploy deploy promote ${deployment.id.slice(0, 8)}`);
4856
+ } else {
4857
+ console.log("Deployment successful!");
4858
+ console.log(`URL: ${deployment.url}`);
4859
+ console.log(`Preview: ${deployment.previewUrl}`);
4860
+ }
4648
4861
  if (options.githubOutput) {
4649
4862
  const githubOutputFile = process.env.GITHUB_OUTPUT;
4650
4863
  if (githubOutputFile) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerodeploy/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zerodeploy": "dist/cli.js"
@@ -23,11 +23,6 @@
23
23
  "publishConfig": {
24
24
  "access": "public"
25
25
  },
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/zerodeploy/zerodeploy.git",
29
- "directory": "apps/cli"
30
- },
31
26
  "keywords": [
32
27
  "deploy",
33
28
  "spa",