@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.
- package/README.md +248 -292
- package/dist/cli.js +242 -29
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
75
|
+
Create a new organization.
|
|
91
76
|
|
|
92
77
|
```bash
|
|
93
78
|
zerodeploy org create "My Company"
|
|
94
79
|
```
|
|
95
80
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
93
|
+
List all sites in an organization.
|
|
107
94
|
|
|
108
95
|
```bash
|
|
109
96
|
zerodeploy site list my-company
|
|
110
97
|
```
|
|
111
98
|
|
|
112
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
117
|
+
#### `zerodeploy site delete <siteSlug> --org <orgSlug>`
|
|
156
118
|
|
|
157
|
-
|
|
119
|
+
Delete a site and all its deployments.
|
|
158
120
|
|
|
159
121
|
```bash
|
|
160
|
-
zerodeploy site
|
|
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.
|
|
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
|
-
- `--
|
|
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
|
|
142
|
+
# Auto-detect build directory
|
|
214
143
|
zerodeploy deploy my-website --org my-company
|
|
215
144
|
|
|
216
|
-
# Build and deploy
|
|
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-
|
|
241
|
-
Preview: https://
|
|
160
|
+
URL: https://my-website.zerodeploy.app
|
|
161
|
+
Preview: https://019b1234-my-website.zerodeploy.app
|
|
242
162
|
```
|
|
243
163
|
|
|
244
|
-
####
|
|
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
|
-
|
|
249
|
-
- **Preview URL**: `https://{shortId}_site_org.zerodeploy.app` - Serves a specific deployment
|
|
166
|
+
List deployment history for a site.
|
|
250
167
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
172
|
+
**Options:**
|
|
173
|
+
- `--limit <number>` - Number of deployments to show (default: 10)
|
|
258
174
|
|
|
259
|
-
|
|
175
|
+
#### `zerodeploy rollback <siteSlug> --org <orgSlug>`
|
|
260
176
|
|
|
261
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
#### `zerodeploy token create <name> --org <org> --site <site>`
|
|
309
192
|
|
|
310
|
-
|
|
193
|
+
Create a deploy token for CI/CD.
|
|
311
194
|
|
|
312
|
-
|
|
195
|
+
```bash
|
|
196
|
+
zerodeploy token create "GitHub Actions" --org my-company --site my-website
|
|
197
|
+
```
|
|
313
198
|
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
209
|
+
#### `zerodeploy token delete <tokenId> --org <org> --site <site>`
|
|
347
210
|
|
|
348
|
-
|
|
211
|
+
Delete a deploy token.
|
|
349
212
|
|
|
350
213
|
```bash
|
|
351
|
-
zerodeploy
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
+
#### `zerodeploy domain add <domain> --org <org> --site <site>`
|
|
368
222
|
|
|
369
|
-
|
|
223
|
+
Add a custom domain to a site. Returns DNS verification instructions.
|
|
370
224
|
|
|
371
225
|
```bash
|
|
372
|
-
zerodeploy
|
|
226
|
+
zerodeploy domain add www.example.com --org my-company --site my-website
|
|
373
227
|
```
|
|
374
228
|
|
|
375
|
-
|
|
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
|
-
|
|
231
|
+
Verify domain ownership after adding the TXT record to your DNS.
|
|
383
232
|
|
|
384
|
-
|
|
233
|
+
```bash
|
|
234
|
+
zerodeploy domain verify www.example.com --org my-company --site my-website
|
|
235
|
+
```
|
|
385
236
|
|
|
386
|
-
#### `zerodeploy
|
|
237
|
+
#### `zerodeploy domain list --org <org> --site <site>`
|
|
387
238
|
|
|
388
|
-
|
|
239
|
+
List all custom domains for a site.
|
|
389
240
|
|
|
390
241
|
```bash
|
|
391
|
-
zerodeploy
|
|
242
|
+
zerodeploy domain list --org my-company --site my-website
|
|
392
243
|
```
|
|
393
244
|
|
|
394
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
267
|
+
# Disable redirects
|
|
268
|
+
zerodeploy domain redirect example.com --org my-company --site my-website --mode none
|
|
407
269
|
```
|
|
408
270
|
|
|
409
|
-
**
|
|
410
|
-
- `--org <orgSlug>` - Organization slug (required)
|
|
411
|
-
- `--site <siteSlug>` - Site slug (required)
|
|
271
|
+
**Custom Domain Setup:**
|
|
412
272
|
|
|
413
|
-
|
|
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
|
-
|
|
281
|
+
Create a `zerodeploy.json` in your project root:
|
|
416
282
|
|
|
417
283
|
```bash
|
|
418
|
-
zerodeploy
|
|
284
|
+
zerodeploy init --org my-company --site my-website
|
|
419
285
|
```
|
|
420
286
|
|
|
421
|
-
|
|
422
|
-
```
|
|
423
|
-
Deploy tokens for my-company/my-website:
|
|
287
|
+
This creates:
|
|
424
288
|
|
|
425
|
-
|
|
426
|
-
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"org": "my-company",
|
|
292
|
+
"site": "my-website"
|
|
293
|
+
}
|
|
427
294
|
```
|
|
428
295
|
|
|
429
|
-
|
|
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
|
|
299
|
+
zerodeploy deploy
|
|
435
300
|
```
|
|
436
301
|
|
|
437
|
-
##
|
|
302
|
+
## Deployed Sites
|
|
438
303
|
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
https://<subdomain>.zerodeploy.app
|
|
451
310
|
```
|
|
452
311
|
|
|
453
|
-
|
|
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://<
|
|
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
|
-
-
|
|
329
|
+
- Works with React Router, Vue Router, and other client-side routers
|
|
463
330
|
|
|
464
331
|
### Caching
|
|
465
332
|
|
|
466
|
-
|
|
467
|
-
- **HTML files**:
|
|
468
|
-
- **Hashed assets** (e.g., `main.abc123.js`):
|
|
469
|
-
- **Other assets**:
|
|
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
|
-
##
|
|
338
|
+
## CI/CD Integration
|
|
472
339
|
|
|
473
|
-
###
|
|
340
|
+
### GitHub Actions
|
|
474
341
|
|
|
475
|
-
|
|
342
|
+
**1. Create a deploy token:**
|
|
476
343
|
|
|
477
|
-
|
|
344
|
+
```bash
|
|
345
|
+
zerodeploy token create "GitHub Actions" --org my-company --site my-website
|
|
346
|
+
```
|
|
478
347
|
|
|
479
|
-
|
|
348
|
+
**2. Add the token to your repository secrets** as `ZERODEPLOY_TOKEN`
|
|
480
349
|
|
|
481
|
-
|
|
350
|
+
**3. Create `.github/workflows/deploy.yml`:**
|
|
482
351
|
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
|
|
352
|
+
```yaml
|
|
353
|
+
name: Deploy
|
|
354
|
+
on:
|
|
355
|
+
push:
|
|
356
|
+
branches: [main]
|
|
486
357
|
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
374
|
+
**PR Preview workflow:**
|
|
492
375
|
|
|
493
|
-
|
|
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
|
-
#
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
463
|
+
Use the correct org slug (lowercase, hyphenated). Check with `zerodeploy org list`.
|
|
516
464
|
|
|
517
465
|
### "Site not found"
|
|
518
466
|
|
|
519
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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("<
|
|
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
|
|
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
|
|
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(
|
|
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("<
|
|
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
|
|
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
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
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.
|
|
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",
|