@zerodeploy/cli 0.1.3 → 0.1.5

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/dist/cli.js +1196 -384
  2. package/dist/index.js +6915 -0
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2745,6 +2745,48 @@ import os2 from "os";
2745
2745
  import path2 from "path";
2746
2746
 
2747
2747
  // src/utils/errors.ts
2748
+ var ExitCode = {
2749
+ SUCCESS: 0,
2750
+ AUTH_ERROR: 1,
2751
+ NOT_FOUND: 2,
2752
+ VALIDATION_ERROR: 3,
2753
+ RATE_LIMIT: 4,
2754
+ SERVER_ERROR: 5,
2755
+ NETWORK_ERROR: 6,
2756
+ BILLING_ERROR: 7
2757
+ };
2758
+ function getExitCode(status, errorCode) {
2759
+ if (errorCode) {
2760
+ if (errorCode === "billing_required" || errorCode.includes("billing")) {
2761
+ return ExitCode.BILLING_ERROR;
2762
+ }
2763
+ if (errorCode === "validation_error" || errorCode.includes("invalid")) {
2764
+ return ExitCode.VALIDATION_ERROR;
2765
+ }
2766
+ if (errorCode === "rate_limit_exceeded" || errorCode.includes("rate_limit")) {
2767
+ return ExitCode.RATE_LIMIT;
2768
+ }
2769
+ }
2770
+ if (status === 402) {
2771
+ return ExitCode.BILLING_ERROR;
2772
+ }
2773
+ if (status === 401 || status === 403) {
2774
+ return ExitCode.AUTH_ERROR;
2775
+ }
2776
+ if (status === 404) {
2777
+ return ExitCode.NOT_FOUND;
2778
+ }
2779
+ if (status === 422 || status === 400) {
2780
+ return ExitCode.VALIDATION_ERROR;
2781
+ }
2782
+ if (status === 429) {
2783
+ return ExitCode.RATE_LIMIT;
2784
+ }
2785
+ if (status >= 500) {
2786
+ return ExitCode.SERVER_ERROR;
2787
+ }
2788
+ return ExitCode.AUTH_ERROR;
2789
+ }
2748
2790
  function parseApiError(body) {
2749
2791
  if (!body || typeof body !== "object") {
2750
2792
  return {
@@ -2809,6 +2851,18 @@ function displayError(error) {
2809
2851
  }
2810
2852
  console.error();
2811
2853
  }
2854
+ async function handleApiError(response) {
2855
+ let body;
2856
+ try {
2857
+ body = await response.json();
2858
+ } catch {
2859
+ body = null;
2860
+ }
2861
+ const error = parseApiError(body);
2862
+ displayError(error);
2863
+ const exitCode = getExitCode(response.status, error.code);
2864
+ process.exit(exitCode);
2865
+ }
2812
2866
  function displayNetworkError(error) {
2813
2867
  if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
2814
2868
  displayError({
@@ -2838,15 +2892,19 @@ function displayAuthError() {
2838
2892
  docs: "https://zerodeploy.dev/docs/cli/auth"
2839
2893
  });
2840
2894
  }
2895
+ function handleAuthError() {
2896
+ displayAuthError();
2897
+ process.exit(ExitCode.AUTH_ERROR);
2898
+ }
2841
2899
 
2842
2900
  // src/auth/token.ts
2843
2901
  var DIR = path2.join(os2.homedir(), ".zerodeploy");
2844
2902
  var TOKEN_PATH = path2.join(DIR, "token");
2845
2903
  function saveToken(token) {
2846
2904
  if (!fs6.existsSync(DIR)) {
2847
- fs6.mkdirSync(DIR);
2905
+ fs6.mkdirSync(DIR, { mode: 448 });
2848
2906
  }
2849
- fs6.writeFileSync(TOKEN_PATH, token, "utf8");
2907
+ fs6.writeFileSync(TOKEN_PATH, token, { encoding: "utf8", mode: 384 });
2850
2908
  }
2851
2909
  function loadToken() {
2852
2910
  const envToken = process.env.ZERODEPLOY_TOKEN;
@@ -2881,8 +2939,8 @@ var loginCommand = new Command2("login").description("Login via GitHub OAuth wit
2881
2939
  if (!token) {
2882
2940
  res.writeHead(400);
2883
2941
  res.end("Login failed: missing token");
2884
- console.error("Login failed: no token received");
2885
- process.exit(1);
2942
+ displayError({ code: "auth_error", message: "Login failed: no token received" });
2943
+ process.exit(ExitCode.AUTH_ERROR);
2886
2944
  }
2887
2945
  saveToken(token);
2888
2946
  if (pending) {
@@ -3204,7 +3262,8 @@ var hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
3204
3262
  var DEFAULT_OPTIONS = {
3205
3263
  maxRetries: 3,
3206
3264
  baseDelayMs: 1000,
3207
- maxDelayMs: 1e4
3265
+ maxDelayMs: 1e4,
3266
+ timeoutMs: 30000
3208
3267
  };
3209
3268
  var RETRYABLE_STATUS_CODES = new Set([
3210
3269
  408,
@@ -3277,8 +3336,17 @@ async function fetchWithRetry(input, init, options) {
3277
3336
  const opts = { ...DEFAULT_OPTIONS, ...options };
3278
3337
  let lastError;
3279
3338
  for (let attempt = 0;attempt <= opts.maxRetries; attempt++) {
3339
+ let controller;
3340
+ let timeoutId;
3341
+ if (opts.timeoutMs > 0) {
3342
+ controller = new AbortController;
3343
+ timeoutId = setTimeout(() => controller.abort(), opts.timeoutMs);
3344
+ }
3280
3345
  try {
3281
- const response = await fetch(input, init);
3346
+ const fetchInit = controller ? { ...init, signal: controller.signal } : init ?? {};
3347
+ const response = await fetch(input, fetchInit);
3348
+ if (timeoutId)
3349
+ clearTimeout(timeoutId);
3282
3350
  if (isRetryableStatus(response.status) && attempt < opts.maxRetries) {
3283
3351
  const headerDelay = response.status === 429 ? getDelayFromHeaders(response) : null;
3284
3352
  const delayMs = headerDelay ?? calculateDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
@@ -3289,6 +3357,8 @@ async function fetchWithRetry(input, init, options) {
3289
3357
  }
3290
3358
  return response;
3291
3359
  } catch (error) {
3360
+ if (timeoutId)
3361
+ clearTimeout(timeoutId);
3292
3362
  lastError = error instanceof Error ? error : new Error(String(error));
3293
3363
  if (!isRetryableError(error) || attempt >= opts.maxRetries) {
3294
3364
  throw lastError;
@@ -3311,6 +3381,35 @@ function formatRetryMessage(attempt, maxRetries, error) {
3311
3381
  return `Retry ${attempt}/${maxRetries} (${errorDesc})`;
3312
3382
  }
3313
3383
 
3384
+ // ../../packages/api-client/src/interceptors.ts
3385
+ async function applyRequestInterceptors(request, interceptors) {
3386
+ let req = request;
3387
+ for (const interceptor of interceptors) {
3388
+ req = await interceptor(req);
3389
+ }
3390
+ return req;
3391
+ }
3392
+ async function applyResponseInterceptors(response, interceptors) {
3393
+ let res = response;
3394
+ for (const interceptor of interceptors) {
3395
+ res = await interceptor(res);
3396
+ }
3397
+ return res;
3398
+ }
3399
+ function createInterceptedFetch(baseFetch, interceptors) {
3400
+ return async (input, init) => {
3401
+ let request = new Request(input, init);
3402
+ if (interceptors.request?.length) {
3403
+ request = await applyRequestInterceptors(request, interceptors.request);
3404
+ }
3405
+ let response = await baseFetch(request);
3406
+ if (interceptors.response?.length) {
3407
+ response = await applyResponseInterceptors(response, interceptors.response);
3408
+ }
3409
+ return response;
3410
+ };
3411
+ }
3412
+
3314
3413
  // ../../packages/api-client/src/index.ts
3315
3414
  var DEFAULT_RETRY_OPTIONS = {
3316
3415
  maxRetries: 3,
@@ -3329,6 +3428,10 @@ function createClient(baseUrl, token, options) {
3329
3428
  } else if (options?.useCredentials) {
3330
3429
  fetchFn = (input, init) => fetch(input, { ...init, credentials: "include" });
3331
3430
  }
3431
+ if (options?.interceptors && (options.interceptors.request?.length || options.interceptors.response?.length)) {
3432
+ const baseFetch = fetchFn || fetch;
3433
+ fetchFn = createInterceptedFetch(baseFetch, options.interceptors);
3434
+ }
3332
3435
  return hc(baseUrl, {
3333
3436
  headers: token ? { Authorization: `Bearer ${token}` } : undefined,
3334
3437
  fetch: fetchFn
@@ -3353,18 +3456,22 @@ function getClient(token, retryOptions) {
3353
3456
  }
3354
3457
 
3355
3458
  // src/commands/whoami.ts
3356
- var whoamiCommand = new Command2("whoami").description("Show the currently logged-in user").action(async () => {
3459
+ var whoamiCommand = new Command2("whoami").description("Show the currently logged-in user").option("--json", "Output as JSON").action(async (options) => {
3357
3460
  const token = loadToken();
3358
3461
  if (!token) {
3359
- console.log("❌ You are not logged in. Run `zerodeploy login` first.");
3360
- process.exit(1);
3462
+ handleAuthError();
3361
3463
  }
3362
3464
  try {
3363
3465
  const client = getClient(token);
3364
3466
  const res = await client.auth.me.$get();
3365
- if (!res.ok)
3366
- throw new Error(`API Error ${res.status}`);
3467
+ if (!res.ok) {
3468
+ await handleApiError(res);
3469
+ }
3367
3470
  const data = await res.json();
3471
+ if (options.json) {
3472
+ console.log(JSON.stringify(data, null, 2));
3473
+ return;
3474
+ }
3368
3475
  console.log("Logged in as:");
3369
3476
  console.log(` Username: ${data.username}`);
3370
3477
  console.log(` Email: ${data.email || "(not set)"}`);
@@ -3376,8 +3483,9 @@ var whoamiCommand = new Command2("whoami").description("Show the currently logge
3376
3483
  console.log(" We'll email you when your account is approved.");
3377
3484
  }
3378
3485
  } catch (err) {
3379
- console.error("Failed to fetch user info:", err.message || err);
3380
- process.exit(1);
3486
+ const message = err instanceof Error ? err.message : String(err);
3487
+ displayError({ code: "network_error", message: `Failed to fetch user info: ${message}` });
3488
+ process.exit(ExitCode.NETWORK_ERROR);
3381
3489
  }
3382
3490
  });
3383
3491
 
@@ -3396,19 +3504,16 @@ function formatUsageLine(label, current, max, suffix = "") {
3396
3504
  const percentage = Math.round(current / max * 100);
3397
3505
  return ` ${label.padEnd(24)} ${bar} ${current}/${max}${suffix} (${percentage}%)`;
3398
3506
  }
3399
- var usageCommand = new Command2("usage").description("Show current usage and plan limits").option("--json", "Output as JSON").action(async (options) => {
3507
+ var usageCommand = new Command2("usage").description("Show current usage and limits").option("--json", "Output as JSON").action(async (options) => {
3400
3508
  const token = loadToken();
3401
3509
  if (!token) {
3402
- displayAuthError();
3403
- process.exit(1);
3510
+ handleAuthError();
3404
3511
  }
3405
3512
  try {
3406
3513
  const client = getClient(token);
3407
3514
  const res = await client.auth.me.usage.$get();
3408
3515
  if (!res.ok) {
3409
- const body = await res.json();
3410
- displayError(parseApiError(body));
3411
- process.exit(1);
3516
+ await handleApiError(res);
3412
3517
  }
3413
3518
  const data = await res.json();
3414
3519
  if (options.json) {
@@ -3416,14 +3521,12 @@ var usageCommand = new Command2("usage").description("Show current usage and pla
3416
3521
  return;
3417
3522
  }
3418
3523
  console.log();
3419
- console.log(`Plan: ${data.plan.toUpperCase()}`);
3420
- console.log();
3421
3524
  console.log("Account Usage:");
3422
3525
  console.log(formatUsageLine("Organizations", data.usage.orgs, data.limits.max_orgs));
3423
3526
  console.log(formatUsageLine("Total Sites", data.usage.sites, data.limits.max_orgs * data.limits.max_sites_per_org));
3424
3527
  console.log(formatUsageLine("Deployments (month)", data.usage.deployments_this_month, data.limits.max_deployments_per_month));
3425
3528
  console.log();
3426
- console.log("Plan Limits:");
3529
+ console.log("Limits:");
3427
3530
  console.log(` Sites per org: ${data.limits.max_sites_per_org}`);
3428
3531
  console.log(` Deployments per day: ${data.limits.max_deployments_per_day}`);
3429
3532
  console.log(` Deployments per month: ${data.limits.max_deployments_per_month}`);
@@ -3448,66 +3551,71 @@ var usageCommand = new Command2("usage").description("Show current usage and pla
3448
3551
  } catch (err) {
3449
3552
  const message = err instanceof Error ? err.message : String(err);
3450
3553
  displayError({
3451
- code: "api_error",
3554
+ code: "network_error",
3452
3555
  message: `Failed to fetch usage info: ${message}`
3453
3556
  });
3454
- process.exit(1);
3557
+ process.exit(ExitCode.NETWORK_ERROR);
3455
3558
  }
3456
3559
  });
3457
3560
 
3458
3561
  // src/commands/org/list.ts
3459
- var orgListCommand = new Command2("list").description("List your organizations").action(async () => {
3562
+ var orgListCommand = new Command2("list").description("List your organizations").option("--json", "Output as JSON").action(async (options) => {
3460
3563
  const token = loadToken();
3461
3564
  if (!token) {
3462
- console.log("❌ Not logged in. Run `zerodeploy login` first.");
3463
- return;
3565
+ handleAuthError();
3464
3566
  }
3465
3567
  try {
3466
3568
  const client = getClient(token);
3467
3569
  const res = await client.orgs.$get();
3468
- if (!res.ok)
3469
- throw new Error(`API Error ${res.status}`);
3570
+ if (!res.ok) {
3571
+ await handleApiError(res);
3572
+ }
3470
3573
  const { data: orgs } = await res.json();
3574
+ if (options.json) {
3575
+ console.log(JSON.stringify(orgs, null, 2));
3576
+ return;
3577
+ }
3471
3578
  if (orgs.length === 0) {
3472
3579
  console.log("No organizations found.");
3473
3580
  } else {
3474
3581
  console.log("Organizations:");
3475
3582
  for (const o of orgs) {
3476
- console.log(` ${o.slug.padEnd(20)} ${o.name} [${o.plan}]`);
3583
+ console.log(` ${o.slug.padEnd(20)} ${o.name}`);
3477
3584
  }
3478
3585
  }
3479
3586
  } catch (err) {
3480
- console.error("Failed to fetch orgs:", err.message || err);
3587
+ const message = err instanceof Error ? err.message : String(err);
3588
+ displayError({ code: "network_error", message: `Failed to fetch orgs: ${message}` });
3589
+ process.exit(ExitCode.NETWORK_ERROR);
3481
3590
  }
3482
3591
  });
3483
3592
 
3484
3593
  // src/commands/org/create.ts
3485
- var orgCreateCommand = new Command2("create").description("Create a new organization").argument("<name>", "Organization name").action(async (name) => {
3594
+ var orgCreateCommand = new Command2("create").description("Create a new organization").argument("<name>", "Organization name").option("--json", "Output as JSON").action(async (name, options) => {
3486
3595
  const token = loadToken();
3487
3596
  if (!token) {
3488
- displayAuthError();
3489
- return;
3597
+ handleAuthError();
3490
3598
  }
3491
3599
  try {
3492
3600
  const client = getClient(token);
3493
3601
  const res = await client.orgs.$post({ json: { name } });
3494
3602
  if (!res.ok) {
3495
- const body = await res.json();
3496
- displayError(parseApiError(body));
3603
+ await handleApiError(res);
3604
+ }
3605
+ const { data: org } = await res.json();
3606
+ if (options.json) {
3607
+ console.log(JSON.stringify(org, null, 2));
3497
3608
  return;
3498
3609
  }
3499
- const org = await res.json();
3500
3610
  console.log(`
3501
- Created org: ${org.name}`);
3611
+ Created org: ${org.name}`);
3502
3612
  console.log(` Slug: ${org.slug}`);
3503
3613
  console.log(` ID: ${org.id}
3504
3614
  `);
3505
3615
  } catch (err) {
3506
- if (err instanceof Error) {
3507
- displayNetworkError(err);
3508
- } else {
3509
- displayError({ code: "unknown_error", message: "Failed to create org" });
3510
- }
3616
+ const message = err instanceof Error ? err.message : String(err);
3617
+ displayError({ code: "network_error", message: `Failed to create org: ${message}` });
3618
+ process.exit(ExitCode.NETWORK_ERROR);
3511
3619
  }
3512
3620
  });
3513
3621
 
@@ -3528,8 +3636,7 @@ function prompt(question) {
3528
3636
  var orgDeleteCommand = new Command2("delete").description("Delete an organization (must have no sites)").argument("<orgSlug>", "Organization slug").option("--force", "Skip confirmation prompt").action(async (orgSlug, options) => {
3529
3637
  const token = loadToken();
3530
3638
  if (!token) {
3531
- displayAuthError();
3532
- return;
3639
+ handleAuthError();
3533
3640
  }
3534
3641
  if (!options.force) {
3535
3642
  const answer = await prompt(`Are you sure you want to delete organization "${orgSlug}"? This cannot be undone. (y/N) `);
@@ -3544,20 +3651,16 @@ var orgDeleteCommand = new Command2("delete").description("Delete an organizatio
3544
3651
  param: { orgSlug }
3545
3652
  });
3546
3653
  if (!res.ok) {
3547
- const body = await res.json();
3548
- displayError(parseApiError(body));
3549
- return;
3654
+ await handleApiError(res);
3550
3655
  }
3551
3656
  const result = await res.json();
3552
3657
  console.log(`
3553
- ${result.message}
3658
+ ${result.message}
3554
3659
  `);
3555
3660
  } catch (err) {
3556
- if (err instanceof Error) {
3557
- displayNetworkError(err);
3558
- } else {
3559
- displayError({ code: "unknown_error", message: "Failed to delete organization" });
3560
- }
3661
+ const message = err instanceof Error ? err.message : String(err);
3662
+ displayError({ code: "network_error", message: `Failed to delete organization: ${message}` });
3663
+ process.exit(ExitCode.NETWORK_ERROR);
3561
3664
  }
3562
3665
  });
3563
3666
 
@@ -3565,18 +3668,22 @@ var orgDeleteCommand = new Command2("delete").description("Delete an organizatio
3565
3668
  var orgCommand = new Command2("org").description("Manage organizations").addCommand(orgListCommand).addCommand(orgCreateCommand).addCommand(orgDeleteCommand);
3566
3669
 
3567
3670
  // src/commands/site/list.ts
3568
- var siteListCommand = new Command2("list").description("List sites in an organization").argument("<orgSlug>", "Organization slug").action(async (orgSlug) => {
3671
+ var siteListCommand = new Command2("list").description("List sites in an organization").argument("<orgSlug>", "Organization slug").option("--json", "Output as JSON").action(async (orgSlug, options) => {
3569
3672
  const token = loadToken();
3570
3673
  if (!token) {
3571
- console.log("Not logged in. Run: zerodeploy login");
3572
- return;
3674
+ handleAuthError();
3573
3675
  }
3574
3676
  try {
3575
3677
  const client = getClient(token);
3576
3678
  const res = await client.orgs[":orgSlug"].sites.$get({ param: { orgSlug } });
3577
- if (!res.ok)
3578
- throw new Error(`API Error ${res.status}`);
3679
+ if (!res.ok) {
3680
+ await handleApiError(res);
3681
+ }
3579
3682
  const { data: sites } = await res.json();
3683
+ if (options.json) {
3684
+ console.log(JSON.stringify(sites, null, 2));
3685
+ return;
3686
+ }
3580
3687
  if (sites.length === 0) {
3581
3688
  console.log("No sites found.");
3582
3689
  return;
@@ -3587,17 +3694,17 @@ var siteListCommand = new Command2("list").description("List sites in an organiz
3587
3694
  console.log(` ${s.slug.padEnd(20)} ${s.name}${repo}`);
3588
3695
  }
3589
3696
  } catch (err) {
3590
- const message = err instanceof Error ? err.message : "Unknown error";
3591
- console.error("Failed to list sites:", message);
3697
+ const message = err instanceof Error ? err.message : String(err);
3698
+ displayError({ code: "network_error", message: `Failed to list sites: ${message}` });
3699
+ process.exit(ExitCode.NETWORK_ERROR);
3592
3700
  }
3593
3701
  });
3594
3702
 
3595
3703
  // src/commands/site/create.ts
3596
- var siteCreateCommand = new Command2("create").description("Create a site in an organization").argument("<orgSlug>", "Organization slug").argument("<name>", "Site name").requiredOption("--subdomain <subdomain>", 'Subdomain for the site (e.g., "my-site" for my-site.zerodeploy.app)').option("--repo <owner/repo>", 'Link to GitHub repository (e.g., "vercel/next.js")').action(async (orgSlug, name, options) => {
3704
+ var siteCreateCommand = new Command2("create").description("Create a site in an organization").argument("<orgSlug>", "Organization slug").argument("<name>", "Site name").option("--subdomain <subdomain>", "Subdomain for the site (defaults to slugified name)").option("--repo <owner/repo>", 'Link to GitHub repository (e.g., "vercel/next.js")').option("--json", "Output as JSON").action(async (orgSlug, name, options) => {
3597
3705
  const token = loadToken();
3598
3706
  if (!token) {
3599
- console.log("Not logged in. Run: zerodeploy login");
3600
- return;
3707
+ handleAuthError();
3601
3708
  }
3602
3709
  try {
3603
3710
  const client = getClient(token);
@@ -3606,11 +3713,14 @@ var siteCreateCommand = new Command2("create").description("Create a site in an
3606
3713
  json: { name, subdomain: options.subdomain, githubRepo: options.repo }
3607
3714
  });
3608
3715
  if (!res.ok) {
3609
- const error = await res.json();
3610
- throw new Error(error.error || `API Error ${res.status}`);
3716
+ await handleApiError(res);
3717
+ }
3718
+ const { data: site } = await res.json();
3719
+ if (options.json) {
3720
+ console.log(JSON.stringify(site, null, 2));
3721
+ return;
3611
3722
  }
3612
- const site = await res.json();
3613
- console.log(`✅ Created site: ${site.name}`);
3723
+ console.log(`Created site: ${site.name}`);
3614
3724
  console.log(` Subdomain: ${site.subdomain}.zerodeploy.app`);
3615
3725
  console.log(` Slug: ${site.slug}`);
3616
3726
  console.log(` ID: ${site.id}`);
@@ -3618,8 +3728,9 @@ var siteCreateCommand = new Command2("create").description("Create a site in an
3618
3728
  console.log(` Repo: ${site.github_repo}`);
3619
3729
  }
3620
3730
  } catch (err) {
3621
- const message = err instanceof Error ? err.message : "Unknown error";
3622
- console.error("Failed to create site:", message);
3731
+ const message = err instanceof Error ? err.message : String(err);
3732
+ displayError({ code: "network_error", message: `Failed to create site: ${message}` });
3733
+ process.exit(ExitCode.NETWORK_ERROR);
3623
3734
  }
3624
3735
  });
3625
3736
 
@@ -3640,8 +3751,7 @@ function prompt2(question) {
3640
3751
  var siteDeleteCommand = new Command2("delete").description("Delete a site and all its deployments").argument("<siteSlug>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--force", "Skip confirmation prompt").action(async (siteSlug, options) => {
3641
3752
  const token = loadToken();
3642
3753
  if (!token) {
3643
- console.log("Not logged in. Run: zerodeploy login");
3644
- return;
3754
+ handleAuthError();
3645
3755
  }
3646
3756
  if (!options.force) {
3647
3757
  const answer = await prompt2(`Are you sure you want to delete site "${siteSlug}" and all its deployments? This cannot be undone. (y/N) `);
@@ -3656,23 +3766,22 @@ var siteDeleteCommand = new Command2("delete").description("Delete a site and al
3656
3766
  param: { orgSlug: options.org, siteSlug }
3657
3767
  });
3658
3768
  if (!res.ok) {
3659
- const error = await res.json();
3660
- throw new Error(error.error || `API Error ${res.status}`);
3769
+ await handleApiError(res);
3661
3770
  }
3662
3771
  const result = await res.json();
3663
- console.log(`✅ ${result.message}`);
3772
+ console.log(result.message);
3664
3773
  } catch (err) {
3665
- const message = err instanceof Error ? err.message : "Unknown error";
3666
- console.error("Failed to delete site:", message);
3774
+ const message = err instanceof Error ? err.message : String(err);
3775
+ displayError({ code: "network_error", message: `Failed to delete site: ${message}` });
3776
+ process.exit(ExitCode.NETWORK_ERROR);
3667
3777
  }
3668
3778
  });
3669
3779
 
3670
3780
  // src/commands/site/link.ts
3671
- var siteLinkCommand = new Command2("link").description("Link a site to a GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<repo>", 'GitHub repository (e.g., "owner/repo")').action(async (orgSlug, siteSlug, repo) => {
3781
+ var siteLinkCommand = new Command2("link").description("Link a site to a GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<repo>", 'GitHub repository (e.g., "owner/repo")').option("--json", "Output as JSON").action(async (orgSlug, siteSlug, repo, options) => {
3672
3782
  const token = loadToken();
3673
3783
  if (!token) {
3674
- console.log("Not logged in. Run: zerodeploy login");
3675
- return;
3784
+ handleAuthError();
3676
3785
  }
3677
3786
  try {
3678
3787
  const client = getClient(token);
@@ -3681,22 +3790,24 @@ var siteLinkCommand = new Command2("link").description("Link a site to a GitHub
3681
3790
  json: { githubRepo: repo }
3682
3791
  });
3683
3792
  if (!res.ok) {
3684
- const error = await res.json();
3685
- console.log(`Error: ${error.error || "Failed to link repository"}`);
3793
+ await handleApiError(res);
3794
+ }
3795
+ const { data: site } = await res.json();
3796
+ if (options.json) {
3797
+ console.log(JSON.stringify(site, null, 2));
3686
3798
  return;
3687
3799
  }
3688
- const site = await res.json();
3689
3800
  console.log(`Site "${site.name}" linked to ${site.github_repo}`);
3690
3801
  } catch (err) {
3691
- const message = err instanceof Error ? err.message : "Unknown error";
3692
- console.error("Failed to link repository:", message);
3802
+ const message = err instanceof Error ? err.message : String(err);
3803
+ displayError({ code: "network_error", message: `Failed to link repository: ${message}` });
3804
+ process.exit(ExitCode.NETWORK_ERROR);
3693
3805
  }
3694
3806
  });
3695
3807
  var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from its GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").action(async (orgSlug, siteSlug) => {
3696
3808
  const token = loadToken();
3697
3809
  if (!token) {
3698
- console.log("Not logged in. Run: zerodeploy login");
3699
- return;
3810
+ handleAuthError();
3700
3811
  }
3701
3812
  try {
3702
3813
  const client = getClient(token);
@@ -3705,15 +3816,14 @@ var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from i
3705
3816
  json: { githubRepo: null }
3706
3817
  });
3707
3818
  if (!res.ok) {
3708
- const error = await res.json();
3709
- console.log(`Error: ${error.error || "Failed to unlink repository"}`);
3710
- return;
3819
+ await handleApiError(res);
3711
3820
  }
3712
- const site = await res.json();
3821
+ const { data: site } = await res.json();
3713
3822
  console.log(`Site "${site.name}" unlinked from GitHub repository`);
3714
3823
  } catch (err) {
3715
- const message = err instanceof Error ? err.message : "Unknown error";
3716
- console.error("Failed to unlink repository:", message);
3824
+ const message = err instanceof Error ? err.message : String(err);
3825
+ displayError({ code: "network_error", message: `Failed to unlink repository: ${message}` });
3826
+ process.exit(ExitCode.NETWORK_ERROR);
3717
3827
  }
3718
3828
  });
3719
3829
 
@@ -3721,8 +3831,7 @@ var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from i
3721
3831
  var siteSubdomainCommand = new Command2("subdomain").description("Update the subdomain for a site").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<subdomain>", "New subdomain").action(async (orgSlug, siteSlug, subdomain) => {
3722
3832
  const token = loadToken();
3723
3833
  if (!token) {
3724
- console.log("Not logged in. Run: zerodeploy login");
3725
- return;
3834
+ handleAuthError();
3726
3835
  }
3727
3836
  try {
3728
3837
  const client = getClient(token);
@@ -3731,15 +3840,15 @@ var siteSubdomainCommand = new Command2("subdomain").description("Update the sub
3731
3840
  json: { subdomain }
3732
3841
  });
3733
3842
  if (!res.ok) {
3734
- const error = await res.json();
3735
- throw new Error(error.error || `API Error ${res.status}`);
3843
+ await handleApiError(res);
3736
3844
  }
3737
- const site = await res.json();
3738
- console.log(`✅ Updated subdomain for ${site.name}`);
3845
+ const { data: site } = await res.json();
3846
+ console.log(`Updated subdomain for ${site.name}`);
3739
3847
  console.log(` New URL: ${site.subdomain}.zerodeploy.app`);
3740
3848
  } catch (err) {
3741
- const message = err instanceof Error ? err.message : "Unknown error";
3742
- console.error("Failed to update subdomain:", message);
3849
+ const message = err instanceof Error ? err.message : String(err);
3850
+ displayError({ code: "network_error", message: `Failed to update subdomain: ${message}` });
3851
+ process.exit(ExitCode.NETWORK_ERROR);
3743
3852
  }
3744
3853
  });
3745
3854
 
@@ -3747,8 +3856,7 @@ var siteSubdomainCommand = new Command2("subdomain").description("Update the sub
3747
3856
  var siteRenameCommand = new Command2("rename").description("Rename a site").argument("<siteSlug>", "Site slug").argument("<newName>", "New name for the site").requiredOption("--org <orgSlug>", "Organization slug").action(async (siteSlug, newName, options) => {
3748
3857
  const token = loadToken();
3749
3858
  if (!token) {
3750
- console.log("Not logged in. Run: zerodeploy login");
3751
- return;
3859
+ handleAuthError();
3752
3860
  }
3753
3861
  try {
3754
3862
  const client = getClient(token);
@@ -3757,14 +3865,14 @@ var siteRenameCommand = new Command2("rename").description("Rename a site").argu
3757
3865
  json: { name: newName }
3758
3866
  });
3759
3867
  if (!res.ok) {
3760
- const error = await res.json();
3761
- throw new Error(error.error || `API Error ${res.status}`);
3868
+ await handleApiError(res);
3762
3869
  }
3763
- const site = await res.json();
3764
- console.log(`✅ Renamed site to: ${site.name}`);
3870
+ const { data: site } = await res.json();
3871
+ console.log(`Renamed site to: ${site.name}`);
3765
3872
  } catch (err) {
3766
- const message = err instanceof Error ? err.message : "Unknown error";
3767
- console.error("Failed to rename site:", message);
3873
+ const message = err instanceof Error ? err.message : String(err);
3874
+ displayError({ code: "network_error", message: `Failed to rename site: ${message}` });
3875
+ process.exit(ExitCode.NETWORK_ERROR);
3768
3876
  }
3769
3877
  });
3770
3878
 
@@ -3784,16 +3892,19 @@ function formatNumber(n) {
3784
3892
  return (n / 1000).toFixed(1) + "K";
3785
3893
  return n.toString();
3786
3894
  }
3787
- var siteStatsCommand = new Command2("stats").description("View site traffic analytics").argument("<site>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--period <period>", "Time period: 24h, 7d, or 30d", "7d").action(async (site, options) => {
3895
+ var siteStatsCommand = new Command2("stats").description("View site traffic analytics").argument("<site>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--period <period>", "Time period: 24h, 7d, or 30d", "7d").option("--json", "Output as JSON").action(async (site, options) => {
3788
3896
  const token = loadToken();
3789
3897
  if (!token) {
3790
- console.log("Not logged in. Run: zerodeploy login");
3791
- return;
3898
+ handleAuthError();
3792
3899
  }
3793
3900
  const validPeriods = ["24h", "7d", "30d"];
3794
3901
  if (!validPeriods.includes(options.period)) {
3795
- console.error(`Invalid period. Use: ${validPeriods.join(", ")}`);
3796
- return;
3902
+ displayError({
3903
+ code: "validation_error",
3904
+ message: `Invalid period: ${options.period}`,
3905
+ hint: `Valid periods: ${validPeriods.join(", ")}`
3906
+ });
3907
+ process.exit(ExitCode.VALIDATION_ERROR);
3797
3908
  }
3798
3909
  try {
3799
3910
  const client = getClient(token);
@@ -3802,10 +3913,13 @@ var siteStatsCommand = new Command2("stats").description("View site traffic anal
3802
3913
  query: { period: options.period }
3803
3914
  });
3804
3915
  if (!res.ok) {
3805
- const error = await res.json();
3806
- throw new Error(error.error || `API Error ${res.status}`);
3916
+ await handleApiError(res);
3917
+ }
3918
+ const { data } = await res.json();
3919
+ if (options.json) {
3920
+ console.log(JSON.stringify(data, null, 2));
3921
+ return;
3807
3922
  }
3808
- const data = await res.json();
3809
3923
  if (!data.configured) {
3810
3924
  console.log("Analytics not configured for this site.");
3811
3925
  return;
@@ -3873,8 +3987,9 @@ var siteStatsCommand = new Command2("stats").description("View site traffic anal
3873
3987
  }
3874
3988
  console.log("View detailed analytics in the dashboard.");
3875
3989
  } catch (err) {
3876
- const message = err instanceof Error ? err.message : "Unknown error";
3877
- console.error("Failed to get analytics:", message);
3990
+ const message = err instanceof Error ? err.message : String(err);
3991
+ displayError({ code: "network_error", message: `Failed to get analytics: ${message}` });
3992
+ process.exit(ExitCode.NETWORK_ERROR);
3878
3993
  }
3879
3994
  });
3880
3995
 
@@ -3885,8 +4000,7 @@ var siteCommand = new Command2("site").description("Manage sites").addCommand(si
3885
4000
  var domainAddCommand = new Command2("add").description("Add a custom domain to a site").argument("<domain>", "Domain to add (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domain, options) => {
3886
4001
  const token = loadToken();
3887
4002
  if (!token) {
3888
- console.log("Not logged in. Run: zerodeploy login");
3889
- return;
4003
+ handleAuthError();
3890
4004
  }
3891
4005
  try {
3892
4006
  const client = getClient(token);
@@ -3895,17 +4009,16 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3895
4009
  json: { domain }
3896
4010
  });
3897
4011
  if (!res.ok) {
3898
- const error = await res.json();
3899
- throw new Error(error.error || `API Error ${res.status}`);
4012
+ await handleApiError(res);
3900
4013
  }
3901
- const data = await res.json();
4014
+ const { data } = await res.json();
3902
4015
  console.log(`
3903
- Domain added: ${data.domain}`);
4016
+ Domain added: ${data.domain}`);
3904
4017
  console.log(` ID: ${data.id}`);
3905
4018
  console.log(` Status: ${data.verification_status}`);
3906
4019
  console.log();
3907
- console.log("\uD83D\uDCCB DNS Verification Required");
3908
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
4020
+ console.log("DNS Verification Required");
4021
+ console.log("-------------------------");
3909
4022
  console.log();
3910
4023
  console.log("Add the following TXT record to your DNS:");
3911
4024
  console.log();
@@ -3917,8 +4030,9 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3917
4030
  console.log(` zerodeploy domain verify ${data.domain} --org ${options.org} --site ${options.site}`);
3918
4031
  console.log();
3919
4032
  } catch (err) {
3920
- const message = err instanceof Error ? err.message : "Unknown error";
3921
- console.error("Failed to add domain:", message);
4033
+ const message = err instanceof Error ? err.message : String(err);
4034
+ displayError({ code: "network_error", message: `Failed to add domain: ${message}` });
4035
+ process.exit(ExitCode.NETWORK_ERROR);
3922
4036
  }
3923
4037
  });
3924
4038
 
@@ -3926,11 +4040,11 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3926
4040
  function formatStatus(status) {
3927
4041
  switch (status) {
3928
4042
  case "verified":
3929
- return "verified";
4043
+ return "verified";
3930
4044
  case "pending":
3931
- return "pending";
4045
+ return "pending";
3932
4046
  case "failed":
3933
- return "failed";
4047
+ return "failed";
3934
4048
  default:
3935
4049
  return status;
3936
4050
  }
@@ -3938,18 +4052,17 @@ function formatStatus(status) {
3938
4052
  function formatRedirect(mode) {
3939
4053
  switch (mode) {
3940
4054
  case "www_to_apex":
3941
- return "wwwapex";
4055
+ return "www->apex";
3942
4056
  case "apex_to_www":
3943
- return "apexwww";
4057
+ return "apex->www";
3944
4058
  default:
3945
4059
  return "";
3946
4060
  }
3947
4061
  }
3948
- 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) => {
4062
+ var domainListCommand = new Command2("list").description("List custom domains for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--json", "Output as JSON").action(async (options) => {
3949
4063
  const token = loadToken();
3950
4064
  if (!token) {
3951
- console.log("Not logged in. Run: zerodeploy login");
3952
- return;
4065
+ handleAuthError();
3953
4066
  }
3954
4067
  try {
3955
4068
  const client = getClient(token);
@@ -3957,10 +4070,13 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3957
4070
  param: { orgSlug: options.org, siteSlug: options.site }
3958
4071
  });
3959
4072
  if (!res.ok) {
3960
- const error = await res.json();
3961
- throw new Error(error.error || `API Error ${res.status}`);
4073
+ await handleApiError(res);
3962
4074
  }
3963
4075
  const { data: domains } = await res.json();
4076
+ if (options.json) {
4077
+ console.log(JSON.stringify(domains, null, 2));
4078
+ return;
4079
+ }
3964
4080
  if (domains.length === 0) {
3965
4081
  console.log("No custom domains configured.");
3966
4082
  console.log();
@@ -3977,17 +4093,17 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3977
4093
  }
3978
4094
  console.log();
3979
4095
  } catch (err) {
3980
- const message = err instanceof Error ? err.message : "Unknown error";
3981
- console.error("Failed to list domains:", message);
4096
+ const message = err instanceof Error ? err.message : String(err);
4097
+ displayError({ code: "network_error", message: `Failed to list domains: ${message}` });
4098
+ process.exit(ExitCode.NETWORK_ERROR);
3982
4099
  }
3983
4100
  });
3984
4101
 
3985
4102
  // src/commands/domain/verify.ts
3986
- 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) => {
4103
+ 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").option("--json", "Output as JSON").action(async (domainName, options) => {
3987
4104
  const token = loadToken();
3988
4105
  if (!token) {
3989
- console.log("Not logged in. Run: zerodeploy login");
3990
- return;
4106
+ handleAuthError();
3991
4107
  }
3992
4108
  try {
3993
4109
  const client = getClient(token);
@@ -3995,39 +4111,40 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3995
4111
  param: { orgSlug: options.org, siteSlug: options.site }
3996
4112
  });
3997
4113
  if (!listRes.ok) {
3998
- const error = await listRes.json();
3999
- throw new Error(error.error || `API Error ${listRes.status}`);
4114
+ await handleApiError(listRes);
4000
4115
  }
4001
4116
  const { data: domains } = await listRes.json();
4002
4117
  const domain = domains.find((d) => d.domain === domainName);
4003
4118
  if (!domain) {
4004
- console.error(`
4005
- ❌ Domain not found: ${domainName}`);
4006
- console.log();
4007
- console.log("Add it first with:");
4008
- console.log(` zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`);
4009
- return;
4119
+ displayError({
4120
+ code: "not_found",
4121
+ message: `Domain not found: ${domainName}`,
4122
+ hint: `Add it first with:
4123
+ zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`
4124
+ });
4125
+ process.exit(ExitCode.NOT_FOUND);
4010
4126
  }
4011
4127
  const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].verify.$post({
4012
4128
  param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
4013
4129
  });
4014
- const data = await res.json();
4130
+ const { data } = await res.json();
4015
4131
  if (!res.ok) {
4016
- console.error(`
4017
- Verification failed for ${domainName}`);
4018
- if (data.message) {
4019
- console.log();
4020
- console.log(data.message);
4021
- }
4022
- console.log();
4023
- console.log("Tips:");
4024
- console.log(" • DNS changes can take up to 48 hours to propagate");
4025
- console.log(` • Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}`);
4026
- console.log(" • Try again in a few minutes");
4132
+ displayError({
4133
+ code: "verification_failed",
4134
+ message: `Verification failed for ${domainName}`,
4135
+ hint: `Tips:
4136
+ - DNS changes can take up to 48 hours to propagate
4137
+ - Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}
4138
+ - Try again in a few minutes`
4139
+ });
4140
+ process.exit(ExitCode.VALIDATION_ERROR);
4141
+ }
4142
+ if (options.json) {
4143
+ console.log(JSON.stringify(data, null, 2));
4027
4144
  return;
4028
4145
  }
4029
4146
  console.log(`
4030
- Domain verified: ${data.domain}`);
4147
+ Domain verified: ${data.domain}`);
4031
4148
  console.log();
4032
4149
  if (data.cloudflare) {
4033
4150
  console.log(` Cloudflare SSL: ${data.cloudflare.status}`);
@@ -4036,8 +4153,8 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
4036
4153
  const domainParts = data.domain.split(".");
4037
4154
  const isApexDomain = domainParts.length === 2;
4038
4155
  const target = `${data.siteSubdomain || "your-site"}.zerodeploy.app`;
4039
- console.log("\uD83D\uDCCB Final DNS Setup");
4040
- console.log("━━━━━━━━━━━━━━━━━━━");
4156
+ console.log("Final DNS Setup");
4157
+ console.log("---------------");
4041
4158
  console.log();
4042
4159
  if (isApexDomain) {
4043
4160
  console.log("For apex domains, DNS setup depends on your provider:");
@@ -4067,8 +4184,9 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
4067
4184
  console.log(` https://${data.domain}`);
4068
4185
  console.log();
4069
4186
  } catch (err) {
4070
- const message = err instanceof Error ? err.message : "Unknown error";
4071
- console.error("Failed to verify domain:", message);
4187
+ const message = err instanceof Error ? err.message : String(err);
4188
+ displayError({ code: "network_error", message: `Failed to verify domain: ${message}` });
4189
+ process.exit(ExitCode.NETWORK_ERROR);
4072
4190
  }
4073
4191
  });
4074
4192
 
@@ -4076,8 +4194,7 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
4076
4194
  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) => {
4077
4195
  const token = loadToken();
4078
4196
  if (!token) {
4079
- console.log("Not logged in. Run: zerodeploy login");
4080
- return;
4197
+ handleAuthError();
4081
4198
  }
4082
4199
  try {
4083
4200
  const client = getClient(token);
@@ -4085,31 +4202,31 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
4085
4202
  param: { orgSlug: options.org, siteSlug: options.site }
4086
4203
  });
4087
4204
  if (!listRes.ok) {
4088
- const error = await listRes.json();
4089
- throw new Error(error.error || `API Error ${listRes.status}`);
4205
+ await handleApiError(listRes);
4090
4206
  }
4091
4207
  const { data: domains } = await listRes.json();
4092
4208
  const domain = domains.find((d) => d.domain === domainName);
4093
4209
  if (!domain) {
4094
- console.error(`
4095
- ❌ Domain not found: ${domainName}`);
4096
- console.log();
4097
- console.log("List domains with:");
4098
- console.log(` zerodeploy domain list --org ${options.org} --site ${options.site}`);
4099
- return;
4210
+ displayError({
4211
+ code: "not_found",
4212
+ message: `Domain not found: ${domainName}`,
4213
+ hint: `List domains with:
4214
+ zerodeploy domain list --org ${options.org} --site ${options.site}`
4215
+ });
4216
+ process.exit(ExitCode.NOT_FOUND);
4100
4217
  }
4101
4218
  const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].$delete({
4102
4219
  param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
4103
4220
  });
4104
4221
  if (!res.ok) {
4105
- const error = await res.json();
4106
- throw new Error(error.error || `API Error ${res.status}`);
4222
+ await handleApiError(res);
4107
4223
  }
4108
4224
  const data = await res.json();
4109
- console.log(`✅ ${data.message}`);
4225
+ console.log(data.message);
4110
4226
  } catch (err) {
4111
- const message = err instanceof Error ? err.message : "Unknown error";
4112
- console.error("Failed to remove domain:", message);
4227
+ const message = err instanceof Error ? err.message : String(err);
4228
+ displayError({ code: "network_error", message: `Failed to remove domain: ${message}` });
4229
+ process.exit(ExitCode.NETWORK_ERROR);
4113
4230
  }
4114
4231
  });
4115
4232
 
@@ -4119,24 +4236,26 @@ function formatRedirectMode(mode) {
4119
4236
  case "none":
4120
4237
  return "No redirect";
4121
4238
  case "www_to_apex":
4122
- return "www apex (e.g., www.example.com example.com)";
4239
+ return "www -> apex (e.g., www.example.com -> example.com)";
4123
4240
  case "apex_to_www":
4124
- return "apex www (e.g., example.com www.example.com)";
4241
+ return "apex -> www (e.g., example.com -> www.example.com)";
4125
4242
  default:
4126
4243
  return mode;
4127
4244
  }
4128
4245
  }
4129
- 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) => {
4246
+ 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").option("--json", "Output as JSON").action(async (domain, options) => {
4130
4247
  const token = loadToken();
4131
4248
  if (!token) {
4132
- console.log("Not logged in. Run: zerodeploy login");
4133
- return;
4249
+ handleAuthError();
4134
4250
  }
4135
4251
  const validModes = ["none", "www_to_apex", "apex_to_www"];
4136
4252
  if (!validModes.includes(options.mode)) {
4137
- console.error(`Invalid mode: ${options.mode}`);
4138
- console.error("Valid modes: none, www_to_apex, apex_to_www");
4139
- return;
4253
+ displayError({
4254
+ code: "validation_error",
4255
+ message: `Invalid mode: ${options.mode}`,
4256
+ hint: "Valid modes: none, www_to_apex, apex_to_www"
4257
+ });
4258
+ process.exit(ExitCode.VALIDATION_ERROR);
4140
4259
  }
4141
4260
  try {
4142
4261
  const client = getClient(token);
@@ -4144,32 +4263,38 @@ var domainRedirectCommand = new Command2("redirect").description("Set redirect m
4144
4263
  param: { orgSlug: options.org, siteSlug: options.site }
4145
4264
  });
4146
4265
  if (!listRes.ok) {
4147
- const error = await listRes.json();
4148
- throw new Error(error.error || `API Error ${listRes.status}`);
4266
+ await handleApiError(listRes);
4149
4267
  }
4150
4268
  const { data: domains } = await listRes.json();
4151
4269
  const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
4152
4270
  if (!targetDomain) {
4153
- console.error(`Domain not found: ${domain}`);
4154
- console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
4155
- return;
4271
+ displayError({
4272
+ code: "not_found",
4273
+ message: `Domain not found: ${domain}`,
4274
+ hint: `Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`
4275
+ });
4276
+ process.exit(ExitCode.NOT_FOUND);
4156
4277
  }
4157
4278
  const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].redirect.$patch({
4158
4279
  param: { orgSlug: options.org, siteSlug: options.site, domainId: targetDomain.id },
4159
4280
  json: { redirectMode: options.mode }
4160
4281
  });
4161
4282
  if (!res.ok) {
4162
- const error = await res.json();
4163
- throw new Error(error.error || `API Error ${res.status}`);
4283
+ await handleApiError(res);
4284
+ }
4285
+ const { data } = await res.json();
4286
+ if (options.json) {
4287
+ console.log(JSON.stringify(data, null, 2));
4288
+ return;
4164
4289
  }
4165
- const data = await res.json();
4166
4290
  console.log(`
4167
- Redirect mode updated for ${data.domain}`);
4291
+ Redirect mode updated for ${data.domain}`);
4168
4292
  console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
4169
4293
  console.log();
4170
4294
  } catch (err) {
4171
- const message = err instanceof Error ? err.message : "Unknown error";
4172
- console.error("Failed to update redirect mode:", message);
4295
+ const message = err instanceof Error ? err.message : String(err);
4296
+ displayError({ code: "network_error", message: `Failed to update redirect mode: ${message}` });
4297
+ process.exit(ExitCode.NETWORK_ERROR);
4173
4298
  }
4174
4299
  });
4175
4300
 
@@ -4177,11 +4302,10 @@ var domainRedirectCommand = new Command2("redirect").description("Set redirect m
4177
4302
  var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
4178
4303
 
4179
4304
  // src/commands/form/list.ts
4180
- var formListCommand = new Command2("list").description("List forms for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
4305
+ var formListCommand = new Command2("list").description("List forms for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--json", "Output as JSON").action(async (options) => {
4181
4306
  const token = loadToken();
4182
4307
  if (!token) {
4183
- console.log("Not logged in. Run: zerodeploy login");
4184
- return;
4308
+ handleAuthError();
4185
4309
  }
4186
4310
  try {
4187
4311
  const client = getClient(token);
@@ -4189,10 +4313,13 @@ var formListCommand = new Command2("list").description("List forms for a site").
4189
4313
  param: { orgSlug: options.org, siteSlug: options.site }
4190
4314
  });
4191
4315
  if (!res.ok) {
4192
- const error = await res.json();
4193
- throw new Error(error.error || `API Error ${res.status}`);
4316
+ await handleApiError(res);
4194
4317
  }
4195
4318
  const { data: forms } = await res.json();
4319
+ if (options.json) {
4320
+ console.log(JSON.stringify(forms, null, 2));
4321
+ return;
4322
+ }
4196
4323
  if (forms.length === 0) {
4197
4324
  console.log("No forms found.");
4198
4325
  console.log();
@@ -4211,17 +4338,17 @@ var formListCommand = new Command2("list").description("List forms for a site").
4211
4338
  console.log("Export submissions with:");
4212
4339
  console.log(` zerodeploy form export <name> --org ${options.org} --site ${options.site}`);
4213
4340
  } catch (err) {
4214
- const message = err instanceof Error ? err.message : "Unknown error";
4215
- console.error("Failed to list forms:", message);
4341
+ const message = err instanceof Error ? err.message : String(err);
4342
+ displayError({ code: "network_error", message: `Failed to list forms: ${message}` });
4343
+ process.exit(ExitCode.NETWORK_ERROR);
4216
4344
  }
4217
4345
  });
4218
4346
 
4219
4347
  // src/commands/form/submissions.ts
4220
- var formSubmissionsCommand = new Command2("submissions").description("View recent form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--limit <n>", "Number of submissions to show", "10").option("--offset <n>", "Offset for pagination", "0").action(async (name, options) => {
4348
+ var formSubmissionsCommand = new Command2("submissions").description("View recent form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--limit <n>", "Number of submissions to show", "10").option("--offset <n>", "Offset for pagination", "0").option("--json", "Output as JSON").action(async (name, options) => {
4221
4349
  const token = loadToken();
4222
4350
  if (!token) {
4223
- console.log("Not logged in. Run: zerodeploy login");
4224
- return;
4351
+ handleAuthError();
4225
4352
  }
4226
4353
  try {
4227
4354
  const client = getClient(token);
@@ -4230,11 +4357,14 @@ var formSubmissionsCommand = new Command2("submissions").description("View recen
4230
4357
  query: { limit: options.limit, offset: options.offset }
4231
4358
  });
4232
4359
  if (!res.ok) {
4233
- const error = await res.json();
4234
- throw new Error(error.error || `API Error ${res.status}`);
4360
+ await handleApiError(res);
4235
4361
  }
4236
4362
  const data = await res.json();
4237
4363
  const { form, submissions, total, limit, offset } = data;
4364
+ if (options.json) {
4365
+ console.log(JSON.stringify(data, null, 2));
4366
+ return;
4367
+ }
4238
4368
  if (submissions.length === 0) {
4239
4369
  console.log(`No submissions found for form "${name}".`);
4240
4370
  return;
@@ -4278,8 +4408,9 @@ var formSubmissionsCommand = new Command2("submissions").description("View recen
4278
4408
  console.log("Export all to CSV with:");
4279
4409
  console.log(` zerodeploy form export ${name} --org ${options.org} --site ${options.site}`);
4280
4410
  } catch (err) {
4281
- const message = err instanceof Error ? err.message : "Unknown error";
4282
- console.error("Failed to get submissions:", message);
4411
+ const message = err instanceof Error ? err.message : String(err);
4412
+ displayError({ code: "network_error", message: `Failed to get submissions: ${message}` });
4413
+ process.exit(ExitCode.NETWORK_ERROR);
4283
4414
  }
4284
4415
  });
4285
4416
 
@@ -4289,8 +4420,7 @@ import { resolve } from "path";
4289
4420
  var formExportCommand = new Command2("export").description("Export form submissions as CSV").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("-o, --output <file>", "Output file path (default: <name>-submissions.csv)").action(async (name, options) => {
4290
4421
  const token = loadToken();
4291
4422
  if (!token) {
4292
- console.log("Not logged in. Run: zerodeploy login");
4293
- return;
4423
+ handleAuthError();
4294
4424
  }
4295
4425
  try {
4296
4426
  const client = getClient(token);
@@ -4302,8 +4432,7 @@ var formExportCommand = new Command2("export").description("Export form submissi
4302
4432
  return;
4303
4433
  }
4304
4434
  if (!res.ok) {
4305
- const error = await res.json();
4306
- throw new Error(error.error || `API Error ${res.status}`);
4435
+ await handleApiError(res);
4307
4436
  }
4308
4437
  const csv = await res.text();
4309
4438
  const outputPath = options.output || `${name}-submissions.csv`;
@@ -4313,8 +4442,9 @@ var formExportCommand = new Command2("export").description("Export form submissi
4313
4442
  `).length - 1;
4314
4443
  console.log(`Exported ${lineCount} submissions to ${outputPath}`);
4315
4444
  } catch (err) {
4316
- const message = err instanceof Error ? err.message : "Unknown error";
4317
- console.error("Failed to export form:", message);
4445
+ const message = err instanceof Error ? err.message : String(err);
4446
+ displayError({ code: "network_error", message: `Failed to export form: ${message}` });
4447
+ process.exit(ExitCode.NETWORK_ERROR);
4318
4448
  }
4319
4449
  });
4320
4450
 
@@ -4335,8 +4465,7 @@ function prompt3(question) {
4335
4465
  var formDeleteCommand = new Command2("delete").description("Delete a form and all its submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--force", "Skip confirmation prompt").action(async (name, options) => {
4336
4466
  const token = loadToken();
4337
4467
  if (!token) {
4338
- console.log("Not logged in. Run: zerodeploy login");
4339
- return;
4468
+ handleAuthError();
4340
4469
  }
4341
4470
  if (!options.force) {
4342
4471
  const answer = await prompt3(`Are you sure you want to delete form "${name}" and all its submissions? This cannot be undone. (y/N) `);
@@ -4351,14 +4480,14 @@ var formDeleteCommand = new Command2("delete").description("Delete a form and al
4351
4480
  param: { orgSlug: options.org, siteSlug: options.site, formName: name }
4352
4481
  });
4353
4482
  if (!res.ok) {
4354
- const error = await res.json();
4355
- throw new Error(error.error || `API Error ${res.status}`);
4483
+ await handleApiError(res);
4356
4484
  }
4357
4485
  const result = await res.json();
4358
- console.log(`✅ ${result.message}`);
4486
+ console.log(`${result.message}`);
4359
4487
  } catch (err) {
4360
- const message = err instanceof Error ? err.message : "Unknown error";
4361
- console.error("Failed to delete form:", message);
4488
+ const message = err instanceof Error ? err.message : String(err);
4489
+ displayError({ code: "network_error", message: `Failed to delete form: ${message}` });
4490
+ process.exit(ExitCode.NETWORK_ERROR);
4362
4491
  }
4363
4492
  });
4364
4493
 
@@ -4366,20 +4495,21 @@ var formDeleteCommand = new Command2("delete").description("Delete a form and al
4366
4495
  var formNotifyCommand = new Command2("notify").description("Configure email notifications for form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--email <email>", "Email address to receive notifications").option("--disable", "Disable email notifications").action(async (name, options) => {
4367
4496
  const token = loadToken();
4368
4497
  if (!token) {
4369
- console.log("Not logged in. Run: zerodeploy login");
4370
- return;
4498
+ handleAuthError();
4371
4499
  }
4372
4500
  if (options.email && options.disable) {
4373
- console.error("Cannot use both --email and --disable options");
4374
- return;
4501
+ displayError({ code: "validation_error", message: "Cannot use both --email and --disable options" });
4502
+ process.exit(ExitCode.VALIDATION_ERROR);
4375
4503
  }
4376
4504
  if (!options.email && !options.disable) {
4377
- console.error("Please specify --email <email> or --disable");
4378
- console.log();
4379
- console.log("Examples:");
4380
- console.log(` zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --email alerts@example.com`);
4381
- console.log(` zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --disable`);
4382
- return;
4505
+ displayError({
4506
+ code: "validation_error",
4507
+ message: "Please specify --email <email> or --disable",
4508
+ hint: `Examples:
4509
+ zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --email alerts@example.com
4510
+ zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --disable`
4511
+ });
4512
+ process.exit(ExitCode.VALIDATION_ERROR);
4383
4513
  }
4384
4514
  try {
4385
4515
  const client = getClient(token);
@@ -4389,8 +4519,7 @@ var formNotifyCommand = new Command2("notify").description("Configure email noti
4389
4519
  json: { notification_email: notificationEmail }
4390
4520
  });
4391
4521
  if (!res.ok) {
4392
- const error = await res.json();
4393
- throw new Error(error.error || `API Error ${res.status}`);
4522
+ await handleApiError(res);
4394
4523
  }
4395
4524
  const { form } = await res.json();
4396
4525
  if (form.notification_email) {
@@ -4400,8 +4529,9 @@ var formNotifyCommand = new Command2("notify").description("Configure email noti
4400
4529
  console.log(`Email notifications disabled for form "${name}"`);
4401
4530
  }
4402
4531
  } catch (err) {
4403
- const message = err instanceof Error ? err.message : "Unknown error";
4404
- console.error("Failed to update form notifications:", message);
4532
+ const message = err instanceof Error ? err.message : String(err);
4533
+ displayError({ code: "network_error", message: `Failed to update form notifications: ${message}` });
4534
+ process.exit(ExitCode.NETWORK_ERROR);
4405
4535
  }
4406
4536
  });
4407
4537
 
@@ -5261,33 +5391,136 @@ function createProgressBar(options) {
5261
5391
  };
5262
5392
  }
5263
5393
 
5264
- // src/commands/deploy/list.ts
5265
- 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) => {
5266
- const token = loadToken();
5267
- if (!token) {
5268
- console.log("❌ Not logged in. Run: zerodeploy login");
5269
- return;
5394
+ // src/utils/ci.ts
5395
+ function detectCI() {
5396
+ if (process.env.GITHUB_ACTIONS === "true") {
5397
+ return {
5398
+ name: "github-actions",
5399
+ prNumber: extractGitHubPRNumber(),
5400
+ prTitle: null,
5401
+ commitSha: process.env.GITHUB_SHA || null,
5402
+ branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || null,
5403
+ commitMessage: null
5404
+ };
5270
5405
  }
5271
- try {
5272
- const client = getClient(token);
5273
- const res = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5274
- param: { orgSlug: options.org, siteSlug }
5275
- });
5276
- if (!res.ok) {
5277
- const error = await res.json();
5278
- console.error(`❌ ${error.error}`);
5279
- return;
5280
- }
5281
- const { data: deployments } = await res.json();
5282
- const limit = parseInt(options.limit, 10);
5283
- const shown = deployments.slice(0, limit);
5284
- if (shown.length === 0) {
5285
- console.log("No deployments found");
5286
- return;
5287
- }
5288
- console.log(`
5289
- Deployments for ${options.org}/${siteSlug}:
5290
- `);
5406
+ if (process.env.CIRCLECI === "true") {
5407
+ return {
5408
+ name: "circleci",
5409
+ prNumber: extractCircleCIPRNumber(),
5410
+ prTitle: null,
5411
+ commitSha: process.env.CIRCLE_SHA1 || null,
5412
+ branch: process.env.CIRCLE_BRANCH || null,
5413
+ commitMessage: null
5414
+ };
5415
+ }
5416
+ if (process.env.GITLAB_CI === "true") {
5417
+ return {
5418
+ name: "gitlab-ci",
5419
+ prNumber: process.env.CI_MERGE_REQUEST_IID ? parseInt(process.env.CI_MERGE_REQUEST_IID, 10) : null,
5420
+ prTitle: process.env.CI_MERGE_REQUEST_TITLE || null,
5421
+ commitSha: process.env.CI_COMMIT_SHA || null,
5422
+ branch: process.env.CI_COMMIT_REF_NAME || null,
5423
+ commitMessage: process.env.CI_COMMIT_MESSAGE || null
5424
+ };
5425
+ }
5426
+ if (process.env.BITBUCKET_BUILD_NUMBER) {
5427
+ return {
5428
+ name: "bitbucket-pipelines",
5429
+ prNumber: process.env.BITBUCKET_PR_ID ? parseInt(process.env.BITBUCKET_PR_ID, 10) : null,
5430
+ prTitle: null,
5431
+ commitSha: process.env.BITBUCKET_COMMIT || null,
5432
+ branch: process.env.BITBUCKET_BRANCH || null,
5433
+ commitMessage: process.env.BITBUCKET_COMMIT_MESSAGE || null
5434
+ };
5435
+ }
5436
+ if (process.env.TF_BUILD === "True") {
5437
+ return {
5438
+ name: "azure-pipelines",
5439
+ prNumber: process.env.SYSTEM_PULLREQUEST_PULLREQUESTID ? parseInt(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID, 10) : null,
5440
+ prTitle: null,
5441
+ commitSha: process.env.BUILD_SOURCEVERSION || null,
5442
+ branch: process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH || process.env.BUILD_SOURCEBRANCHNAME || null,
5443
+ commitMessage: process.env.BUILD_SOURCEVERSIONMESSAGE || null
5444
+ };
5445
+ }
5446
+ if (process.env.TRAVIS === "true") {
5447
+ const travisPR = process.env.TRAVIS_PULL_REQUEST;
5448
+ return {
5449
+ name: "travis-ci",
5450
+ prNumber: travisPR && travisPR !== "false" ? parseInt(travisPR, 10) : null,
5451
+ prTitle: null,
5452
+ commitSha: process.env.TRAVIS_COMMIT || null,
5453
+ branch: process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH || null,
5454
+ commitMessage: process.env.TRAVIS_COMMIT_MESSAGE || null
5455
+ };
5456
+ }
5457
+ if (process.env.JENKINS_URL) {
5458
+ return {
5459
+ name: "jenkins",
5460
+ prNumber: extractJenkinsPRNumber(),
5461
+ prTitle: null,
5462
+ commitSha: process.env.GIT_COMMIT || null,
5463
+ branch: process.env.CHANGE_BRANCH || process.env.GIT_BRANCH || null,
5464
+ commitMessage: null
5465
+ };
5466
+ }
5467
+ return null;
5468
+ }
5469
+ function extractGitHubPRNumber() {
5470
+ const ref = process.env.GITHUB_REF;
5471
+ if (process.env.GITHUB_EVENT_NAME === "pull_request" && ref) {
5472
+ const match = ref.match(/refs\/pull\/(\d+)/);
5473
+ if (match?.[1])
5474
+ return parseInt(match[1], 10);
5475
+ }
5476
+ return null;
5477
+ }
5478
+ function extractCircleCIPRNumber() {
5479
+ const url = process.env.CIRCLE_PULL_REQUEST;
5480
+ if (url) {
5481
+ const match = url.match(/\/pull\/(\d+)/);
5482
+ if (match?.[1])
5483
+ return parseInt(match[1], 10);
5484
+ }
5485
+ return null;
5486
+ }
5487
+ function extractJenkinsPRNumber() {
5488
+ const changeId = process.env.CHANGE_ID;
5489
+ if (changeId) {
5490
+ const num = parseInt(changeId, 10);
5491
+ if (!isNaN(num))
5492
+ return num;
5493
+ }
5494
+ return null;
5495
+ }
5496
+
5497
+ // src/commands/deploy/list.ts
5498
+ 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) => {
5499
+ const token = loadToken();
5500
+ if (!token) {
5501
+ console.log("❌ Not logged in. Run: zerodeploy login");
5502
+ return;
5503
+ }
5504
+ try {
5505
+ const client = getClient(token);
5506
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5507
+ param: { orgSlug: options.org, siteSlug }
5508
+ });
5509
+ if (!res.ok) {
5510
+ const error = await res.json();
5511
+ console.error(`❌ ${error.error}`);
5512
+ return;
5513
+ }
5514
+ const { data: deployments } = await res.json();
5515
+ const limit = parseInt(options.limit, 10);
5516
+ const shown = deployments.slice(0, limit);
5517
+ if (shown.length === 0) {
5518
+ console.log("No deployments found");
5519
+ return;
5520
+ }
5521
+ console.log(`
5522
+ Deployments for ${options.org}/${siteSlug}:
5523
+ `);
5291
5524
  for (const d of shown) {
5292
5525
  const current = d.is_current ? " ← current" : "";
5293
5526
  const status = formatStatus2(d.status);
@@ -5333,8 +5566,8 @@ var deployRollbackCommand = new Command2("rollback").description("Rollback to a
5333
5566
  }
5334
5567
  const result = await res.json();
5335
5568
  console.log(`✅ ${result.message}`);
5336
- console.log(` Deployment: ${result.deployment.id}`);
5337
- console.log(` URL: ${result.deployment.url}`);
5569
+ console.log(` Deployment: ${result.data.deployment.id}`);
5570
+ console.log(` URL: ${result.data.deployment.url}`);
5338
5571
  } catch (err) {
5339
5572
  console.error("Failed to rollback:", err.message);
5340
5573
  }
@@ -5359,8 +5592,8 @@ var deployPromoteCommand = new Command2("promote").description("Promote a previe
5359
5592
  }
5360
5593
  const result = await res.json();
5361
5594
  console.log("Deployment promoted to production!");
5362
- console.log(` Deployment: ${result.deployment.id}`);
5363
- console.log(` URL: ${result.deployment.url}`);
5595
+ console.log(` Deployment: ${result.data.deployment.id}`);
5596
+ console.log(` URL: ${result.data.deployment.url}`);
5364
5597
  } catch (err) {
5365
5598
  const message = err instanceof Error ? err.message : "Unknown error";
5366
5599
  console.error("Failed to promote deployment:", message);
@@ -5493,7 +5726,7 @@ async function uploadArchive(token, uploadUrl, archive, onRetry) {
5493
5726
  });
5494
5727
  return res.ok;
5495
5728
  }
5496
- 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("--no-verify", "Skip deployment verification").option("--no-auto-rollback", "Disable automatic rollback on verification failure").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) => {
5729
+ 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("--prod", "Force production deploy (override auto-preview for PRs)").option("--no-verify", "Skip deployment verification").option("--no-auto-rollback", "Disable automatic rollback on verification failure").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) => {
5497
5730
  const cwd = process.cwd();
5498
5731
  const token = loadToken();
5499
5732
  if (!token) {
@@ -5539,7 +5772,7 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
5539
5772
  console.log(`Error: ${error.error || "Failed to create site"}`);
5540
5773
  return;
5541
5774
  }
5542
- const site = await createRes.json();
5775
+ const { data: site } = await createRes.json();
5543
5776
  siteSlug = site.slug;
5544
5777
  console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
5545
5778
  const configPath = getConfigPath(cwd);
@@ -5669,21 +5902,90 @@ Error: Build failed`);
5669
5902
  console.log(`Found ${files.length} files (${formatBytes2(totalSize)})`);
5670
5903
  try {
5671
5904
  const client = getClient(token);
5905
+ const ci = detectCI();
5906
+ if (ci) {
5907
+ console.log(`Detected CI: ${ci.name}`);
5908
+ if (ci.prNumber)
5909
+ console.log(` PR: #${ci.prNumber}`);
5910
+ if (ci.commitSha)
5911
+ console.log(` Commit: ${ci.commitSha.slice(0, 7)}`);
5912
+ if (ci.branch)
5913
+ console.log(` Branch: ${ci.branch}`);
5914
+ console.log("");
5915
+ }
5916
+ const isPreview = options.preview === true || !options.prod && ci?.prNumber != null;
5917
+ if (!options.preview && !options.prod && ci?.prNumber != null) {
5918
+ console.log("Auto-preview enabled for PR deployment");
5919
+ console.log(" Use --prod to deploy to production instead");
5920
+ console.log("");
5921
+ }
5672
5922
  const deployPayload = { siteSlug, orgSlug };
5673
- if (options.pr) {
5674
- deployPayload.prNumber = parseInt(options.pr, 10);
5923
+ const prNumber = options.pr ? parseInt(options.pr, 10) : ci?.prNumber;
5924
+ if (prNumber) {
5925
+ deployPayload.prNumber = prNumber;
5675
5926
  }
5676
- if (options.prTitle) {
5677
- deployPayload.prTitle = options.prTitle;
5927
+ const prTitle = options.prTitle ?? ci?.prTitle;
5928
+ if (prTitle) {
5929
+ deployPayload.prTitle = prTitle;
5678
5930
  }
5679
- if (options.commit) {
5680
- deployPayload.commitSha = options.commit;
5931
+ const commitSha = options.commit ?? ci?.commitSha;
5932
+ if (commitSha) {
5933
+ deployPayload.commitSha = commitSha;
5681
5934
  }
5682
- if (options.commitMessage) {
5683
- deployPayload.commitMessage = options.commitMessage;
5935
+ const commitMessage = options.commitMessage ?? ci?.commitMessage;
5936
+ if (commitMessage) {
5937
+ deployPayload.commitMessage = commitMessage;
5684
5938
  }
5685
- if (options.branch) {
5686
- deployPayload.branch = options.branch;
5939
+ const branch = options.branch ?? ci?.branch;
5940
+ if (branch) {
5941
+ deployPayload.branch = branch;
5942
+ }
5943
+ if (!isPreview && commitSha) {
5944
+ const findRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments["by-commit"][":commitSha"].$get({
5945
+ param: { orgSlug, siteSlug, commitSha }
5946
+ });
5947
+ if (findRes.ok) {
5948
+ const { data: existingDeployment } = await findRes.json();
5949
+ if (existingDeployment && existingDeployment.status === "ready") {
5950
+ console.log(`Found existing deployment for commit ${commitSha.slice(0, 8)}`);
5951
+ console.log("Promoting to production instead of re-uploading...");
5952
+ console.log("");
5953
+ const promoteRes = await client.deployments[":id"].rollback.$post({
5954
+ param: { id: existingDeployment.id }
5955
+ });
5956
+ if (!promoteRes.ok) {
5957
+ const body = await promoteRes.json();
5958
+ displayError(parseApiError(body));
5959
+ return;
5960
+ }
5961
+ const { data: promoteData } = await promoteRes.json();
5962
+ console.log("Deployment promoted!");
5963
+ if (options.githubOutput) {
5964
+ const domain = "zerodeploy.app";
5965
+ const deploymentUrl = `https://${siteSlug}.${domain}`;
5966
+ const previewUrl = `https://${promoteData.deployment.id.slice(0, 8)}-${siteSlug}.${domain}`;
5967
+ const githubOutputFile = process.env.GITHUB_OUTPUT;
5968
+ if (githubOutputFile) {
5969
+ const { appendFileSync } = await import("node:fs");
5970
+ appendFileSync(githubOutputFile, `deployment_id=${promoteData.deployment.id}
5971
+ `);
5972
+ appendFileSync(githubOutputFile, `deployment_url=${deploymentUrl}
5973
+ `);
5974
+ appendFileSync(githubOutputFile, `preview_url=${previewUrl}
5975
+ `);
5976
+ appendFileSync(githubOutputFile, `is_preview=false
5977
+ `);
5978
+ } else {
5979
+ console.log("");
5980
+ console.log("::set-output name=deployment_id::" + promoteData.deployment.id);
5981
+ console.log("::set-output name=deployment_url::" + deploymentUrl);
5982
+ console.log("::set-output name=preview_url::" + previewUrl);
5983
+ console.log("::set-output name=is_preview::false");
5984
+ }
5985
+ }
5986
+ return;
5987
+ }
5988
+ }
5687
5989
  }
5688
5990
  const createRes = await client.deployments.$post({
5689
5991
  json: deployPayload
@@ -5693,7 +5995,7 @@ Error: Build failed`);
5693
5995
  displayError(parseApiError(body));
5694
5996
  return;
5695
5997
  }
5696
- const deployment = await createRes.json();
5998
+ const { data: deployment } = await createRes.json();
5697
5999
  console.log(`Created deployment: ${deployment.id}`);
5698
6000
  const progressBar2 = createProgressBar({ total: files.length, label: "Archiving" });
5699
6001
  const archive = await createTarGz(files, (current, total) => {
@@ -5720,7 +6022,7 @@ Error: Build failed`);
5720
6022
  "Content-Type": "application/json",
5721
6023
  Authorization: `Bearer ${token}`
5722
6024
  },
5723
- body: JSON.stringify({ preview: options.preview || false })
6025
+ body: JSON.stringify({ preview: isPreview })
5724
6026
  }, {
5725
6027
  maxRetries: 3,
5726
6028
  onRetry: (attempt, error, delayMs) => {
@@ -5732,7 +6034,7 @@ Error: Build failed`);
5732
6034
  return;
5733
6035
  }
5734
6036
  finalizeSpinner.stop();
5735
- const verifyUrl = options.preview ? deployment.previewUrl : deployment.url;
6037
+ const verifyUrl = isPreview ? deployment.previewUrl : deployment.url;
5736
6038
  let verified = false;
5737
6039
  if (options.verify !== false) {
5738
6040
  const verifySpinner = createSpinner("Verifying...");
@@ -5748,7 +6050,7 @@ Error: Build failed`);
5748
6050
  } else if (verification.error) {
5749
6051
  console.log(` ${verification.error}`);
5750
6052
  }
5751
- if (options.autoRollback !== false && !options.preview) {
6053
+ if (options.autoRollback !== false && !isPreview) {
5752
6054
  console.log("");
5753
6055
  console.log("Auto-rolling back to previous deployment...");
5754
6056
  const rollback = await autoRollback(token, orgSlug, siteSlug, deployment.id);
@@ -5771,7 +6073,7 @@ Error: Build failed`);
5771
6073
  }
5772
6074
  }
5773
6075
  console.log("");
5774
- if (options.preview) {
6076
+ if (isPreview) {
5775
6077
  console.log("Preview deployment created!");
5776
6078
  if (verified) {
5777
6079
  console.log(`Preview: ${deployment.previewUrl} (verified)`);
@@ -5799,12 +6101,15 @@ Error: Build failed`);
5799
6101
  appendFileSync(githubOutputFile, `deployment_url=${deployment.url}
5800
6102
  `);
5801
6103
  appendFileSync(githubOutputFile, `preview_url=${deployment.previewUrl}
6104
+ `);
6105
+ appendFileSync(githubOutputFile, `is_preview=${isPreview}
5802
6106
  `);
5803
6107
  } else {
5804
6108
  console.log("");
5805
6109
  console.log("::set-output name=deployment_id::" + deployment.id);
5806
6110
  console.log("::set-output name=deployment_url::" + deployment.url);
5807
6111
  console.log("::set-output name=preview_url::" + deployment.previewUrl);
6112
+ console.log("::set-output name=is_preview::" + isPreview);
5808
6113
  }
5809
6114
  }
5810
6115
  } catch (err) {
@@ -5820,20 +6125,24 @@ Error: Build failed`);
5820
6125
  });
5821
6126
 
5822
6127
  // src/commands/deployments/list.ts
5823
- var deploymentsListCommand = new Command2("list").description("List deployments for a site").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").action(async (site, options) => {
6128
+ var deploymentsListCommand = new Command2("list").description("List deployments for a site").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").option("--json", "Output as JSON").action(async (site, options) => {
5824
6129
  const token = loadToken();
5825
6130
  if (!token) {
5826
- console.log("Not logged in. Run: zerodeploy login");
5827
- return;
6131
+ handleAuthError();
5828
6132
  }
5829
6133
  try {
5830
6134
  const client = getClient(token);
5831
6135
  const res = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5832
6136
  param: { orgSlug: options.org, siteSlug: site }
5833
6137
  });
5834
- if (!res.ok)
5835
- throw new Error(`API Error ${res.status}`);
6138
+ if (!res.ok) {
6139
+ await handleApiError(res);
6140
+ }
5836
6141
  const { data: deployments } = await res.json();
6142
+ if (options.json) {
6143
+ console.log(JSON.stringify(deployments, null, 2));
6144
+ return;
6145
+ }
5837
6146
  if (deployments.length === 0) {
5838
6147
  console.log("No deployments found.");
5839
6148
  return;
@@ -5856,8 +6165,9 @@ var deploymentsListCommand = new Command2("list").description("List deployments
5856
6165
  console.log();
5857
6166
  }
5858
6167
  } catch (err) {
5859
- const message = err instanceof Error ? err.message : "Unknown error";
5860
- console.error("Failed to list deployments:", message);
6168
+ const message = err instanceof Error ? err.message : String(err);
6169
+ displayError({ code: "network_error", message: `Failed to list deployments: ${message}` });
6170
+ process.exit(ExitCode.NETWORK_ERROR);
5861
6171
  }
5862
6172
  });
5863
6173
 
@@ -5873,24 +6183,23 @@ function formatBytes3(bytes) {
5873
6183
  function formatStatus3(status) {
5874
6184
  switch (status) {
5875
6185
  case "pending":
5876
- return "Pending";
6186
+ return "Pending";
5877
6187
  case "uploading":
5878
- return "\uD83D\uDCE4 Uploading";
6188
+ return "Uploading";
5879
6189
  case "processing":
5880
- return "⚙️ Processing";
6190
+ return "Processing";
5881
6191
  case "ready":
5882
- return "Ready";
6192
+ return "Ready";
5883
6193
  case "failed":
5884
- return "Failed";
6194
+ return "Failed";
5885
6195
  default:
5886
6196
  return status;
5887
6197
  }
5888
6198
  }
5889
- var deploymentsShowCommand = new Command2("show").description("View deployment details").argument("<id>", "Deployment ID (full or short)").action(async (id) => {
6199
+ var deploymentsShowCommand = new Command2("show").description("View deployment details").argument("<id>", "Deployment ID (full or short)").option("--json", "Output as JSON").action(async (id, options) => {
5890
6200
  const token = loadToken();
5891
6201
  if (!token) {
5892
- console.log("Not logged in. Run: zerodeploy login");
5893
- return;
6202
+ handleAuthError();
5894
6203
  }
5895
6204
  try {
5896
6205
  const client = getClient(token);
@@ -5899,15 +6208,20 @@ var deploymentsShowCommand = new Command2("show").description("View deployment d
5899
6208
  });
5900
6209
  if (!res.ok) {
5901
6210
  if (res.status === 404) {
5902
- console.error(`Deployment not found: ${id}`);
5903
- console.log();
5904
- console.log("Use the full deployment ID or at least 8 characters.");
5905
- return;
6211
+ displayError({
6212
+ code: "not_found",
6213
+ message: `Deployment not found: ${id}`,
6214
+ hint: "Use the full deployment ID or at least 8 characters."
6215
+ });
6216
+ process.exit(ExitCode.NOT_FOUND);
5906
6217
  }
5907
- const error = await res.json();
5908
- throw new Error(error.error || `API Error ${res.status}`);
6218
+ await handleApiError(res);
6219
+ }
6220
+ const { data: d } = await res.json();
6221
+ if (options.json) {
6222
+ console.log(JSON.stringify(d, null, 2));
6223
+ return;
5909
6224
  }
5910
- const d = await res.json();
5911
6225
  console.log("Deployment Details");
5912
6226
  console.log("=".repeat(50));
5913
6227
  console.log();
@@ -5957,8 +6271,9 @@ var deploymentsShowCommand = new Command2("show").description("View deployment d
5957
6271
  console.log(` zerodeploy rollback <site> --org <org> --to ${d.id.slice(0, 8)}`);
5958
6272
  }
5959
6273
  } catch (err) {
5960
- const message = err instanceof Error ? err.message : "Unknown error";
5961
- console.error("Failed to get deployment:", message);
6274
+ const message = err instanceof Error ? err.message : String(err);
6275
+ displayError({ code: "network_error", message: `Failed to get deployment: ${message}` });
6276
+ process.exit(ExitCode.NETWORK_ERROR);
5962
6277
  }
5963
6278
  });
5964
6279
 
@@ -5969,8 +6284,7 @@ var deploymentsCommand = new Command2("deployments").description("Manage deploym
5969
6284
  var rollbackCommand = new Command2("rollback").description("Rollback a site to a previous deployment").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").option("--to <deploymentId>", "Deployment ID to rollback to (defaults to previous deployment)").action(async (site, options) => {
5970
6285
  const token = loadToken();
5971
6286
  if (!token) {
5972
- console.log("Not logged in. Run: zerodeploy login");
5973
- return;
6287
+ handleAuthError();
5974
6288
  }
5975
6289
  try {
5976
6290
  const client = getClient(token);
@@ -5979,13 +6293,14 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
5979
6293
  const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5980
6294
  param: { orgSlug: options.org, siteSlug: site }
5981
6295
  });
5982
- if (!listRes.ok)
5983
- throw new Error(`API Error ${listRes.status}`);
6296
+ if (!listRes.ok) {
6297
+ await handleApiError(listRes);
6298
+ }
5984
6299
  const { data: deployments } = await listRes.json();
5985
6300
  const readyDeployments = deployments.filter((d) => d.status === "ready");
5986
6301
  if (readyDeployments.length < 2) {
5987
- console.log("No previous deployment to rollback to.");
5988
- return;
6302
+ displayError({ code: "validation_error", message: "No previous deployment to rollback to." });
6303
+ process.exit(ExitCode.VALIDATION_ERROR);
5989
6304
  }
5990
6305
  const currentIndex = readyDeployments.findIndex((d) => d.is_current);
5991
6306
  if (currentIndex === -1 || currentIndex >= readyDeployments.length - 1) {
@@ -5998,138 +6313,402 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
5998
6313
  param: { id: deploymentId }
5999
6314
  });
6000
6315
  if (!res.ok) {
6001
- const error = await res.json();
6002
- throw new Error(error.error || `API Error ${res.status}`);
6316
+ await handleApiError(res);
6003
6317
  }
6004
6318
  const result = await res.json();
6005
- const shortId = result.deployment.id.slice(0, 8);
6006
- const commit = result.deployment.commit_sha ? ` (${result.deployment.commit_sha.slice(0, 7)})` : "";
6319
+ const shortId = result.data.deployment.id.slice(0, 8);
6320
+ const commit = result.data.deployment.commit_sha ? ` (${result.data.deployment.commit_sha.slice(0, 7)})` : "";
6007
6321
  console.log(`Rolled back to deployment ${shortId}${commit}`);
6008
- console.log(`URL: ${result.deployment.url}`);
6322
+ console.log(`URL: ${result.data.deployment.url}`);
6009
6323
  } catch (err) {
6010
- const message = err instanceof Error ? err.message : "Unknown error";
6011
- console.error("Failed to rollback:", message);
6324
+ const message = err instanceof Error ? err.message : String(err);
6325
+ displayError({ code: "network_error", message: `Failed to rollback: ${message}` });
6326
+ process.exit(ExitCode.NETWORK_ERROR);
6012
6327
  }
6013
6328
  });
6014
6329
 
6015
6330
  // src/commands/token/create.ts
6016
- var tokenCreateCommand = new Command2("create").description("Create a deploy token for a site").argument("<name>", 'Token name (e.g., "GitHub Actions")').requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (name, options) => {
6331
+ var tokenCreateCommand = new Command2("create").description("Create an API token").argument("<name>", 'Token name (e.g., "GitHub Actions", "CI Pipeline")').option("--site <siteId>", "Create a site-scoped deploy token (instead of a PAT)").option("--org <orgSlug>", "Create an org-scoped deploy token (instead of a PAT)").option("--expires <days>", "Token expiration in days (default: never)", parseInt).option("--json", "Output as JSON").action(async (name, options) => {
6017
6332
  const token = loadToken();
6018
6333
  if (!token) {
6019
- console.log("Not logged in. Run: zerodeploy login");
6020
- return;
6334
+ handleAuthError();
6335
+ }
6336
+ if (options.site && options.org) {
6337
+ displayError({ code: "validation_error", message: "--site and --org cannot be used together" });
6338
+ process.exit(ExitCode.VALIDATION_ERROR);
6021
6339
  }
6022
6340
  try {
6023
6341
  const client = getClient(token);
6024
- const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$post({
6025
- param: { orgSlug: options.org, siteSlug: options.site },
6026
- json: { name }
6027
- });
6342
+ let scopeType = "user";
6343
+ let permissions = ["*"];
6344
+ if (options.site) {
6345
+ scopeType = "site";
6346
+ permissions = ["deploy"];
6347
+ } else if (options.org) {
6348
+ scopeType = "org";
6349
+ permissions = ["deploy"];
6350
+ }
6351
+ const body = {
6352
+ name,
6353
+ scope_type: scopeType,
6354
+ permissions
6355
+ };
6356
+ if (options.site) {
6357
+ body.site_id = options.site;
6358
+ }
6359
+ if (options.org) {
6360
+ body.org_slug = options.org;
6361
+ }
6362
+ if (options.expires) {
6363
+ body.expires_in_days = options.expires;
6364
+ }
6365
+ const res = await client.tokens.$post({ json: body });
6028
6366
  if (!res.ok) {
6029
- const error = await res.json();
6030
- console.log(`Error: ${error.error || "Failed to create token"}`);
6367
+ await handleApiError(res);
6368
+ }
6369
+ const { data: result } = await res.json();
6370
+ if (options.json) {
6371
+ console.log(JSON.stringify(result, null, 2));
6031
6372
  return;
6032
6373
  }
6033
- const result = await res.json();
6374
+ let typeLabel = "Personal Access Token (PAT)";
6375
+ if (result.scope_type === "site") {
6376
+ typeLabel = "Site Deploy Token";
6377
+ } else if (result.scope_type === "org") {
6378
+ typeLabel = "Org Deploy Token";
6379
+ }
6034
6380
  console.log("");
6035
- console.log("Deploy token created successfully!");
6381
+ console.log(`${typeLabel} created successfully!`);
6036
6382
  console.log("");
6037
- console.log(`Name: ${result.name}`);
6038
- console.log(`ID: ${result.id}`);
6383
+ console.log(`Name: ${result.name}`);
6384
+ console.log(`ID: ${result.id}`);
6385
+ console.log(`Prefix: ${result.token_prefix}`);
6386
+ console.log(`Permissions: ${result.permissions.join(", ")}`);
6387
+ if (result.org_slug) {
6388
+ console.log(`Org: ${result.org_slug}`);
6389
+ }
6390
+ if (result.expires_at) {
6391
+ console.log(`Expires: ${new Date(result.expires_at).toLocaleDateString()}`);
6392
+ }
6039
6393
  console.log("");
6040
6394
  console.log("Token (save this - it will not be shown again):");
6041
6395
  console.log("");
6042
6396
  console.log(` ${result.token}`);
6043
6397
  console.log("");
6044
- console.log("Usage in GitHub Actions:");
6045
- console.log(" Add this token as a repository secret named ZERODEPLOY_TOKEN");
6398
+ if (result.scope_type === "site" || result.scope_type === "org") {
6399
+ console.log("Usage in GitHub Actions:");
6400
+ console.log(" Add this token as a repository secret named ZERODEPLOY_TOKEN");
6401
+ if (result.scope_type === "org") {
6402
+ console.log(" This token can deploy to any site in the organization.");
6403
+ }
6404
+ } else {
6405
+ console.log("Usage:");
6406
+ console.log(" Use this token in the Authorization header:");
6407
+ console.log(" Authorization: Bearer <token>");
6408
+ }
6046
6409
  console.log("");
6047
6410
  } catch (err) {
6048
- const message = err instanceof Error ? err.message : "Unknown error";
6049
- console.log(`Failed to create token: ${message}`);
6411
+ const message = err instanceof Error ? err.message : String(err);
6412
+ displayError({ code: "network_error", message: `Failed to create token: ${message}` });
6413
+ process.exit(ExitCode.NETWORK_ERROR);
6050
6414
  }
6051
6415
  });
6052
6416
 
6053
6417
  // src/commands/token/list.ts
6054
- var tokenListCommand = new Command2("list").description("List deploy tokens for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
6418
+ var tokenListCommand = new Command2("list").description("List API tokens").option("--site <siteId>", "Filter by site ID (for site-scoped deploy tokens)").option("--org <orgId>", "Filter by org ID (for org-scoped deploy tokens)").option("--type <type>", "Filter by type: user (PAT), site, or org (deploy tokens)", "all").option("--json", "Output as JSON").action(async (options) => {
6055
6419
  const token = loadToken();
6056
6420
  if (!token) {
6057
- console.log("Not logged in. Run: zerodeploy login");
6058
- return;
6421
+ handleAuthError();
6059
6422
  }
6060
6423
  try {
6061
6424
  const client = getClient(token);
6062
- const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6063
- param: { orgSlug: options.org, siteSlug: options.site }
6064
- });
6425
+ const query = {};
6426
+ if (options.type === "user") {
6427
+ query.scope_type = "user";
6428
+ } else if (options.type === "site") {
6429
+ query.scope_type = "site";
6430
+ } else if (options.type === "org") {
6431
+ query.scope_type = "org";
6432
+ }
6433
+ if (options.site) {
6434
+ query.site_id = options.site;
6435
+ }
6436
+ if (options.org) {
6437
+ query.org_id = options.org;
6438
+ }
6439
+ const res = await client.tokens.$get({ query });
6065
6440
  if (!res.ok) {
6066
- const error = await res.json();
6067
- console.log(`Error: ${error.error || "Failed to list tokens"}`);
6068
- return;
6441
+ await handleApiError(res);
6069
6442
  }
6070
6443
  const { data: tokens } = await res.json();
6444
+ if (options.json) {
6445
+ console.log(JSON.stringify(tokens, null, 2));
6446
+ return;
6447
+ }
6071
6448
  if (tokens.length === 0) {
6072
- console.log(`No deploy tokens for ${options.org}/${options.site}`);
6449
+ console.log("No API tokens found");
6450
+ if (options.type !== "all") {
6451
+ console.log(`(filtered by type: ${options.type})`);
6452
+ }
6073
6453
  return;
6074
6454
  }
6075
- console.log(`Deploy tokens for ${options.org}/${options.site}:`);
6455
+ console.log("API Tokens:");
6076
6456
  console.log("");
6077
6457
  for (const t of tokens) {
6078
6458
  const created = new Date(t.created_at).toLocaleDateString();
6079
6459
  const lastUsed = t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : "Never";
6080
- console.log(` ${t.id.slice(0, 8)} ${t.name.padEnd(20)} Created: ${created} Last used: ${lastUsed}`);
6460
+ const expires = t.expires_at ? new Date(t.expires_at).toLocaleDateString() : "Never";
6461
+ let typeLabel = "PAT";
6462
+ if (t.scope_type === "site") {
6463
+ typeLabel = "Site";
6464
+ } else if (t.scope_type === "org") {
6465
+ typeLabel = "Org";
6466
+ }
6467
+ const permissions = t.permissions.join(", ");
6468
+ console.log(` ${t.token_prefix.padEnd(14)} ${t.name.padEnd(20)} [${typeLabel}]`);
6469
+ console.log(` Created: ${created} Last used: ${lastUsed} Expires: ${expires}`);
6470
+ console.log(` Permissions: ${permissions}`);
6471
+ if (t.site_id) {
6472
+ console.log(` Site ID: ${t.site_id}`);
6473
+ }
6474
+ if (t.org_slug) {
6475
+ console.log(` Org: ${t.org_slug}`);
6476
+ }
6477
+ console.log("");
6081
6478
  }
6082
- console.log("");
6083
6479
  } catch (err) {
6084
- const message = err instanceof Error ? err.message : "Unknown error";
6085
- console.log(`Failed to list tokens: ${message}`);
6480
+ const message = err instanceof Error ? err.message : String(err);
6481
+ displayError({ code: "network_error", message: `Failed to list tokens: ${message}` });
6482
+ process.exit(ExitCode.NETWORK_ERROR);
6086
6483
  }
6087
6484
  });
6088
6485
 
6089
6486
  // src/commands/token/delete.ts
6090
- var tokenDeleteCommand = new Command2("delete").description("Delete a deploy token").argument("<tokenId>", "Token ID (or first 8 characters)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (tokenId, options) => {
6487
+ var tokenDeleteCommand = new Command2("delete").description("Delete an API token").argument("<tokenId>", 'Token ID or token prefix (e.g., "zd_abc12345")').action(async (tokenIdOrPrefix) => {
6091
6488
  const token = loadToken();
6092
6489
  if (!token) {
6093
- console.log("Not logged in. Run: zerodeploy login");
6094
- return;
6490
+ handleAuthError();
6095
6491
  }
6096
6492
  try {
6097
6493
  const client = getClient(token);
6098
- let fullTokenId = tokenId;
6099
- if (tokenId.length < 36) {
6100
- const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6101
- param: { orgSlug: options.org, siteSlug: options.site }
6102
- });
6494
+ let fullTokenId = tokenIdOrPrefix;
6495
+ if (tokenIdOrPrefix.length < 36) {
6496
+ const listRes = await client.tokens.$get({ query: {} });
6103
6497
  if (!listRes.ok) {
6104
- const error = await listRes.json();
6105
- console.log(`Error: ${error.error || "Failed to find token"}`);
6106
- return;
6498
+ await handleApiError(listRes);
6107
6499
  }
6108
6500
  const { data: tokens } = await listRes.json();
6109
- const match = tokens.find((t) => t.id.startsWith(tokenId));
6501
+ const match = tokens.find((t) => t.token_prefix === tokenIdOrPrefix || t.token_prefix.startsWith(tokenIdOrPrefix) || t.id.startsWith(tokenIdOrPrefix));
6110
6502
  if (!match) {
6111
- console.log(`Error: No token found starting with "${tokenId}"`);
6112
- return;
6503
+ displayError({ code: "not_found", message: `No token found matching "${tokenIdOrPrefix}"`, hint: "Use `zerodeploy token list` to see available tokens" });
6504
+ process.exit(ExitCode.NOT_FOUND);
6113
6505
  }
6114
6506
  fullTokenId = match.id;
6507
+ console.log(`Found token: ${match.name} (${match.token_prefix})`);
6115
6508
  }
6116
- const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens[":tokenId"].$delete({
6117
- param: { orgSlug: options.org, siteSlug: options.site, tokenId: fullTokenId }
6509
+ const res = await client.tokens[":tokenId"].$delete({
6510
+ param: { tokenId: fullTokenId }
6118
6511
  });
6119
6512
  if (!res.ok) {
6120
- const error = await res.json();
6121
- console.log(`Error: ${error.error || "Failed to delete token"}`);
6122
- return;
6513
+ await handleApiError(res);
6123
6514
  }
6124
6515
  console.log("Token deleted successfully");
6125
6516
  } catch (err) {
6126
- const message = err instanceof Error ? err.message : "Unknown error";
6127
- console.log(`Failed to delete token: ${message}`);
6517
+ const message = err instanceof Error ? err.message : String(err);
6518
+ displayError({ code: "network_error", message: `Failed to delete token: ${message}` });
6519
+ process.exit(ExitCode.NETWORK_ERROR);
6128
6520
  }
6129
6521
  });
6130
6522
 
6131
6523
  // src/commands/token/index.ts
6132
- var tokenCommand = new Command2("token").description("Manage deploy tokens for CI/CD").addCommand(tokenCreateCommand).addCommand(tokenListCommand).addCommand(tokenDeleteCommand);
6524
+ var tokenCommand = new Command2("token").description("Manage API tokens (PATs and deploy tokens)").addCommand(tokenCreateCommand).addCommand(tokenListCommand).addCommand(tokenDeleteCommand);
6525
+
6526
+ // src/commands/billing/index.ts
6527
+ function formatCents(cents) {
6528
+ return `$${(cents / 100).toFixed(2)}`;
6529
+ }
6530
+ function formatDate(dateStr) {
6531
+ return new Date(dateStr).toLocaleDateString("en-US", {
6532
+ year: "numeric",
6533
+ month: "short",
6534
+ day: "numeric"
6535
+ });
6536
+ }
6537
+ function formatMonth(monthStr) {
6538
+ const [year, month] = monthStr.split("-");
6539
+ return new Date(Number(year), Number(month) - 1).toLocaleDateString("en-US", {
6540
+ year: "numeric",
6541
+ month: "long"
6542
+ });
6543
+ }
6544
+ function getReasonLabel(reason) {
6545
+ const labels = {
6546
+ beta_credit: "Beta Credit",
6547
+ promo_credit: "Promotional Credit",
6548
+ support_credit: "Support Credit",
6549
+ refund: "Refund"
6550
+ };
6551
+ return labels[reason] || reason;
6552
+ }
6553
+ var billingUsageCommand = new Command2("usage").description("Show account balance and current period usage").option("--json", "Output as JSON").action(async (options) => {
6554
+ const token = loadToken();
6555
+ if (!token) {
6556
+ handleAuthError();
6557
+ }
6558
+ try {
6559
+ const client = getClient(token);
6560
+ const res = await client.billing.usage.$get();
6561
+ if (!res.ok) {
6562
+ await handleApiError(res);
6563
+ }
6564
+ const { data } = await res.json();
6565
+ if (options.json) {
6566
+ console.log(JSON.stringify(data, null, 2));
6567
+ return;
6568
+ }
6569
+ console.log();
6570
+ console.log("Account Balance");
6571
+ console.log("───────────────");
6572
+ const balanceColor = data.balance_cents > 0 ? "\x1B[32m" : "\x1B[33m";
6573
+ const reset = "\x1B[0m";
6574
+ console.log(` Balance: ${balanceColor}${data.balance_formatted}${reset}`);
6575
+ console.log(` Status: ${data.billing_status}`);
6576
+ console.log(` Can Deploy: ${data.can_deploy ? "\x1B[32mYes\x1B[0m" : "\x1B[31mNo\x1B[0m"}`);
6577
+ if (!data.can_deploy) {
6578
+ console.log();
6579
+ console.log("\x1B[33m ⚠ Add balance to enable deployments\x1B[0m");
6580
+ }
6581
+ console.log();
6582
+ console.log("Current Period Usage");
6583
+ console.log("────────────────────");
6584
+ console.log(` Period: ${formatDate(data.current_period.start)} - ${formatDate(data.current_period.end)}`);
6585
+ console.log();
6586
+ const usage = data.current_period.usage;
6587
+ const included = data.current_period.included;
6588
+ const cost = data.current_period.estimated_cost;
6589
+ console.log(" Resource Used Included Overage");
6590
+ console.log(" ─────────────────────────────────────────────────────");
6591
+ console.log(` Storage ${usage.storage_gb.toFixed(2)} GB ${included.storage_gb} GB ${cost.storage_cents > 0 ? formatCents(cost.storage_cents) : "-"}`);
6592
+ console.log(` Requests ${usage.requests_millions.toFixed(2)}M ${included.requests_millions}M ${cost.requests_cents > 0 ? formatCents(cost.requests_cents) : "-"}`);
6593
+ console.log(` Form Submissions ${usage.form_submissions} ${included.form_submissions} ${cost.forms_cents > 0 ? formatCents(cost.forms_cents) : "-"}`);
6594
+ console.log(` Custom Domains ${usage.domains_count} ${included.domains_count} ${cost.domains_cents > 0 ? formatCents(cost.domains_cents) : "-"}`);
6595
+ console.log();
6596
+ console.log(` Base: ${formatCents(cost.base_cents)}/month`);
6597
+ if (cost.usage_total_cents > 0) {
6598
+ console.log(` Usage: +${formatCents(cost.usage_total_cents)}`);
6599
+ }
6600
+ console.log(` ─────────────────────────────────────────────────────`);
6601
+ console.log(` Estimated Total: ${formatCents(cost.total_cents)}`);
6602
+ console.log();
6603
+ } catch (err) {
6604
+ const message = err instanceof Error ? err.message : String(err);
6605
+ displayError({
6606
+ code: "network_error",
6607
+ message: `Failed to fetch billing info: ${message}`
6608
+ });
6609
+ process.exit(ExitCode.NETWORK_ERROR);
6610
+ }
6611
+ });
6612
+ var billingBillsCommand = new Command2("bills").description("Show billing history").option("--json", "Output as JSON").option("--limit <n>", "Number of bills to show", "10").action(async (options) => {
6613
+ const token = loadToken();
6614
+ if (!token) {
6615
+ handleAuthError();
6616
+ }
6617
+ try {
6618
+ const client = getClient(token);
6619
+ const res = await client.billing.bills.$get({
6620
+ query: { limit: options.limit }
6621
+ });
6622
+ if (!res.ok) {
6623
+ await handleApiError(res);
6624
+ }
6625
+ const { data: bills, pagination } = await res.json();
6626
+ if (options.json) {
6627
+ console.log(JSON.stringify({ bills, pagination }, null, 2));
6628
+ return;
6629
+ }
6630
+ console.log();
6631
+ console.log("Billing History");
6632
+ console.log("───────────────");
6633
+ if (bills.length === 0) {
6634
+ console.log(" No billing history yet");
6635
+ console.log();
6636
+ return;
6637
+ }
6638
+ console.log();
6639
+ console.log(" Period Status Total");
6640
+ console.log(" ─────────────────────────────────────");
6641
+ for (const bill of bills) {
6642
+ const statusColor = bill.status === "paid" ? "\x1B[32m" : bill.status === "finalized" ? "\x1B[33m" : "\x1B[90m";
6643
+ const reset = "\x1B[0m";
6644
+ console.log(` ${formatMonth(bill.billing_month).padEnd(18)} ${statusColor}${bill.status.padEnd(10)}${reset} ${formatCents(bill.final_amount_cents)}`);
6645
+ }
6646
+ console.log();
6647
+ if (pagination.total > bills.length) {
6648
+ console.log(` Showing ${bills.length} of ${pagination.total} bills`);
6649
+ console.log();
6650
+ }
6651
+ } catch (err) {
6652
+ const message = err instanceof Error ? err.message : String(err);
6653
+ displayError({
6654
+ code: "network_error",
6655
+ message: `Failed to fetch bills: ${message}`
6656
+ });
6657
+ process.exit(ExitCode.NETWORK_ERROR);
6658
+ }
6659
+ });
6660
+ var billingAdjustmentsCommand = new Command2("adjustments").description("Show balance adjustment history (credits)").option("--json", "Output as JSON").option("--limit <n>", "Number of adjustments to show", "10").action(async (options) => {
6661
+ const token = loadToken();
6662
+ if (!token) {
6663
+ handleAuthError();
6664
+ }
6665
+ try {
6666
+ const client = getClient(token);
6667
+ const res = await client.billing.adjustments.$get({
6668
+ query: { limit: options.limit }
6669
+ });
6670
+ if (!res.ok) {
6671
+ await handleApiError(res);
6672
+ }
6673
+ const { data: adjustments, pagination } = await res.json();
6674
+ if (options.json) {
6675
+ console.log(JSON.stringify({ adjustments, pagination }, null, 2));
6676
+ return;
6677
+ }
6678
+ console.log();
6679
+ console.log("Balance Adjustments");
6680
+ console.log("───────────────────");
6681
+ if (adjustments.length === 0) {
6682
+ console.log(" No balance adjustments yet");
6683
+ console.log();
6684
+ return;
6685
+ }
6686
+ console.log();
6687
+ console.log(" Date Reason Amount");
6688
+ console.log(" ────────────────────────────────────────");
6689
+ for (const adj of adjustments) {
6690
+ console.log(` ${formatDate(adj.created_at).padEnd(12)} ${getReasonLabel(adj.reason).padEnd(18)} \x1B[32m+${formatCents(adj.amount_cents)}\x1B[0m`);
6691
+ if (adj.description) {
6692
+ console.log(` ${adj.description}`);
6693
+ }
6694
+ }
6695
+ console.log();
6696
+ if (pagination.total > adjustments.length) {
6697
+ console.log(` Showing ${adjustments.length} of ${pagination.total} adjustments`);
6698
+ console.log();
6699
+ }
6700
+ } catch (err) {
6701
+ const message = err instanceof Error ? err.message : String(err);
6702
+ displayError({
6703
+ code: "network_error",
6704
+ message: `Failed to fetch adjustments: ${message}`
6705
+ });
6706
+ process.exit(ExitCode.NETWORK_ERROR);
6707
+ }
6708
+ });
6709
+ var billingCommand = new Command2("billing").description("View account balance and billing information").addCommand(billingUsageCommand).addCommand(billingBillsCommand).addCommand(billingAdjustmentsCommand).action(async () => {
6710
+ await billingUsageCommand.parseAsync(["usage"], { from: "user" });
6711
+ });
6133
6712
 
6134
6713
  // src/commands/init.ts
6135
6714
  import { writeFileSync, existsSync as existsSync2 } from "node:fs";
@@ -6282,9 +6861,239 @@ var accountCommand = new Command2("account").description("Manage your ZeroDeploy
6282
6861
  }
6283
6862
  }));
6284
6863
 
6864
+ // src/lib/version.ts
6865
+ var VERSION = "0.1.5";
6866
+
6867
+ // src/commands/inspect.ts
6868
+ function getCommandByPath(rootCommand, path3) {
6869
+ let current = rootCommand;
6870
+ for (const name of path3) {
6871
+ const subcommand = current.commands.find((cmd) => cmd.name() === name);
6872
+ if (!subcommand) {
6873
+ return null;
6874
+ }
6875
+ current = subcommand;
6876
+ }
6877
+ return current;
6878
+ }
6879
+ function extractArguments(cmd) {
6880
+ const args = cmd.registeredArguments || [];
6881
+ return args.map((arg) => {
6882
+ const metadata = {
6883
+ name: arg.name(),
6884
+ description: arg.description || "",
6885
+ required: arg.required
6886
+ };
6887
+ if (arg.defaultValue !== undefined) {
6888
+ metadata.default = String(arg.defaultValue);
6889
+ }
6890
+ return metadata;
6891
+ });
6892
+ }
6893
+ function extractOptions(cmd) {
6894
+ const options = cmd.options || [];
6895
+ return options.filter((opt) => {
6896
+ const longName = opt.long?.replace(/^--/, "") || "";
6897
+ return longName !== "help" && longName !== "version";
6898
+ }).map((opt) => {
6899
+ const longName = opt.long?.replace(/^--/, "") || "";
6900
+ const shortName = opt.short?.replace(/^-/, "");
6901
+ const metadata = {
6902
+ name: longName,
6903
+ description: opt.description || "",
6904
+ required: opt.mandatory || false
6905
+ };
6906
+ if (shortName) {
6907
+ metadata.short = shortName;
6908
+ }
6909
+ if (opt.flags.includes("<") || opt.flags.includes("[")) {
6910
+ metadata.type = "string";
6911
+ } else {
6912
+ metadata.type = "boolean";
6913
+ }
6914
+ if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
6915
+ metadata.default = String(opt.defaultValue);
6916
+ }
6917
+ return metadata;
6918
+ });
6919
+ }
6920
+ function generateExamples(cmd, fullPath) {
6921
+ const examples = [];
6922
+ const cmdPath = ["zerodeploy", ...fullPath].join(" ");
6923
+ const args = cmd.registeredArguments || [];
6924
+ let basicExample = cmdPath;
6925
+ for (const arg of args) {
6926
+ if (arg.required) {
6927
+ basicExample += ` <${arg.name()}>`;
6928
+ }
6929
+ }
6930
+ if (basicExample !== cmdPath || args.length === 0) {
6931
+ examples.push(basicExample);
6932
+ }
6933
+ const cmdName = fullPath[fullPath.length - 1];
6934
+ const parentName = fullPath[fullPath.length - 2];
6935
+ if (cmdName === "list" && parentName === "org") {
6936
+ examples.push("zerodeploy org list");
6937
+ } else if (cmdName === "create" && parentName === "org") {
6938
+ examples.push("zerodeploy org create my-company");
6939
+ } else if (cmdName === "list" && parentName === "site") {
6940
+ examples.push("zerodeploy site list acme");
6941
+ } else if (cmdName === "create" && parentName === "site") {
6942
+ examples.push('zerodeploy site create acme "My Site"');
6943
+ } else if (cmdName === "deploy") {
6944
+ examples.push("zerodeploy deploy");
6945
+ examples.push("zerodeploy deploy web --org acme --dir ./dist");
6946
+ }
6947
+ return examples;
6948
+ }
6949
+ function getCommandMetadata(rootCommand, path3) {
6950
+ if (path3.length === 0) {
6951
+ const commands = rootCommand.commands.map((cmd2) => cmd2.name()).filter((name) => name !== "help").sort();
6952
+ return {
6953
+ name: "zerodeploy",
6954
+ version: VERSION,
6955
+ commands
6956
+ };
6957
+ }
6958
+ const cmd = getCommandByPath(rootCommand, path3);
6959
+ if (!cmd) {
6960
+ return { error: `Command not found: ${path3.join(" ")}` };
6961
+ }
6962
+ const metadata = {
6963
+ name: cmd.name(),
6964
+ description: cmd.description() || ""
6965
+ };
6966
+ const subcommands = cmd.commands.map((sub) => sub.name()).filter((name) => name !== "help");
6967
+ if (subcommands.length > 0) {
6968
+ metadata.subcommands = subcommands.sort();
6969
+ return metadata;
6970
+ }
6971
+ metadata.usage = `zerodeploy ${path3.join(" ")}` + (cmd.registeredArguments || []).map((arg) => arg.required ? ` <${arg.name()}>` : ` [${arg.name()}]`).join("");
6972
+ const args = extractArguments(cmd);
6973
+ if (args.length > 0) {
6974
+ metadata.arguments = args;
6975
+ }
6976
+ const options = extractOptions(cmd);
6977
+ if (options.length > 0) {
6978
+ metadata.options = options;
6979
+ }
6980
+ const examples = generateExamples(cmd, path3);
6981
+ if (examples.length > 0) {
6982
+ metadata.examples = [...new Set(examples)];
6983
+ }
6984
+ return metadata;
6985
+ }
6986
+ function createInspectCommand(rootProgram) {
6987
+ return new Command2("inspect").description("Output command metadata as JSON (for LLM/automation use)").argument("[command...]", 'Command path to inspect (e.g., "org create")').action((commandPath) => {
6988
+ const metadata = getCommandMetadata(rootProgram, commandPath || []);
6989
+ console.log(JSON.stringify(metadata, null, 2));
6990
+ });
6991
+ }
6992
+
6993
+ // src/lib/version.ts
6994
+ var VERSION2 = "0.1.5";
6995
+
6996
+ // src/lib/update-check.ts
6997
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
6998
+ import { homedir } from "os";
6999
+ import { join as join2 } from "path";
7000
+ var PACKAGE_NAME = "@zerodeploy/cli";
7001
+ var CACHE_FILE = join2(homedir(), ".zerodeploy", "update-check.json");
7002
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7003
+ function compareVersions(current, latest) {
7004
+ const currentParts = current.split(".").map(Number);
7005
+ const latestParts = latest.split(".").map(Number);
7006
+ for (let i2 = 0;i2 < Math.max(currentParts.length, latestParts.length); i2++) {
7007
+ const a = currentParts[i2] || 0;
7008
+ const b = latestParts[i2] || 0;
7009
+ if (a < b)
7010
+ return -1;
7011
+ if (a > b)
7012
+ return 1;
7013
+ }
7014
+ return 0;
7015
+ }
7016
+ function readCache() {
7017
+ try {
7018
+ if (!existsSync3(CACHE_FILE))
7019
+ return null;
7020
+ const data = JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
7021
+ if (typeof data.latestVersion !== "string" || typeof data.checkedAt !== "number") {
7022
+ return null;
7023
+ }
7024
+ return data;
7025
+ } catch {
7026
+ return null;
7027
+ }
7028
+ }
7029
+ function writeCache(cache) {
7030
+ try {
7031
+ const dir = join2(homedir(), ".zerodeploy");
7032
+ if (!existsSync3(dir)) {
7033
+ mkdirSync(dir, { recursive: true });
7034
+ }
7035
+ writeFileSync2(CACHE_FILE, JSON.stringify(cache), "utf-8");
7036
+ } catch {}
7037
+ }
7038
+ async function fetchLatestVersion() {
7039
+ try {
7040
+ const controller = new AbortController;
7041
+ const timeout = setTimeout(() => controller.abort(), 3000);
7042
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
7043
+ signal: controller.signal,
7044
+ headers: { Accept: "application/json" }
7045
+ });
7046
+ clearTimeout(timeout);
7047
+ if (!res.ok)
7048
+ return null;
7049
+ const data = await res.json();
7050
+ return data.version || null;
7051
+ } catch {
7052
+ return null;
7053
+ }
7054
+ }
7055
+ async function checkForUpdates() {
7056
+ if (process.env.CI || process.env.ZERODEPLOY_NO_UPDATE_CHECK) {
7057
+ return;
7058
+ }
7059
+ try {
7060
+ const cache = readCache();
7061
+ const now = Date.now();
7062
+ let latestVersion = null;
7063
+ if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
7064
+ latestVersion = cache.latestVersion;
7065
+ } else {
7066
+ latestVersion = await fetchLatestVersion();
7067
+ if (latestVersion) {
7068
+ writeCache({ latestVersion, checkedAt: now });
7069
+ } else if (cache) {
7070
+ latestVersion = cache.latestVersion;
7071
+ }
7072
+ }
7073
+ if (!latestVersion)
7074
+ return;
7075
+ if (compareVersions(VERSION, latestVersion) < 0) {
7076
+ printUpdateBanner(latestVersion);
7077
+ }
7078
+ } catch {}
7079
+ }
7080
+ function printUpdateBanner(latestVersion) {
7081
+ const message = `Update available: ${VERSION} → ${latestVersion}`;
7082
+ const command = "Run `npm update -g @zerodeploy/cli` to update";
7083
+ const width = Math.max(message.length, command.length) + 4;
7084
+ const top = "╭" + "─".repeat(width) + "╮";
7085
+ const bottom = "╰" + "─".repeat(width) + "╯";
7086
+ const pad = (s) => "│ " + s.padEnd(width - 2) + "│";
7087
+ console.error("");
7088
+ console.error(top);
7089
+ console.error(pad(message));
7090
+ console.error(pad(command));
7091
+ console.error(bottom);
7092
+ }
7093
+
6285
7094
  // src/index.ts
6286
7095
  var program3 = new Command;
6287
- program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.0").enablePositionalOptions();
7096
+ program3.name("zerodeploy").description("ZeroDeploy CLI").version(VERSION2).enablePositionalOptions();
6288
7097
  program3.addCommand(loginCommand);
6289
7098
  program3.addCommand(logoutCommand);
6290
7099
  program3.addCommand(whoamiCommand);
@@ -6297,6 +7106,9 @@ program3.addCommand(deployCommand);
6297
7106
  program3.addCommand(deploymentsCommand);
6298
7107
  program3.addCommand(rollbackCommand);
6299
7108
  program3.addCommand(tokenCommand);
7109
+ program3.addCommand(billingCommand);
6300
7110
  program3.addCommand(initCommand);
6301
7111
  program3.addCommand(accountCommand);
6302
- program3.parse(process.argv);
7112
+ program3.addCommand(createInspectCommand(program3));
7113
+ await program3.parseAsync(process.argv);
7114
+ await checkForUpdates();