@zerodeploy/cli 0.1.1 → 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 +62 -0
  2. package/dist/cli.js +242 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -214,6 +214,68 @@ Delete a deploy token.
214
214
  zerodeploy token delete 019b1234 --org my-company --site my-website
215
215
  ```
216
216
 
217
+ ### Custom Domains
218
+
219
+ Connect your own domain to any ZeroDeploy site with automatic SSL.
220
+
221
+ #### `zerodeploy domain add <domain> --org <org> --site <site>`
222
+
223
+ Add a custom domain to a site. Returns DNS verification instructions.
224
+
225
+ ```bash
226
+ zerodeploy domain add www.example.com --org my-company --site my-website
227
+ ```
228
+
229
+ #### `zerodeploy domain verify <domain> --org <org> --site <site>`
230
+
231
+ Verify domain ownership after adding the TXT record to your DNS.
232
+
233
+ ```bash
234
+ zerodeploy domain verify www.example.com --org my-company --site my-website
235
+ ```
236
+
237
+ #### `zerodeploy domain list --org <org> --site <site>`
238
+
239
+ List all custom domains for a site.
240
+
241
+ ```bash
242
+ zerodeploy domain list --org my-company --site my-website
243
+ ```
244
+
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
251
+ ```
252
+
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`
259
+
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
263
+
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
266
+
267
+ # Disable redirects
268
+ zerodeploy domain redirect example.com --org my-company --site my-website --mode none
269
+ ```
270
+
271
+ **Custom Domain Setup:**
272
+
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
+
217
279
  ## Configuration File
218
280
 
219
281
  Create a `zerodeploy.json` in your project root:
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.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zerodeploy": "dist/cli.js"