@zerodeploy/cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +184 -16
  2. package/dist/cli.js +1344 -209
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2743,6 +2743,103 @@ import http from "node:http";
2743
2743
  import fs6 from "fs";
2744
2744
  import os2 from "os";
2745
2745
  import path2 from "path";
2746
+
2747
+ // src/utils/errors.ts
2748
+ function parseApiError(body) {
2749
+ if (!body || typeof body !== "object") {
2750
+ return {
2751
+ code: "unknown_error",
2752
+ message: "Unknown error occurred"
2753
+ };
2754
+ }
2755
+ const response = body;
2756
+ if (typeof response.error === "object" && response.error !== null) {
2757
+ return response.error;
2758
+ }
2759
+ if (typeof response.error === "string") {
2760
+ return {
2761
+ code: "api_error",
2762
+ message: response.error
2763
+ };
2764
+ }
2765
+ if ("message" in response && typeof response.message === "string") {
2766
+ return {
2767
+ code: "api_error",
2768
+ message: response.message
2769
+ };
2770
+ }
2771
+ if ("success" in response && response.success === false) {
2772
+ const zodError = response;
2773
+ if (zodError.error?.issues?.[0]) {
2774
+ const issue = zodError.error.issues[0];
2775
+ const field = issue.path?.join(".") || "";
2776
+ return {
2777
+ code: "validation_error",
2778
+ message: field ? `${field}: ${issue.message}` : issue.message
2779
+ };
2780
+ }
2781
+ }
2782
+ return {
2783
+ code: "unknown_error",
2784
+ message: "Unknown error occurred"
2785
+ };
2786
+ }
2787
+ function displayError(error) {
2788
+ if (typeof error === "string") {
2789
+ console.error(`
2790
+ ❌ ${error}
2791
+ `);
2792
+ return;
2793
+ }
2794
+ console.error(`
2795
+ ❌ ${error.message}`);
2796
+ if (error.details && Object.keys(error.details).length > 0) {
2797
+ console.error();
2798
+ for (const [key, value] of Object.entries(error.details)) {
2799
+ const formattedKey = key.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
2800
+ console.error(` ${formattedKey}: ${value}`);
2801
+ }
2802
+ }
2803
+ if (error.hint) {
2804
+ console.error();
2805
+ console.error(` Hint: ${error.hint}`);
2806
+ }
2807
+ if (error.docs) {
2808
+ console.error(` Docs: ${error.docs}`);
2809
+ }
2810
+ console.error();
2811
+ }
2812
+ function displayNetworkError(error) {
2813
+ if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
2814
+ displayError({
2815
+ code: "connection_error",
2816
+ message: "Could not connect to ZeroDeploy API",
2817
+ hint: "Check your internet connection and try again"
2818
+ });
2819
+ } else if (error.message.includes("timeout")) {
2820
+ displayError({
2821
+ code: "timeout_error",
2822
+ message: "Request timed out",
2823
+ hint: "Check your internet connection and try again"
2824
+ });
2825
+ } else {
2826
+ displayError({
2827
+ code: "network_error",
2828
+ message: error.message || "Network error occurred",
2829
+ hint: "Check your internet connection and try again"
2830
+ });
2831
+ }
2832
+ }
2833
+ function displayAuthError() {
2834
+ displayError({
2835
+ code: "unauthorized",
2836
+ message: "Not logged in",
2837
+ hint: "For local use: Run `zerodeploy login`\nFor CI/CD: Set ZERODEPLOY_TOKEN environment variable",
2838
+ docs: "https://zerodeploy.dev/docs/cli/auth"
2839
+ });
2840
+ }
2841
+
2842
+ // src/auth/token.ts
2746
2843
  var DIR = path2.join(os2.homedir(), ".zerodeploy");
2747
2844
  var TOKEN_PATH = path2.join(DIR, "token");
2748
2845
  function saveToken(token) {
@@ -2780,6 +2877,7 @@ var loginCommand = new Command2("login").description("Login via GitHub OAuth wit
2780
2877
  if (req.url?.startsWith(CALLBACK_PATH)) {
2781
2878
  const url = new URL(req.url, `http://localhost:${PORT}`);
2782
2879
  const token = url.searchParams.get("token");
2880
+ const pending = url.searchParams.get("pending") === "true";
2783
2881
  if (!token) {
2784
2882
  res.writeHead(400);
2785
2883
  res.end("Login failed: missing token");
@@ -2787,9 +2885,20 @@ var loginCommand = new Command2("login").description("Login via GitHub OAuth wit
2787
2885
  process.exit(1);
2788
2886
  }
2789
2887
  saveToken(token);
2790
- res.writeHead(200, { "Content-Type": "text/plain" });
2791
- res.end("✅ Login successful! You can close this window.");
2792
- console.log(" Login successful! Token saved locally.");
2888
+ if (pending) {
2889
+ res.writeHead(200, { "Content-Type": "text/plain" });
2890
+ res.end("You're on the waitlist! We'll email you when your account is approved.");
2891
+ console.log("");
2892
+ console.log("\uD83D\uDD50 You're on the waitlist!");
2893
+ console.log("");
2894
+ console.log("Thanks for signing up. Your account is pending approval.");
2895
+ console.log("We'll send you an email when you're approved.");
2896
+ console.log("");
2897
+ } else {
2898
+ res.writeHead(200, { "Content-Type": "text/plain" });
2899
+ res.end("Login successful! You can close this window.");
2900
+ console.log("Login successful! Token saved locally.");
2901
+ }
2793
2902
  server.close();
2794
2903
  } else {
2795
2904
  res.writeHead(404);
@@ -3091,16 +3200,156 @@ var hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
3091
3200
  return req;
3092
3201
  }, []);
3093
3202
 
3203
+ // ../../packages/api-client/src/retry.ts
3204
+ var DEFAULT_OPTIONS = {
3205
+ maxRetries: 3,
3206
+ baseDelayMs: 1000,
3207
+ maxDelayMs: 1e4
3208
+ };
3209
+ var RETRYABLE_STATUS_CODES = new Set([
3210
+ 408,
3211
+ 429,
3212
+ 502,
3213
+ 503,
3214
+ 504
3215
+ ]);
3216
+ function isRetryableError(error) {
3217
+ if (!(error instanceof Error))
3218
+ return false;
3219
+ const message = error.message.toLowerCase();
3220
+ if (message.includes("fetch failed"))
3221
+ return true;
3222
+ if (message.includes("econnrefused"))
3223
+ return true;
3224
+ if (message.includes("econnreset"))
3225
+ return true;
3226
+ if (message.includes("etimedout"))
3227
+ return true;
3228
+ if (message.includes("enotfound"))
3229
+ return true;
3230
+ if (message.includes("enetunreach"))
3231
+ return true;
3232
+ if (message.includes("timeout"))
3233
+ return true;
3234
+ if (message.includes("aborted"))
3235
+ return true;
3236
+ if (message.includes("ssl"))
3237
+ return true;
3238
+ if (message.includes("certificate"))
3239
+ return true;
3240
+ return false;
3241
+ }
3242
+ function isRetryableStatus(status) {
3243
+ return RETRYABLE_STATUS_CODES.has(status);
3244
+ }
3245
+ function calculateDelay(attempt, baseDelayMs, maxDelayMs) {
3246
+ const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
3247
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
3248
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
3249
+ return Math.round(cappedDelay + jitter);
3250
+ }
3251
+ function getDelayFromHeaders(response) {
3252
+ const retryAfter = response.headers.get("Retry-After");
3253
+ if (retryAfter) {
3254
+ const seconds = parseInt(retryAfter, 10);
3255
+ if (!isNaN(seconds)) {
3256
+ return seconds * 1000;
3257
+ }
3258
+ const date = Date.parse(retryAfter);
3259
+ if (!isNaN(date)) {
3260
+ return Math.max(0, date - Date.now());
3261
+ }
3262
+ }
3263
+ const resetAt = response.headers.get("X-RateLimit-Reset");
3264
+ if (resetAt) {
3265
+ const resetTimestamp = parseInt(resetAt, 10);
3266
+ if (!isNaN(resetTimestamp)) {
3267
+ const delayMs = resetTimestamp * 1000 - Date.now();
3268
+ return Math.max(1000, delayMs);
3269
+ }
3270
+ }
3271
+ return null;
3272
+ }
3273
+ function sleep(ms) {
3274
+ return new Promise((resolve) => setTimeout(resolve, ms));
3275
+ }
3276
+ async function fetchWithRetry(input, init, options) {
3277
+ const opts = { ...DEFAULT_OPTIONS, ...options };
3278
+ let lastError;
3279
+ for (let attempt = 0;attempt <= opts.maxRetries; attempt++) {
3280
+ try {
3281
+ const response = await fetch(input, init);
3282
+ if (isRetryableStatus(response.status) && attempt < opts.maxRetries) {
3283
+ const headerDelay = response.status === 429 ? getDelayFromHeaders(response) : null;
3284
+ const delayMs = headerDelay ?? calculateDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
3285
+ const cappedDelay = Math.min(delayMs, opts.maxDelayMs);
3286
+ opts.onRetry?.(attempt + 1, response, cappedDelay);
3287
+ await sleep(cappedDelay);
3288
+ continue;
3289
+ }
3290
+ return response;
3291
+ } catch (error) {
3292
+ lastError = error instanceof Error ? error : new Error(String(error));
3293
+ if (!isRetryableError(error) || attempt >= opts.maxRetries) {
3294
+ throw lastError;
3295
+ }
3296
+ const delayMs = calculateDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
3297
+ opts.onRetry?.(attempt + 1, lastError, delayMs);
3298
+ await sleep(delayMs);
3299
+ }
3300
+ }
3301
+ throw lastError ?? new Error("Max retries exceeded");
3302
+ }
3303
+ function createRetryFetch(options) {
3304
+ return (input, init) => {
3305
+ return fetchWithRetry(input, init, options);
3306
+ };
3307
+ }
3308
+ function formatRetryMessage(attempt, maxRetries, error) {
3309
+ const errorDesc = error instanceof Response ? `HTTP ${error.status}` : error.message.split(`
3310
+ `)[0];
3311
+ return `Retry ${attempt}/${maxRetries} (${errorDesc})`;
3312
+ }
3313
+
3094
3314
  // ../../packages/api-client/src/index.ts
3095
- function createClient(baseUrl, token) {
3315
+ var DEFAULT_RETRY_OPTIONS = {
3316
+ maxRetries: 3,
3317
+ baseDelayMs: 1000,
3318
+ maxDelayMs: 1e4
3319
+ };
3320
+ function createClient(baseUrl, token, options) {
3321
+ let fetchFn = options?.fetch;
3322
+ if (!fetchFn && options?.retry !== false) {
3323
+ const retryOpts = typeof options?.retry === "object" ? options.retry : DEFAULT_RETRY_OPTIONS;
3324
+ fetchFn = createRetryFetch(retryOpts);
3325
+ }
3326
+ if (options?.useCredentials && fetchFn) {
3327
+ const baseFetch = fetchFn;
3328
+ fetchFn = (input, init) => baseFetch(input, { ...init, credentials: "include" });
3329
+ } else if (options?.useCredentials) {
3330
+ fetchFn = (input, init) => fetch(input, { ...init, credentials: "include" });
3331
+ }
3096
3332
  return hc(baseUrl, {
3097
- headers: token ? { Authorization: `Bearer ${token}` } : undefined
3333
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
3334
+ fetch: fetchFn
3098
3335
  });
3099
3336
  }
3100
3337
 
3101
3338
  // src/auth/http.ts
3102
- function getClient(token) {
3103
- return createClient(API_URL, token);
3339
+ var DEFAULT_RETRY_OPTIONS2 = {
3340
+ maxRetries: 3,
3341
+ baseDelayMs: 1000,
3342
+ maxDelayMs: 1e4,
3343
+ onRetry: (attempt, error, delayMs) => {
3344
+ if (process.stderr.isTTY) {
3345
+ const msg = formatRetryMessage(attempt, 3, error);
3346
+ process.stderr.write(`\r\x1B[K ${msg}, waiting ${Math.round(delayMs / 1000)}s...`);
3347
+ }
3348
+ }
3349
+ };
3350
+ function getClient(token, retryOptions) {
3351
+ const opts = { ...DEFAULT_RETRY_OPTIONS2, ...retryOptions };
3352
+ return createClient(API_URL, token, { retry: opts });
3104
3353
  }
3105
3354
 
3106
3355
  // src/commands/whoami.ts
@@ -3118,10 +3367,90 @@ var whoamiCommand = new Command2("whoami").description("Show the currently logge
3118
3367
  const data = await res.json();
3119
3368
  console.log("Logged in as:");
3120
3369
  console.log(` Username: ${data.username}`);
3370
+ console.log(` Email: ${data.email || "(not set)"}`);
3121
3371
  console.log(` User ID: ${data.id}`);
3122
3372
  console.log(` Admin: ${data.isAdmin ? "Yes" : "No"}`);
3373
+ if (data.approved === false) {
3374
+ console.log("");
3375
+ console.log("\uD83D\uDD50 Status: Pending approval (waitlist)");
3376
+ console.log(" We'll email you when your account is approved.");
3377
+ }
3378
+ } catch (err) {
3379
+ console.error("Failed to fetch user info:", err.message || err);
3380
+ process.exit(1);
3381
+ }
3382
+ });
3383
+
3384
+ // src/commands/usage.ts
3385
+ function progressBar(current, max, width = 20) {
3386
+ const percentage = Math.min(current / max, 1);
3387
+ const filled = Math.round(percentage * width);
3388
+ const empty = width - filled;
3389
+ const bar = "█".repeat(filled) + "░".repeat(empty);
3390
+ const color = percentage >= 0.9 ? "\x1B[31m" : percentage >= 0.7 ? "\x1B[33m" : "\x1B[32m";
3391
+ const reset = "\x1B[0m";
3392
+ return `${color}${bar}${reset}`;
3393
+ }
3394
+ function formatUsageLine(label, current, max, suffix = "") {
3395
+ const bar = progressBar(current, max);
3396
+ const percentage = Math.round(current / max * 100);
3397
+ return ` ${label.padEnd(24)} ${bar} ${current}/${max}${suffix} (${percentage}%)`;
3398
+ }
3399
+ var usageCommand = new Command2("usage").description("Show current usage and plan limits").option("--json", "Output as JSON").action(async (options) => {
3400
+ const token = loadToken();
3401
+ if (!token) {
3402
+ displayAuthError();
3403
+ process.exit(1);
3404
+ }
3405
+ try {
3406
+ const client = getClient(token);
3407
+ const res = await client.auth.me.usage.$get();
3408
+ if (!res.ok) {
3409
+ const body = await res.json();
3410
+ displayError(parseApiError(body));
3411
+ process.exit(1);
3412
+ }
3413
+ const data = await res.json();
3414
+ if (options.json) {
3415
+ console.log(JSON.stringify(data, null, 2));
3416
+ return;
3417
+ }
3418
+ console.log();
3419
+ console.log(`Plan: ${data.plan.toUpperCase()}`);
3420
+ console.log();
3421
+ console.log("Account Usage:");
3422
+ console.log(formatUsageLine("Organizations", data.usage.orgs, data.limits.max_orgs));
3423
+ console.log(formatUsageLine("Total Sites", data.usage.sites, data.limits.max_orgs * data.limits.max_sites_per_org));
3424
+ console.log(formatUsageLine("Deployments (month)", data.usage.deployments_this_month, data.limits.max_deployments_per_month));
3425
+ console.log();
3426
+ console.log("Plan Limits:");
3427
+ console.log(` Sites per org: ${data.limits.max_sites_per_org}`);
3428
+ console.log(` Deployments per day: ${data.limits.max_deployments_per_day}`);
3429
+ console.log(` Deployments per month: ${data.limits.max_deployments_per_month}`);
3430
+ console.log(` Max deployment size: ${data.limits.max_deployment_size}`);
3431
+ console.log(` Storage per org: ${data.limits.max_storage_per_org}`);
3432
+ console.log(` Deploy tokens per site: ${data.limits.max_deploy_tokens_per_site}`);
3433
+ console.log(` Domains per site: ${data.limits.max_domains_per_site}`);
3434
+ console.log(` API requests/min: ${data.limits.api_requests_per_minute}`);
3435
+ console.log(` Deploy requests/min: ${data.limits.deploy_requests_per_minute}`);
3436
+ console.log();
3437
+ if (data.orgs.length > 0) {
3438
+ console.log("Organization Usage:");
3439
+ for (const org of data.orgs) {
3440
+ console.log();
3441
+ console.log(` ${org.name} (${org.slug}):`);
3442
+ console.log(formatUsageLine("Sites", org.sites_count, org.limits.max_sites));
3443
+ console.log(formatUsageLine("Deployments (month)", org.deployments_this_month, org.limits.max_deployments_per_month));
3444
+ console.log(` Storage: ${org.storage_used} / ${org.limits.max_storage}`);
3445
+ }
3446
+ console.log();
3447
+ }
3123
3448
  } catch (err) {
3124
- console.error("❌ Failed to fetch user info:", err.message || err);
3449
+ const message = err instanceof Error ? err.message : String(err);
3450
+ displayError({
3451
+ code: "api_error",
3452
+ message: `Failed to fetch usage info: ${message}`
3453
+ });
3125
3454
  process.exit(1);
3126
3455
  }
3127
3456
  });
@@ -3138,7 +3467,7 @@ var orgListCommand = new Command2("list").description("List your organizations")
3138
3467
  const res = await client.orgs.$get();
3139
3468
  if (!res.ok)
3140
3469
  throw new Error(`API Error ${res.status}`);
3141
- const orgs = await res.json();
3470
+ const { data: orgs } = await res.json();
3142
3471
  if (orgs.length === 0) {
3143
3472
  console.log("No organizations found.");
3144
3473
  } else {
@@ -3156,7 +3485,7 @@ var orgListCommand = new Command2("list").description("List your organizations")
3156
3485
  var orgCreateCommand = new Command2("create").description("Create a new organization").argument("<name>", "Organization name").action(async (name) => {
3157
3486
  const token = loadToken();
3158
3487
  if (!token) {
3159
- console.log("❌ Not logged in. Run `zerodeploy login` first.");
3488
+ displayAuthError();
3160
3489
  return;
3161
3490
  }
3162
3491
  try {
@@ -3164,16 +3493,21 @@ var orgCreateCommand = new Command2("create").description("Create a new organiza
3164
3493
  const res = await client.orgs.$post({ json: { name } });
3165
3494
  if (!res.ok) {
3166
3495
  const body = await res.json();
3167
- const message = body.error || `API Error ${res.status}`;
3168
- console.error(`❌ ${message}`);
3496
+ displayError(parseApiError(body));
3169
3497
  return;
3170
3498
  }
3171
3499
  const org = await res.json();
3172
- console.log(`✅ Created org: ${org.name}`);
3500
+ console.log(`
3501
+ ✅ Created org: ${org.name}`);
3173
3502
  console.log(` Slug: ${org.slug}`);
3174
- console.log(` ID: ${org.id}`);
3503
+ console.log(` ID: ${org.id}
3504
+ `);
3175
3505
  } catch (err) {
3176
- console.error("Failed to create org:", err.message || err);
3506
+ if (err instanceof Error) {
3507
+ displayNetworkError(err);
3508
+ } else {
3509
+ displayError({ code: "unknown_error", message: "Failed to create org" });
3510
+ }
3177
3511
  }
3178
3512
  });
3179
3513
 
@@ -3194,7 +3528,7 @@ function prompt(question) {
3194
3528
  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) => {
3195
3529
  const token = loadToken();
3196
3530
  if (!token) {
3197
- console.log("Not logged in. Run: zerodeploy login");
3531
+ displayAuthError();
3198
3532
  return;
3199
3533
  }
3200
3534
  if (!options.force) {
@@ -3210,19 +3544,20 @@ var orgDeleteCommand = new Command2("delete").description("Delete an organizatio
3210
3544
  param: { orgSlug }
3211
3545
  });
3212
3546
  if (!res.ok) {
3213
- const error = await res.json();
3214
- if (error.hint) {
3215
- console.error(`Error: ${error.error}`);
3216
- console.error(`Hint: ${error.hint}`);
3217
- return;
3218
- }
3219
- throw new Error(error.error || `API Error ${res.status}`);
3547
+ const body = await res.json();
3548
+ displayError(parseApiError(body));
3549
+ return;
3220
3550
  }
3221
3551
  const result = await res.json();
3222
- console.log(`✅ ${result.message}`);
3552
+ console.log(`
3553
+ ✅ ${result.message}
3554
+ `);
3223
3555
  } catch (err) {
3224
- const message = err instanceof Error ? err.message : "Unknown error";
3225
- console.error("Failed to delete organization:", message);
3556
+ if (err instanceof Error) {
3557
+ displayNetworkError(err);
3558
+ } else {
3559
+ displayError({ code: "unknown_error", message: "Failed to delete organization" });
3560
+ }
3226
3561
  }
3227
3562
  });
3228
3563
 
@@ -3241,7 +3576,7 @@ var siteListCommand = new Command2("list").description("List sites in an organiz
3241
3576
  const res = await client.orgs[":orgSlug"].sites.$get({ param: { orgSlug } });
3242
3577
  if (!res.ok)
3243
3578
  throw new Error(`API Error ${res.status}`);
3244
- const sites = await res.json();
3579
+ const { data: sites } = await res.json();
3245
3580
  if (sites.length === 0) {
3246
3581
  console.log("No sites found.");
3247
3582
  return;
@@ -3340,13 +3675,10 @@ var siteLinkCommand = new Command2("link").description("Link a site to a GitHub
3340
3675
  return;
3341
3676
  }
3342
3677
  try {
3343
- const res = await fetch(`${API_URL}/orgs/${orgSlug}/sites/${siteSlug}`, {
3344
- method: "PATCH",
3345
- headers: {
3346
- "Content-Type": "application/json",
3347
- Authorization: `Bearer ${token}`
3348
- },
3349
- body: JSON.stringify({ githubRepo: repo })
3678
+ const client = getClient(token);
3679
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3680
+ param: { orgSlug, siteSlug },
3681
+ json: { githubRepo: repo }
3350
3682
  });
3351
3683
  if (!res.ok) {
3352
3684
  const error = await res.json();
@@ -3367,13 +3699,10 @@ var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from i
3367
3699
  return;
3368
3700
  }
3369
3701
  try {
3370
- const res = await fetch(`${API_URL}/orgs/${orgSlug}/sites/${siteSlug}`, {
3371
- method: "PATCH",
3372
- headers: {
3373
- "Content-Type": "application/json",
3374
- Authorization: `Bearer ${token}`
3375
- },
3376
- body: JSON.stringify({ githubRepo: null })
3702
+ const client = getClient(token);
3703
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3704
+ param: { orgSlug, siteSlug },
3705
+ json: { githubRepo: null }
3377
3706
  });
3378
3707
  if (!res.ok) {
3379
3708
  const error = await res.json();
@@ -3414,8 +3743,143 @@ var siteSubdomainCommand = new Command2("subdomain").description("Update the sub
3414
3743
  }
3415
3744
  });
3416
3745
 
3746
+ // src/commands/site/rename.ts
3747
+ 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
+ const token = loadToken();
3749
+ if (!token) {
3750
+ console.log("Not logged in. Run: zerodeploy login");
3751
+ return;
3752
+ }
3753
+ try {
3754
+ const client = getClient(token);
3755
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3756
+ param: { orgSlug: options.org, siteSlug },
3757
+ json: { name: newName }
3758
+ });
3759
+ if (!res.ok) {
3760
+ const error = await res.json();
3761
+ throw new Error(error.error || `API Error ${res.status}`);
3762
+ }
3763
+ const site = await res.json();
3764
+ console.log(`✅ Renamed site to: ${site.name}`);
3765
+ } catch (err) {
3766
+ const message = err instanceof Error ? err.message : "Unknown error";
3767
+ console.error("Failed to rename site:", message);
3768
+ }
3769
+ });
3770
+
3771
+ // src/commands/site/stats.ts
3772
+ function formatBytes(bytes) {
3773
+ if (bytes === 0)
3774
+ return "0 B";
3775
+ const k = 1024;
3776
+ const sizes = ["B", "KB", "MB", "GB"];
3777
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3778
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
3779
+ }
3780
+ function formatNumber(n) {
3781
+ if (n >= 1e6)
3782
+ return (n / 1e6).toFixed(1) + "M";
3783
+ if (n >= 1000)
3784
+ return (n / 1000).toFixed(1) + "K";
3785
+ return n.toString();
3786
+ }
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) => {
3788
+ const token = loadToken();
3789
+ if (!token) {
3790
+ console.log("Not logged in. Run: zerodeploy login");
3791
+ return;
3792
+ }
3793
+ const validPeriods = ["24h", "7d", "30d"];
3794
+ if (!validPeriods.includes(options.period)) {
3795
+ console.error(`Invalid period. Use: ${validPeriods.join(", ")}`);
3796
+ return;
3797
+ }
3798
+ try {
3799
+ const client = getClient(token);
3800
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].analytics.$get({
3801
+ param: { orgSlug: options.org, siteSlug: site },
3802
+ query: { period: options.period }
3803
+ });
3804
+ if (!res.ok) {
3805
+ const error = await res.json();
3806
+ throw new Error(error.error || `API Error ${res.status}`);
3807
+ }
3808
+ const data = await res.json();
3809
+ if (!data.configured) {
3810
+ console.log("Analytics not configured for this site.");
3811
+ return;
3812
+ }
3813
+ const { overview, topPages, countries, statusCodes } = data;
3814
+ const periodLabel = options.period === "24h" ? "Last 24 hours" : options.period === "7d" ? "Last 7 days" : "Last 30 days";
3815
+ console.log(`Analytics for ${options.org}/${site}`);
3816
+ console.log(`Period: ${periodLabel}`);
3817
+ console.log();
3818
+ console.log("Overview");
3819
+ console.log(" " + "-".repeat(40));
3820
+ console.log(` Requests: ${formatNumber(overview.totalRequests).padStart(10)}`);
3821
+ console.log(` Bandwidth: ${formatBytes(overview.totalBytes).padStart(10)}`);
3822
+ console.log(` Unique visitors: ${formatNumber(overview.uniqueVisitors).padStart(10)}`);
3823
+ console.log(` Unique pages: ${formatNumber(overview.uniquePaths).padStart(10)}`);
3824
+ if (overview.formSubmissions > 0) {
3825
+ console.log(` Form submissions:${formatNumber(overview.formSubmissions).padStart(10)}`);
3826
+ }
3827
+ if (overview.bounceRate > 0) {
3828
+ console.log(` Bounce rate: ${overview.bounceRate.toFixed(1).padStart(9)}%`);
3829
+ }
3830
+ console.log();
3831
+ if (topPages.length > 0) {
3832
+ console.log("Top Pages");
3833
+ console.log(" " + "-".repeat(50));
3834
+ const maxPages = 5;
3835
+ for (const page of topPages.slice(0, maxPages)) {
3836
+ const path3 = page.path.length > 30 ? page.path.slice(0, 27) + "..." : page.path;
3837
+ console.log(` ${path3.padEnd(32)} ${formatNumber(page.requests).padStart(8)} reqs`);
3838
+ }
3839
+ console.log();
3840
+ }
3841
+ if (countries.length > 0) {
3842
+ console.log("Top Countries");
3843
+ console.log(" " + "-".repeat(40));
3844
+ const maxCountries = 5;
3845
+ for (const c of countries.slice(0, maxCountries)) {
3846
+ console.log(` ${c.country.padEnd(20)} ${formatNumber(c.requests).padStart(8)} reqs`);
3847
+ }
3848
+ console.log();
3849
+ }
3850
+ if (statusCodes.length > 0) {
3851
+ const grouped = { success: 0, redirect: 0, clientError: 0, serverError: 0 };
3852
+ for (const s of statusCodes) {
3853
+ if (s.statusCode >= 200 && s.statusCode < 300)
3854
+ grouped.success += s.requests;
3855
+ else if (s.statusCode >= 300 && s.statusCode < 400)
3856
+ grouped.redirect += s.requests;
3857
+ else if (s.statusCode >= 400 && s.statusCode < 500)
3858
+ grouped.clientError += s.requests;
3859
+ else if (s.statusCode >= 500)
3860
+ grouped.serverError += s.requests;
3861
+ }
3862
+ console.log("Response Codes");
3863
+ console.log(" " + "-".repeat(40));
3864
+ if (grouped.success > 0)
3865
+ console.log(` 2xx (success): ${formatNumber(grouped.success).padStart(10)}`);
3866
+ if (grouped.redirect > 0)
3867
+ console.log(` 3xx (redirect): ${formatNumber(grouped.redirect).padStart(10)}`);
3868
+ if (grouped.clientError > 0)
3869
+ console.log(` 4xx (client err):${formatNumber(grouped.clientError).padStart(10)}`);
3870
+ if (grouped.serverError > 0)
3871
+ console.log(` 5xx (server err):${formatNumber(grouped.serverError).padStart(10)}`);
3872
+ console.log();
3873
+ }
3874
+ console.log("View detailed analytics in the dashboard.");
3875
+ } catch (err) {
3876
+ const message = err instanceof Error ? err.message : "Unknown error";
3877
+ console.error("Failed to get analytics:", message);
3878
+ }
3879
+ });
3880
+
3417
3881
  // src/commands/site/index.ts
3418
- var siteCommand = new Command2("site").description("Manage sites").addCommand(siteListCommand).addCommand(siteCreateCommand).addCommand(siteDeleteCommand).addCommand(siteLinkCommand).addCommand(siteUnlinkCommand).addCommand(siteSubdomainCommand);
3882
+ var siteCommand = new Command2("site").description("Manage sites").addCommand(siteListCommand).addCommand(siteCreateCommand).addCommand(siteDeleteCommand).addCommand(siteRenameCommand).addCommand(siteLinkCommand).addCommand(siteUnlinkCommand).addCommand(siteSubdomainCommand).addCommand(siteStatsCommand);
3419
3883
 
3420
3884
  // src/commands/domain/add.ts
3421
3885
  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) => {
@@ -3425,13 +3889,10 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3425
3889
  return;
3426
3890
  }
3427
3891
  try {
3428
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3429
- method: "POST",
3430
- headers: {
3431
- Authorization: `Bearer ${token}`,
3432
- "Content-Type": "application/json"
3433
- },
3434
- body: JSON.stringify({ domain })
3892
+ const client = getClient(token);
3893
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$post({
3894
+ param: { orgSlug: options.org, siteSlug: options.site },
3895
+ json: { domain }
3435
3896
  });
3436
3897
  if (!res.ok) {
3437
3898
  const error = await res.json();
@@ -3491,16 +3952,15 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3491
3952
  return;
3492
3953
  }
3493
3954
  try {
3494
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3495
- headers: {
3496
- Authorization: `Bearer ${token}`
3497
- }
3955
+ const client = getClient(token);
3956
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
3957
+ param: { orgSlug: options.org, siteSlug: options.site }
3498
3958
  });
3499
3959
  if (!res.ok) {
3500
3960
  const error = await res.json();
3501
3961
  throw new Error(error.error || `API Error ${res.status}`);
3502
3962
  }
3503
- const domains = await res.json();
3963
+ const { data: domains } = await res.json();
3504
3964
  if (domains.length === 0) {
3505
3965
  console.log("No custom domains configured.");
3506
3966
  console.log();
@@ -3530,16 +3990,15 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3530
3990
  return;
3531
3991
  }
3532
3992
  try {
3533
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3534
- headers: {
3535
- Authorization: `Bearer ${token}`
3536
- }
3993
+ const client = getClient(token);
3994
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
3995
+ param: { orgSlug: options.org, siteSlug: options.site }
3537
3996
  });
3538
3997
  if (!listRes.ok) {
3539
3998
  const error = await listRes.json();
3540
3999
  throw new Error(error.error || `API Error ${listRes.status}`);
3541
4000
  }
3542
- const domains = await listRes.json();
4001
+ const { data: domains } = await listRes.json();
3543
4002
  const domain = domains.find((d) => d.domain === domainName);
3544
4003
  if (!domain) {
3545
4004
  console.error(`
@@ -3549,12 +4008,8 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3549
4008
  console.log(` zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`);
3550
4009
  return;
3551
4010
  }
3552
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}/verify`, {
3553
- method: "POST",
3554
- headers: {
3555
- Authorization: `Bearer ${token}`,
3556
- "Content-Type": "application/json"
3557
- }
4011
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].verify.$post({
4012
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3558
4013
  });
3559
4014
  const data = await res.json();
3560
4015
  if (!res.ok) {
@@ -3625,16 +4080,15 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
3625
4080
  return;
3626
4081
  }
3627
4082
  try {
3628
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3629
- headers: {
3630
- Authorization: `Bearer ${token}`
3631
- }
4083
+ const client = getClient(token);
4084
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4085
+ param: { orgSlug: options.org, siteSlug: options.site }
3632
4086
  });
3633
4087
  if (!listRes.ok) {
3634
4088
  const error = await listRes.json();
3635
4089
  throw new Error(error.error || `API Error ${listRes.status}`);
3636
4090
  }
3637
- const domains = await listRes.json();
4091
+ const { data: domains } = await listRes.json();
3638
4092
  const domain = domains.find((d) => d.domain === domainName);
3639
4093
  if (!domain) {
3640
4094
  console.error(`
@@ -3644,11 +4098,8 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
3644
4098
  console.log(` zerodeploy domain list --org ${options.org} --site ${options.site}`);
3645
4099
  return;
3646
4100
  }
3647
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}`, {
3648
- method: "DELETE",
3649
- headers: {
3650
- Authorization: `Bearer ${token}`
3651
- }
4101
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].$delete({
4102
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3652
4103
  });
3653
4104
  if (!res.ok) {
3654
4105
  const error = await res.json();
@@ -3681,58 +4132,285 @@ var domainRedirectCommand = new Command2("redirect").description("Set redirect m
3681
4132
  console.log("Not logged in. Run: zerodeploy login");
3682
4133
  return;
3683
4134
  }
3684
- const validModes = ["none", "www_to_apex", "apex_to_www"];
3685
- if (!validModes.includes(options.mode)) {
3686
- console.error(`Invalid mode: ${options.mode}`);
3687
- console.error("Valid modes: none, www_to_apex, apex_to_www");
4135
+ const validModes = ["none", "www_to_apex", "apex_to_www"];
4136
+ 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;
4140
+ }
4141
+ try {
4142
+ const client = getClient(token);
4143
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4144
+ param: { orgSlug: options.org, siteSlug: options.site }
4145
+ });
4146
+ if (!listRes.ok) {
4147
+ const error = await listRes.json();
4148
+ throw new Error(error.error || `API Error ${listRes.status}`);
4149
+ }
4150
+ const { data: domains } = await listRes.json();
4151
+ const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
4152
+ 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;
4156
+ }
4157
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].redirect.$patch({
4158
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: targetDomain.id },
4159
+ json: { redirectMode: options.mode }
4160
+ });
4161
+ if (!res.ok) {
4162
+ const error = await res.json();
4163
+ throw new Error(error.error || `API Error ${res.status}`);
4164
+ }
4165
+ const data = await res.json();
4166
+ console.log(`
4167
+ ✅ Redirect mode updated for ${data.domain}`);
4168
+ console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
4169
+ console.log();
4170
+ } catch (err) {
4171
+ const message = err instanceof Error ? err.message : "Unknown error";
4172
+ console.error("Failed to update redirect mode:", message);
4173
+ }
4174
+ });
4175
+
4176
+ // src/commands/domain/index.ts
4177
+ var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
4178
+
4179
+ // 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) => {
4181
+ const token = loadToken();
4182
+ if (!token) {
4183
+ console.log("Not logged in. Run: zerodeploy login");
4184
+ return;
4185
+ }
4186
+ try {
4187
+ const client = getClient(token);
4188
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms.$get({
4189
+ param: { orgSlug: options.org, siteSlug: options.site }
4190
+ });
4191
+ if (!res.ok) {
4192
+ const error = await res.json();
4193
+ throw new Error(error.error || `API Error ${res.status}`);
4194
+ }
4195
+ const { data: forms } = await res.json();
4196
+ if (forms.length === 0) {
4197
+ console.log("No forms found.");
4198
+ console.log();
4199
+ console.log("Forms are created automatically when visitors submit to /_forms/<name>");
4200
+ return;
4201
+ }
4202
+ console.log("Forms:");
4203
+ console.log();
4204
+ console.log(" NAME SUBMISSIONS CREATED");
4205
+ console.log(" " + "-".repeat(60));
4206
+ for (const f of forms) {
4207
+ const created = new Date(f.created_at).toLocaleDateString();
4208
+ console.log(` ${f.name.padEnd(30)} ${String(f.submission_count).padStart(11)} ${created}`);
4209
+ }
4210
+ console.log();
4211
+ console.log("Export submissions with:");
4212
+ console.log(` zerodeploy form export <name> --org ${options.org} --site ${options.site}`);
4213
+ } catch (err) {
4214
+ const message = err instanceof Error ? err.message : "Unknown error";
4215
+ console.error("Failed to list forms:", message);
4216
+ }
4217
+ });
4218
+
4219
+ // 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) => {
4221
+ const token = loadToken();
4222
+ if (!token) {
4223
+ console.log("Not logged in. Run: zerodeploy login");
4224
+ return;
4225
+ }
4226
+ try {
4227
+ const client = getClient(token);
4228
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].submissions.$get({
4229
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name },
4230
+ query: { limit: options.limit, offset: options.offset }
4231
+ });
4232
+ if (!res.ok) {
4233
+ const error = await res.json();
4234
+ throw new Error(error.error || `API Error ${res.status}`);
4235
+ }
4236
+ const data = await res.json();
4237
+ const { form, submissions, total, limit, offset } = data;
4238
+ if (submissions.length === 0) {
4239
+ console.log(`No submissions found for form "${name}".`);
4240
+ return;
4241
+ }
4242
+ console.log(`Form: ${form.name}`);
4243
+ console.log(`Total submissions: ${total}`);
4244
+ if (form.notification_email) {
4245
+ console.log(`Notifications: ${form.notification_email}`);
4246
+ }
4247
+ console.log();
4248
+ if (total > limit) {
4249
+ const start = offset + 1;
4250
+ const end = Math.min(offset + submissions.length, total);
4251
+ console.log(`Showing ${start}-${end} of ${total}`);
4252
+ console.log();
4253
+ }
4254
+ for (const sub of submissions) {
4255
+ const date = new Date(sub.created_at).toLocaleString();
4256
+ const shortId = sub.id.slice(0, 8);
4257
+ console.log(` ${shortId} ${date}`);
4258
+ const entries = Object.entries(sub.data);
4259
+ for (const [key, value] of entries) {
4260
+ const displayValue = typeof value === "string" ? value : JSON.stringify(value);
4261
+ const truncated = displayValue.length > 60 ? displayValue.slice(0, 57) + "..." : displayValue;
4262
+ console.log(` ${key}: ${truncated}`);
4263
+ }
4264
+ const meta = [];
4265
+ if (sub.ip_address)
4266
+ meta.push(`IP: ${sub.ip_address}`);
4267
+ if (sub.referrer)
4268
+ meta.push(`from: ${sub.referrer}`);
4269
+ if (meta.length > 0) {
4270
+ console.log(` [${meta.join(", ")}]`);
4271
+ }
4272
+ console.log();
4273
+ }
4274
+ if (total > offset + submissions.length) {
4275
+ const nextOffset = offset + parseInt(options.limit);
4276
+ console.log(`View more with: --offset ${nextOffset}`);
4277
+ }
4278
+ console.log("Export all to CSV with:");
4279
+ console.log(` zerodeploy form export ${name} --org ${options.org} --site ${options.site}`);
4280
+ } catch (err) {
4281
+ const message = err instanceof Error ? err.message : "Unknown error";
4282
+ console.error("Failed to get submissions:", message);
4283
+ }
4284
+ });
4285
+
4286
+ // src/commands/form/export.ts
4287
+ import { writeFile } from "fs/promises";
4288
+ import { resolve } from "path";
4289
+ 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
+ const token = loadToken();
4291
+ if (!token) {
4292
+ console.log("Not logged in. Run: zerodeploy login");
4293
+ return;
4294
+ }
4295
+ try {
4296
+ const client = getClient(token);
4297
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].export.$get({
4298
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name }
4299
+ });
4300
+ if (res.status === 204) {
4301
+ console.log("No submissions to export.");
4302
+ return;
4303
+ }
4304
+ if (!res.ok) {
4305
+ const error = await res.json();
4306
+ throw new Error(error.error || `API Error ${res.status}`);
4307
+ }
4308
+ const csv = await res.text();
4309
+ const outputPath = options.output || `${name}-submissions.csv`;
4310
+ const fullPath = resolve(process.cwd(), outputPath);
4311
+ await writeFile(fullPath, csv, "utf-8");
4312
+ const lineCount = csv.split(`
4313
+ `).length - 1;
4314
+ console.log(`Exported ${lineCount} submissions to ${outputPath}`);
4315
+ } catch (err) {
4316
+ const message = err instanceof Error ? err.message : "Unknown error";
4317
+ console.error("Failed to export form:", message);
4318
+ }
4319
+ });
4320
+
4321
+ // src/commands/form/delete.ts
4322
+ import * as readline3 from "readline";
4323
+ function prompt3(question) {
4324
+ const rl = readline3.createInterface({
4325
+ input: process.stdin,
4326
+ output: process.stdout
4327
+ });
4328
+ return new Promise((resolve2) => {
4329
+ rl.question(question, (answer) => {
4330
+ rl.close();
4331
+ resolve2(answer);
4332
+ });
4333
+ });
4334
+ }
4335
+ 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
+ const token = loadToken();
4337
+ if (!token) {
4338
+ console.log("Not logged in. Run: zerodeploy login");
4339
+ return;
4340
+ }
4341
+ if (!options.force) {
4342
+ const answer = await prompt3(`Are you sure you want to delete form "${name}" and all its submissions? This cannot be undone. (y/N) `);
4343
+ if (answer.toLowerCase() !== "y") {
4344
+ console.log("Cancelled.");
4345
+ return;
4346
+ }
4347
+ }
4348
+ try {
4349
+ const client = getClient(token);
4350
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$delete({
4351
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name }
4352
+ });
4353
+ if (!res.ok) {
4354
+ const error = await res.json();
4355
+ throw new Error(error.error || `API Error ${res.status}`);
4356
+ }
4357
+ const result = await res.json();
4358
+ console.log(`✅ ${result.message}`);
4359
+ } catch (err) {
4360
+ const message = err instanceof Error ? err.message : "Unknown error";
4361
+ console.error("Failed to delete form:", message);
4362
+ }
4363
+ });
4364
+
4365
+ // src/commands/form/notify.ts
4366
+ 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
+ const token = loadToken();
4368
+ if (!token) {
4369
+ console.log("Not logged in. Run: zerodeploy login");
4370
+ return;
4371
+ }
4372
+ if (options.email && options.disable) {
4373
+ console.error("Cannot use both --email and --disable options");
4374
+ return;
4375
+ }
4376
+ 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`);
3688
4382
  return;
3689
4383
  }
3690
4384
  try {
3691
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3692
- headers: {
3693
- Authorization: `Bearer ${token}`
3694
- }
3695
- });
3696
- if (!listRes.ok) {
3697
- const error = await listRes.json();
3698
- throw new Error(error.error || `API Error ${listRes.status}`);
3699
- }
3700
- const domains = await listRes.json();
3701
- const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
3702
- if (!targetDomain) {
3703
- console.error(`Domain not found: ${domain}`);
3704
- console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
3705
- return;
3706
- }
3707
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${targetDomain.id}/redirect`, {
3708
- method: "PATCH",
3709
- headers: {
3710
- Authorization: `Bearer ${token}`,
3711
- "Content-Type": "application/json"
3712
- },
3713
- body: JSON.stringify({ redirectMode: options.mode })
4385
+ const client = getClient(token);
4386
+ const notificationEmail = options.disable ? null : options.email;
4387
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$patch({
4388
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name },
4389
+ json: { notification_email: notificationEmail }
3714
4390
  });
3715
4391
  if (!res.ok) {
3716
4392
  const error = await res.json();
3717
4393
  throw new Error(error.error || `API Error ${res.status}`);
3718
4394
  }
3719
- const data = await res.json();
3720
- console.log(`
3721
- Redirect mode updated for ${data.domain}`);
3722
- console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
3723
- console.log();
4395
+ const { form } = await res.json();
4396
+ if (form.notification_email) {
4397
+ console.log(`Email notifications enabled for form "${name}"`);
4398
+ console.log(` Notifications will be sent to: ${form.notification_email}`);
4399
+ } else {
4400
+ console.log(`Email notifications disabled for form "${name}"`);
4401
+ }
3724
4402
  } catch (err) {
3725
4403
  const message = err instanceof Error ? err.message : "Unknown error";
3726
- console.error("Failed to update redirect mode:", message);
4404
+ console.error("Failed to update form notifications:", message);
3727
4405
  }
3728
4406
  });
3729
4407
 
3730
- // src/commands/domain/index.ts
3731
- var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
4408
+ // src/commands/form/index.ts
4409
+ var formCommand = new Command2("form").description("Manage form submissions").addCommand(formListCommand).addCommand(formSubmissionsCommand).addCommand(formExportCommand).addCommand(formDeleteCommand).addCommand(formNotifyCommand);
3732
4410
 
3733
4411
  // src/commands/deploy/index.ts
3734
- import { resolve as resolve3, basename } from "node:path";
3735
- import { stat as stat2, writeFile } from "node:fs/promises";
4412
+ import { resolve as resolve4, basename } from "node:path";
4413
+ import { stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
3736
4414
  import { spawn } from "node:child_process";
3737
4415
 
3738
4416
  // src/utils/files.ts
@@ -3772,7 +4450,7 @@ async function scanDirectory(rootDir) {
3772
4450
  await scan(rootDir);
3773
4451
  return files;
3774
4452
  }
3775
- function formatBytes(bytes) {
4453
+ function formatBytes2(bytes) {
3776
4454
  if (bytes === 0)
3777
4455
  return "0 B";
3778
4456
  const k = 1024;
@@ -3783,7 +4461,7 @@ function formatBytes(bytes) {
3783
4461
 
3784
4462
  // src/utils/framework.ts
3785
4463
  import { readFile as readFile2 } from "node:fs/promises";
3786
- import { resolve } from "node:path";
4464
+ import { resolve as resolve2 } from "node:path";
3787
4465
  var FRAMEWORKS = [
3788
4466
  {
3789
4467
  name: "Vite",
@@ -3857,7 +4535,7 @@ function hasDep(pkg, dep) {
3857
4535
  }
3858
4536
  async function detectFramework(cwd) {
3859
4537
  try {
3860
- const pkgPath = resolve(cwd, "package.json");
4538
+ const pkgPath = resolve2(cwd, "package.json");
3861
4539
  const pkgContent = await readFile2(pkgPath, "utf-8");
3862
4540
  const pkg = JSON.parse(pkgContent);
3863
4541
  for (const framework of FRAMEWORKS) {
@@ -4400,9 +5078,12 @@ function padToBlock(data) {
4400
5078
  padded.set(data);
4401
5079
  return padded;
4402
5080
  }
4403
- async function createTarArchive(files) {
5081
+ async function createTarArchive(files, onProgress) {
4404
5082
  const parts = [];
4405
- for (const file of files) {
5083
+ const total = files.length;
5084
+ for (let i2 = 0;i2 < files.length; i2++) {
5085
+ const file = files[i2];
5086
+ onProgress?.(i2 + 1, total, file.path);
4406
5087
  const content = await readFile3(file.absolutePath);
4407
5088
  const header = createTarHeader(file.path, content.length);
4408
5089
  const paddedContent = padToBlock(new Uint8Array(content));
@@ -4419,8 +5100,8 @@ async function createTarArchive(files) {
4419
5100
  }
4420
5101
  return tar;
4421
5102
  }
4422
- async function createTarGz(files) {
4423
- const tar = await createTarArchive(files);
5103
+ async function createTarGz(files, onProgress) {
5104
+ const tar = await createTarArchive(files, onProgress);
4424
5105
  return gzipSync(tar, { level: 6 });
4425
5106
  }
4426
5107
  function formatCompression(original, compressed) {
@@ -4430,10 +5111,10 @@ function formatCompression(original, compressed) {
4430
5111
 
4431
5112
  // src/utils/project-config.ts
4432
5113
  import { existsSync, readFileSync } from "node:fs";
4433
- import { resolve as resolve2 } from "node:path";
5114
+ import { resolve as resolve3 } from "node:path";
4434
5115
  var CONFIG_FILENAME = "zerodeploy.json";
4435
5116
  function loadProjectConfig(cwd = process.cwd()) {
4436
- const configPath = resolve2(cwd, CONFIG_FILENAME);
5117
+ const configPath = resolve3(cwd, CONFIG_FILENAME);
4437
5118
  if (!existsSync(configPath)) {
4438
5119
  return {};
4439
5120
  }
@@ -4447,32 +5128,139 @@ function loadProjectConfig(cwd = process.cwd()) {
4447
5128
  }
4448
5129
  }
4449
5130
  function getConfigPath(cwd = process.cwd()) {
4450
- return resolve2(cwd, CONFIG_FILENAME);
5131
+ return resolve3(cwd, CONFIG_FILENAME);
4451
5132
  }
4452
5133
 
4453
5134
  // src/utils/prompt.ts
4454
- import * as readline3 from "node:readline";
5135
+ import * as readline4 from "node:readline";
4455
5136
  async function confirm(message, defaultValue = true) {
4456
- const rl = readline3.createInterface({
5137
+ const rl = readline4.createInterface({
4457
5138
  input: process.stdin,
4458
5139
  output: process.stdout
4459
5140
  });
4460
5141
  const hint = defaultValue ? "[Y/n]" : "[y/N]";
4461
- return new Promise((resolve3) => {
5142
+ return new Promise((resolve4) => {
4462
5143
  rl.question(`${message} ${hint} `, (answer) => {
4463
5144
  rl.close();
4464
5145
  const normalized = answer.trim().toLowerCase();
4465
5146
  if (normalized === "") {
4466
- resolve3(defaultValue);
5147
+ resolve4(defaultValue);
4467
5148
  } else if (normalized === "y" || normalized === "yes") {
4468
- resolve3(true);
5149
+ resolve4(true);
4469
5150
  } else {
4470
- resolve3(false);
5151
+ resolve4(false);
4471
5152
  }
4472
5153
  });
4473
5154
  });
4474
5155
  }
4475
5156
 
5157
+ // src/utils/progress.ts
5158
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5159
+ var SPINNER_INTERVAL = 80;
5160
+ var isTTY = process.stdout.isTTY;
5161
+ function clearLine() {
5162
+ if (isTTY) {
5163
+ process.stdout.write("\r\x1B[K");
5164
+ }
5165
+ }
5166
+ function createSpinner(message) {
5167
+ let frameIndex = 0;
5168
+ let interval = null;
5169
+ let currentMessage = message;
5170
+ function render() {
5171
+ if (!isTTY)
5172
+ return;
5173
+ const frame = SPINNER_FRAMES[frameIndex];
5174
+ clearLine();
5175
+ process.stdout.write(`${frame} ${currentMessage}`);
5176
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
5177
+ }
5178
+ return {
5179
+ start() {
5180
+ if (!isTTY) {
5181
+ console.log(currentMessage);
5182
+ return;
5183
+ }
5184
+ render();
5185
+ interval = setInterval(render, SPINNER_INTERVAL);
5186
+ },
5187
+ stop(finalMessage) {
5188
+ if (interval) {
5189
+ clearInterval(interval);
5190
+ interval = null;
5191
+ }
5192
+ clearLine();
5193
+ if (finalMessage) {
5194
+ console.log(finalMessage);
5195
+ }
5196
+ },
5197
+ update(message2) {
5198
+ currentMessage = message2;
5199
+ if (!isTTY) {
5200
+ console.log(message2);
5201
+ }
5202
+ },
5203
+ succeed(message2) {
5204
+ if (interval) {
5205
+ clearInterval(interval);
5206
+ interval = null;
5207
+ }
5208
+ clearLine();
5209
+ console.log(`✓ ${message2}`);
5210
+ },
5211
+ fail(message2) {
5212
+ if (interval) {
5213
+ clearInterval(interval);
5214
+ interval = null;
5215
+ }
5216
+ clearLine();
5217
+ console.log(`✗ ${message2}`);
5218
+ },
5219
+ warn(message2) {
5220
+ if (interval) {
5221
+ clearInterval(interval);
5222
+ interval = null;
5223
+ }
5224
+ clearLine();
5225
+ console.log(`⚠ ${message2}`);
5226
+ }
5227
+ };
5228
+ }
5229
+ function createProgressBar(options) {
5230
+ const { total, label, width = 20 } = options;
5231
+ let started = false;
5232
+ function render(current, suffix) {
5233
+ const percent = Math.min(current / total, 1);
5234
+ const filled = Math.round(percent * width);
5235
+ const empty = width - filled;
5236
+ const bar = "█".repeat(filled) + "░".repeat(empty);
5237
+ const countStr = `${current}/${total}`;
5238
+ const suffixStr = suffix ? ` ${suffix}` : "";
5239
+ if (isTTY) {
5240
+ clearLine();
5241
+ process.stdout.write(`${label} [${bar}] ${countStr}${suffixStr}`);
5242
+ }
5243
+ }
5244
+ return {
5245
+ update(current, suffix) {
5246
+ if (!isTTY) {
5247
+ if (!started) {
5248
+ console.log(`${label}... ${total} files`);
5249
+ started = true;
5250
+ }
5251
+ return;
5252
+ }
5253
+ render(current, suffix);
5254
+ },
5255
+ done(message) {
5256
+ clearLine();
5257
+ if (message) {
5258
+ console.log(message);
5259
+ }
5260
+ }
5261
+ };
5262
+ }
5263
+
4476
5264
  // src/commands/deploy/list.ts
4477
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) => {
4478
5266
  const token = loadToken();
@@ -4490,7 +5278,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4490
5278
  console.error(`❌ ${error.error}`);
4491
5279
  return;
4492
5280
  }
4493
- const deployments = await res.json();
5281
+ const { data: deployments } = await res.json();
4494
5282
  const limit = parseInt(options.limit, 10);
4495
5283
  const shown = deployments.slice(0, limit);
4496
5284
  if (shown.length === 0) {
@@ -4501,7 +5289,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4501
5289
  Deployments for ${options.org}/${siteSlug}:
4502
5290
  `);
4503
5291
  for (const d of shown) {
4504
- const current = d.isCurrent ? " ← current" : "";
5292
+ const current = d.is_current ? " ← current" : "";
4505
5293
  const status = formatStatus2(d.status);
4506
5294
  const date = new Date(d.created_at).toLocaleString();
4507
5295
  console.log(` ${d.id.slice(0, 8)} ${status} ${date}${current}`);
@@ -4583,21 +5371,82 @@ var deployPromoteCommand = new Command2("promote").description("Promote a previe
4583
5371
  function slugify(input) {
4584
5372
  return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
4585
5373
  }
5374
+ async function verifyDeployment(url, maxRetries = 3, delayMs = 2000) {
5375
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
5376
+ try {
5377
+ const controller = new AbortController;
5378
+ const timeout = setTimeout(() => controller.abort(), 1e4);
5379
+ const res = await fetch(url, {
5380
+ method: "GET",
5381
+ signal: controller.signal
5382
+ });
5383
+ clearTimeout(timeout);
5384
+ if (res.ok) {
5385
+ return { success: true, status: res.status };
5386
+ }
5387
+ if (attempt < maxRetries) {
5388
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
5389
+ continue;
5390
+ }
5391
+ return { success: false, status: res.status };
5392
+ } catch (err) {
5393
+ if (attempt < maxRetries) {
5394
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
5395
+ continue;
5396
+ }
5397
+ const error = err instanceof Error ? err.message : "Unknown error";
5398
+ return { success: false, error };
5399
+ }
5400
+ }
5401
+ return { success: false, error: "Max retries exceeded" };
5402
+ }
5403
+ async function autoRollback(token, orgSlug, siteSlug, currentDeploymentId) {
5404
+ try {
5405
+ const client = getClient(token);
5406
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5407
+ param: { orgSlug, siteSlug }
5408
+ });
5409
+ if (!listRes.ok) {
5410
+ return { success: false, error: "Failed to fetch deployments" };
5411
+ }
5412
+ const { data: deployments } = await listRes.json();
5413
+ const previousDeployment = deployments.find((d) => d.id !== currentDeploymentId && d.status === "ready");
5414
+ if (!previousDeployment) {
5415
+ return { success: false, error: "No previous deployment to rollback to" };
5416
+ }
5417
+ const rollbackRes = await client.deployments[":id"].rollback.$post({
5418
+ param: { id: previousDeployment.id }
5419
+ });
5420
+ if (!rollbackRes.ok) {
5421
+ return { success: false, error: "Rollback request failed" };
5422
+ }
5423
+ return { success: true, deploymentId: previousDeployment.id };
5424
+ } catch (err) {
5425
+ const error = err instanceof Error ? err.message : "Unknown error";
5426
+ return { success: false, error };
5427
+ }
5428
+ }
4586
5429
  async function runCommand(command, cwd) {
4587
5430
  return new Promise((promiseResolve) => {
4588
- const [cmd, ...args] = command.split(" ");
5431
+ const parts = command.split(" ");
5432
+ const cmd = parts[0];
5433
+ if (!cmd) {
5434
+ promiseResolve({ success: false, output: "Empty command" });
5435
+ return;
5436
+ }
5437
+ const args = parts.slice(1);
4589
5438
  const proc = spawn(cmd, args, {
4590
5439
  cwd,
4591
5440
  shell: true,
4592
5441
  stdio: ["inherit", "pipe", "pipe"]
4593
5442
  });
4594
5443
  let output = "";
4595
- proc.stdout?.on("data", (data) => {
5444
+ proc.stdout.on("data", (data) => {
4596
5445
  const text = data.toString();
4597
5446
  output += text;
4598
5447
  process.stdout.write(text);
4599
5448
  });
4600
- proc.stderr?.on("data", (data) => {
5449
+ proc.stderr.on("data", (data) => {
4601
5450
  const text = data.toString();
4602
5451
  output += text;
4603
5452
  process.stderr.write(text);
@@ -4613,12 +5462,12 @@ async function runCommand(command, cwd) {
4613
5462
  async function findBuildDirectory(cwd) {
4614
5463
  const candidates = ["dist", "build", "out", "public", "."];
4615
5464
  for (const dir of candidates) {
4616
- const fullPath = resolve3(cwd, dir);
5465
+ const fullPath = resolve4(cwd, dir);
4617
5466
  try {
4618
5467
  const stats = await stat2(fullPath);
4619
5468
  if (stats.isDirectory()) {
4620
5469
  try {
4621
- await stat2(resolve3(fullPath, "index.html"));
5470
+ await stat2(resolve4(fullPath, "index.html"));
4622
5471
  return fullPath;
4623
5472
  } catch {
4624
5473
  if (dir !== ".")
@@ -4630,22 +5479,25 @@ async function findBuildDirectory(cwd) {
4630
5479
  }
4631
5480
  return null;
4632
5481
  }
4633
- async function uploadArchive(token, uploadUrl, archive) {
4634
- const res = await fetch(uploadUrl, {
5482
+ async function uploadArchive(token, uploadUrl, archive, onRetry) {
5483
+ const res = await fetchWithRetry(uploadUrl, {
4635
5484
  method: "POST",
4636
5485
  headers: {
4637
5486
  "Content-Type": "application/gzip",
4638
5487
  Authorization: `Bearer ${token}`
4639
5488
  },
4640
5489
  body: archive
5490
+ }, {
5491
+ maxRetries: 3,
5492
+ onRetry
4641
5493
  });
4642
5494
  return res.ok;
4643
5495
  }
4644
- var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
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) => {
4645
5497
  const cwd = process.cwd();
4646
5498
  const token = loadToken();
4647
5499
  if (!token) {
4648
- console.log("Not logged in. Run: zerodeploy login");
5500
+ displayAuthError();
4649
5501
  return;
4650
5502
  }
4651
5503
  const config = loadProjectConfig(cwd);
@@ -4692,7 +5544,7 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
4692
5544
  console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
4693
5545
  const configPath = getConfigPath(cwd);
4694
5546
  const newConfig = { org: orgSlug, site: siteSlug, dir: dirOption || config.dir };
4695
- await writeFile(configPath, JSON.stringify(newConfig, null, 2) + `
5547
+ await writeFile2(configPath, JSON.stringify(newConfig, null, 2) + `
4696
5548
  `);
4697
5549
  console.log(`Saved config to zerodeploy.json`);
4698
5550
  console.log("");
@@ -4703,6 +5555,50 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
4703
5555
  console.log(`Detected: ${framework.name}`);
4704
5556
  }
4705
5557
  const shouldBuild = options.build === true;
5558
+ if (shouldBuild || options.install) {
5559
+ const spinner = createSpinner("Checking deployment limits...");
5560
+ spinner.start();
5561
+ try {
5562
+ const client = getClient(token);
5563
+ const sitesRes = await client.orgs[":orgSlug"].sites.$get({
5564
+ param: { orgSlug }
5565
+ });
5566
+ if (!sitesRes.ok) {
5567
+ spinner.fail("Pre-flight check failed");
5568
+ const body = await sitesRes.json();
5569
+ displayError(parseApiError(body));
5570
+ return;
5571
+ }
5572
+ const { data: sites } = await sitesRes.json();
5573
+ const siteExists = sites.some((s) => s.slug === siteSlug);
5574
+ if (!siteExists) {
5575
+ spinner.fail("Pre-flight check failed");
5576
+ displayError({ code: "site_not_found", message: `Site "${siteSlug}" not found in organization "${orgSlug}"` });
5577
+ return;
5578
+ }
5579
+ const usageRes = await client.auth.me.usage.$get();
5580
+ if (usageRes.ok) {
5581
+ const usage = await usageRes.json();
5582
+ const org = usage.orgs.find((o) => o.slug === orgSlug);
5583
+ if (org) {
5584
+ const monthlyUsage = org.deployments_this_month;
5585
+ const monthlyLimit = usage.limits.max_deployments_per_month;
5586
+ const percentUsed = Math.round(monthlyUsage / monthlyLimit * 100);
5587
+ if (percentUsed >= 90) {
5588
+ spinner.warn(`Approaching monthly deployment limit (${monthlyUsage}/${monthlyLimit})`);
5589
+ } else {
5590
+ spinner.succeed("Pre-flight check passed");
5591
+ }
5592
+ } else {
5593
+ spinner.succeed("Pre-flight check passed");
5594
+ }
5595
+ } else {
5596
+ spinner.succeed("Pre-flight check passed");
5597
+ }
5598
+ } catch (err) {
5599
+ spinner.warn("Could not verify limits (will check during deploy)");
5600
+ }
5601
+ }
4706
5602
  if (options.install) {
4707
5603
  const installCmd = config.install || framework?.installCommand || "npm install";
4708
5604
  console.log(`
@@ -4733,7 +5629,7 @@ Error: Build failed`);
4733
5629
  }
4734
5630
  let deployDir;
4735
5631
  if (dirOption) {
4736
- deployDir = resolve3(cwd, dirOption);
5632
+ deployDir = resolve4(cwd, dirOption);
4737
5633
  try {
4738
5634
  const stats = await stat2(deployDir);
4739
5635
  if (!stats.isDirectory()) {
@@ -4745,7 +5641,7 @@ Error: Build failed`);
4745
5641
  return;
4746
5642
  }
4747
5643
  } else {
4748
- const detected = framework ? resolve3(cwd, framework.outputDir) : await findBuildDirectory(cwd);
5644
+ const detected = framework ? resolve4(cwd, framework.outputDir) : await findBuildDirectory(cwd);
4749
5645
  if (!detected) {
4750
5646
  console.log("Error: Could not find build directory. Use --dir to specify.");
4751
5647
  return;
@@ -4770,7 +5666,7 @@ Error: Build failed`);
4770
5666
  return;
4771
5667
  }
4772
5668
  const totalSize = files.reduce((sum, f) => sum + f.size, 0);
4773
- console.log(`Found ${files.length} files (${formatBytes(totalSize)})`);
5669
+ console.log(`Found ${files.length} files (${formatBytes2(totalSize)})`);
4774
5670
  try {
4775
5671
  const client = getClient(token);
4776
5672
  const deployPayload = { siteSlug, orgSlug };
@@ -4794,68 +5690,104 @@ Error: Build failed`);
4794
5690
  });
4795
5691
  if (!createRes.ok) {
4796
5692
  const body = await createRes.json();
4797
- let message = "Failed to create deployment";
4798
- if (typeof body.error === "string") {
4799
- message = body.error;
4800
- } else if (typeof body.message === "string") {
4801
- message = body.message;
4802
- } else if (body.success === false && body.error && typeof body.error === "object") {
4803
- const zodError = body.error;
4804
- if (zodError.issues?.[0]) {
4805
- const issue = zodError.issues[0];
4806
- const field = issue.path?.join(".") || "";
4807
- message = field ? `${field}: ${issue.message}` : issue.message;
4808
- } else if (zodError.message) {
4809
- try {
4810
- const issues = JSON.parse(zodError.message);
4811
- if (issues[0]) {
4812
- const field = issues[0].path?.join(".") || "";
4813
- message = field ? `${field}: ${issues[0].message}` : issues[0].message;
4814
- }
4815
- } catch {
4816
- message = zodError.message;
4817
- }
4818
- }
4819
- }
4820
- console.log(`Error: ${message}`);
5693
+ displayError(parseApiError(body));
4821
5694
  return;
4822
5695
  }
4823
5696
  const deployment = await createRes.json();
4824
5697
  console.log(`Created deployment: ${deployment.id}`);
4825
- console.log("Creating archive...");
4826
- const archive = await createTarGz(files);
4827
- console.log(` ${formatBytes(totalSize)} -> ${formatBytes(archive.length)} (${formatCompression(totalSize, archive.length)})`);
4828
- console.log("Uploading...");
5698
+ const progressBar2 = createProgressBar({ total: files.length, label: "Archiving" });
5699
+ const archive = await createTarGz(files, (current, total) => {
5700
+ progressBar2.update(current);
5701
+ });
5702
+ progressBar2.done();
5703
+ console.log(` ${formatBytes2(totalSize)} -> ${formatBytes2(archive.length)} (${formatCompression(totalSize, archive.length)})`);
5704
+ const uploadSpinner = createSpinner(`Uploading (${formatBytes2(archive.length)})...`);
5705
+ uploadSpinner.start();
4829
5706
  const uploadUrl = deployment.uploadUrl || `${API_URL}/deployments/${deployment.id}/upload`;
4830
- const uploadSuccess = await uploadArchive(token, uploadUrl, archive);
5707
+ const uploadSuccess = await uploadArchive(token, uploadUrl, archive, (attempt, error, delayMs) => {
5708
+ uploadSpinner.update(`Uploading... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
5709
+ });
4831
5710
  if (!uploadSuccess) {
4832
- console.log("Error: Failed to upload archive");
5711
+ uploadSpinner.stop("Error: Failed to upload archive");
4833
5712
  return;
4834
5713
  }
4835
- console.log(` Uploaded ${formatBytes(archive.length)}`);
4836
- console.log("Finalizing...");
4837
- const finalizeRes = await fetch(`${API_URL}/deployments/${deployment.id}/finalize`, {
5714
+ uploadSpinner.stop(` Uploaded ${formatBytes2(archive.length)}`);
5715
+ const finalizeSpinner = createSpinner("Finalizing...");
5716
+ finalizeSpinner.start();
5717
+ const finalizeRes = await fetchWithRetry(`${API_URL}/deployments/${deployment.id}/finalize`, {
4838
5718
  method: "POST",
4839
5719
  headers: {
4840
5720
  "Content-Type": "application/json",
4841
5721
  Authorization: `Bearer ${token}`
4842
5722
  },
4843
5723
  body: JSON.stringify({ preview: options.preview || false })
5724
+ }, {
5725
+ maxRetries: 3,
5726
+ onRetry: (attempt, error, delayMs) => {
5727
+ finalizeSpinner.update(`Finalizing... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
5728
+ }
4844
5729
  });
4845
5730
  if (!finalizeRes.ok) {
4846
- console.log("Error: Failed to finalize deployment");
5731
+ finalizeSpinner.stop("Error: Failed to finalize deployment");
4847
5732
  return;
4848
5733
  }
5734
+ finalizeSpinner.stop();
5735
+ const verifyUrl = options.preview ? deployment.previewUrl : deployment.url;
5736
+ let verified = false;
5737
+ if (options.verify !== false) {
5738
+ const verifySpinner = createSpinner("Verifying...");
5739
+ verifySpinner.start();
5740
+ const verification = await verifyDeployment(verifyUrl);
5741
+ verified = verification.success;
5742
+ if (!verified) {
5743
+ verifySpinner.stop();
5744
+ console.log("");
5745
+ console.log("Warning: Could not verify deployment");
5746
+ if (verification.status) {
5747
+ console.log(` Received status ${verification.status}`);
5748
+ } else if (verification.error) {
5749
+ console.log(` ${verification.error}`);
5750
+ }
5751
+ if (options.autoRollback !== false && !options.preview) {
5752
+ console.log("");
5753
+ console.log("Auto-rolling back to previous deployment...");
5754
+ const rollback = await autoRollback(token, orgSlug, siteSlug, deployment.id);
5755
+ if (rollback.success) {
5756
+ console.log(`Rolled back to ${rollback.deploymentId?.slice(0, 8)}`);
5757
+ console.log("");
5758
+ console.log("Deployment failed verification and was rolled back.");
5759
+ console.log(`Failed deployment: ${deployment.id.slice(0, 8)}`);
5760
+ console.log(`Check manually: ${verifyUrl}`);
5761
+ return;
5762
+ } else {
5763
+ console.log(` Could not auto-rollback: ${rollback.error}`);
5764
+ console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
5765
+ }
5766
+ } else {
5767
+ console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
5768
+ }
5769
+ } else {
5770
+ verifySpinner.stop();
5771
+ }
5772
+ }
4849
5773
  console.log("");
4850
5774
  if (options.preview) {
4851
5775
  console.log("Preview deployment created!");
4852
- console.log(`Preview: ${deployment.previewUrl}`);
5776
+ if (verified) {
5777
+ console.log(`Preview: ${deployment.previewUrl} (verified)`);
5778
+ } else {
5779
+ console.log(`Preview: ${deployment.previewUrl}`);
5780
+ }
4853
5781
  console.log("");
4854
5782
  console.log(`To make this deployment live, run:`);
4855
5783
  console.log(` zerodeploy deploy promote ${deployment.id.slice(0, 8)}`);
4856
5784
  } else {
4857
5785
  console.log("Deployment successful!");
4858
- console.log(`URL: ${deployment.url}`);
5786
+ if (verified) {
5787
+ console.log(`URL: ${deployment.url} (verified)`);
5788
+ } else {
5789
+ console.log(`URL: ${deployment.url}`);
5790
+ }
4859
5791
  console.log(`Preview: ${deployment.previewUrl}`);
4860
5792
  }
4861
5793
  if (options.githubOutput) {
@@ -4876,8 +5808,14 @@ Error: Build failed`);
4876
5808
  }
4877
5809
  }
4878
5810
  } catch (err) {
4879
- const message = err instanceof Error ? err.message : "Unknown error";
4880
- console.log(`Deploy failed: ${message}`);
5811
+ if (err instanceof Error) {
5812
+ displayNetworkError(err);
5813
+ } else {
5814
+ displayError({
5815
+ code: "unknown_error",
5816
+ message: "Deploy failed: Unknown error"
5817
+ });
5818
+ }
4881
5819
  }
4882
5820
  });
4883
5821
 
@@ -4895,7 +5833,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4895
5833
  });
4896
5834
  if (!res.ok)
4897
5835
  throw new Error(`API Error ${res.status}`);
4898
- const deployments = await res.json();
5836
+ const { data: deployments } = await res.json();
4899
5837
  if (deployments.length === 0) {
4900
5838
  console.log("No deployments found.");
4901
5839
  return;
@@ -4903,7 +5841,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4903
5841
  console.log("Deployments:");
4904
5842
  console.log();
4905
5843
  for (const d of deployments) {
4906
- const current = d.isCurrent ? " (current)" : "";
5844
+ const current = d.is_current ? " (current)" : "";
4907
5845
  const date = new Date(d.created_at).toLocaleString();
4908
5846
  const shortId = d.id.slice(0, 8);
4909
5847
  const commit = d.commit_sha ? ` [${d.commit_sha.slice(0, 7)}]` : "";
@@ -4911,8 +5849,9 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4911
5849
  const message = d.commit_message ? ` - ${(d.commit_message.split(`
4912
5850
  `)[0] ?? "").slice(0, 50)}` : "";
4913
5851
  console.log(` ${shortId} ${d.status.padEnd(10)} ${date}${current}`);
5852
+ console.log(` ${d.preview_url}`);
4914
5853
  if (commit || message) {
4915
- console.log(` ${commit}${branch}${message}`);
5854
+ console.log(` ${commit}${branch}${message}`);
4916
5855
  }
4917
5856
  console.log();
4918
5857
  }
@@ -4922,8 +5861,109 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4922
5861
  }
4923
5862
  });
4924
5863
 
5864
+ // src/commands/deployments/show.ts
5865
+ function formatBytes3(bytes) {
5866
+ if (bytes === 0)
5867
+ return "0 B";
5868
+ const k = 1024;
5869
+ const sizes = ["B", "KB", "MB", "GB"];
5870
+ const i2 = Math.floor(Math.log(bytes) / Math.log(k));
5871
+ return parseFloat((bytes / Math.pow(k, i2)).toFixed(1)) + " " + sizes[i2];
5872
+ }
5873
+ function formatStatus3(status) {
5874
+ switch (status) {
5875
+ case "pending":
5876
+ return "⏳ Pending";
5877
+ case "uploading":
5878
+ return "\uD83D\uDCE4 Uploading";
5879
+ case "processing":
5880
+ return "⚙️ Processing";
5881
+ case "ready":
5882
+ return "✅ Ready";
5883
+ case "failed":
5884
+ return "❌ Failed";
5885
+ default:
5886
+ return status;
5887
+ }
5888
+ }
5889
+ var deploymentsShowCommand = new Command2("show").description("View deployment details").argument("<id>", "Deployment ID (full or short)").action(async (id) => {
5890
+ const token = loadToken();
5891
+ if (!token) {
5892
+ console.log("Not logged in. Run: zerodeploy login");
5893
+ return;
5894
+ }
5895
+ try {
5896
+ const client = getClient(token);
5897
+ const res = await client.deployments[":id"].$get({
5898
+ param: { id }
5899
+ });
5900
+ if (!res.ok) {
5901
+ 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;
5906
+ }
5907
+ const error = await res.json();
5908
+ throw new Error(error.error || `API Error ${res.status}`);
5909
+ }
5910
+ const d = await res.json();
5911
+ console.log("Deployment Details");
5912
+ console.log("=".repeat(50));
5913
+ console.log();
5914
+ console.log(`ID: ${d.id}`);
5915
+ console.log(`Status: ${formatStatus3(d.status)}`);
5916
+ console.log(`Created: ${new Date(d.created_at).toLocaleString()}`);
5917
+ console.log();
5918
+ console.log("URLs");
5919
+ console.log("-".repeat(50));
5920
+ console.log(`Production: ${d.url}`);
5921
+ console.log(`Preview: ${d.preview_url}`);
5922
+ console.log();
5923
+ if (d.commit_sha || d.branch || d.pr_number) {
5924
+ console.log("Git Info");
5925
+ console.log("-".repeat(50));
5926
+ if (d.branch) {
5927
+ console.log(`Branch: ${d.branch}`);
5928
+ }
5929
+ if (d.commit_sha) {
5930
+ console.log(`Commit: ${d.commit_sha}`);
5931
+ }
5932
+ if (d.commit_message) {
5933
+ const firstLine = d.commit_message.split(`
5934
+ `)[0] ?? "";
5935
+ console.log(`Message: ${firstLine.slice(0, 60)}${firstLine.length > 60 ? "..." : ""}`);
5936
+ }
5937
+ if (d.pr_number) {
5938
+ console.log(`PR: #${d.pr_number}${d.pr_title ? ` - ${d.pr_title}` : ""}`);
5939
+ }
5940
+ console.log();
5941
+ }
5942
+ console.log("Files");
5943
+ console.log("-".repeat(50));
5944
+ console.log(`Count: ${d.file_count.toLocaleString()} files`);
5945
+ console.log(`Size: ${formatBytes3(d.total_size_bytes)}`);
5946
+ console.log();
5947
+ if (d.status === "failed" && d.error_message) {
5948
+ console.log("Error");
5949
+ console.log("-".repeat(50));
5950
+ console.log(d.error_message);
5951
+ console.log();
5952
+ }
5953
+ if (d.status === "ready") {
5954
+ console.log("Actions");
5955
+ console.log("-".repeat(50));
5956
+ console.log(`Rollback to this deployment:`);
5957
+ console.log(` zerodeploy rollback <site> --org <org> --to ${d.id.slice(0, 8)}`);
5958
+ }
5959
+ } catch (err) {
5960
+ const message = err instanceof Error ? err.message : "Unknown error";
5961
+ console.error("Failed to get deployment:", message);
5962
+ }
5963
+ });
5964
+
4925
5965
  // src/commands/deployments/index.ts
4926
- var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand);
5966
+ var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand).addCommand(deploymentsShowCommand);
4927
5967
 
4928
5968
  // src/commands/rollback.ts
4929
5969
  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) => {
@@ -4941,13 +5981,13 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
4941
5981
  });
4942
5982
  if (!listRes.ok)
4943
5983
  throw new Error(`API Error ${listRes.status}`);
4944
- const deployments = await listRes.json();
5984
+ const { data: deployments } = await listRes.json();
4945
5985
  const readyDeployments = deployments.filter((d) => d.status === "ready");
4946
5986
  if (readyDeployments.length < 2) {
4947
5987
  console.log("No previous deployment to rollback to.");
4948
5988
  return;
4949
5989
  }
4950
- const currentIndex = readyDeployments.findIndex((d) => d.isCurrent);
5990
+ const currentIndex = readyDeployments.findIndex((d) => d.is_current);
4951
5991
  if (currentIndex === -1 || currentIndex >= readyDeployments.length - 1) {
4952
5992
  deploymentId = readyDeployments[1].id;
4953
5993
  } else {
@@ -4980,13 +6020,10 @@ var tokenCreateCommand = new Command2("create").description("Create a deploy tok
4980
6020
  return;
4981
6021
  }
4982
6022
  try {
4983
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
4984
- method: "POST",
4985
- headers: {
4986
- "Content-Type": "application/json",
4987
- Authorization: `Bearer ${token}`
4988
- },
4989
- body: JSON.stringify({ name })
6023
+ 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 }
4990
6027
  });
4991
6028
  if (!res.ok) {
4992
6029
  const error = await res.json();
@@ -5021,17 +6058,16 @@ var tokenListCommand = new Command2("list").description("List deploy tokens for
5021
6058
  return;
5022
6059
  }
5023
6060
  try {
5024
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
5025
- headers: {
5026
- Authorization: `Bearer ${token}`
5027
- }
6061
+ const client = getClient(token);
6062
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6063
+ param: { orgSlug: options.org, siteSlug: options.site }
5028
6064
  });
5029
6065
  if (!res.ok) {
5030
6066
  const error = await res.json();
5031
6067
  console.log(`Error: ${error.error || "Failed to list tokens"}`);
5032
6068
  return;
5033
6069
  }
5034
- const tokens = await res.json();
6070
+ const { data: tokens } = await res.json();
5035
6071
  if (tokens.length === 0) {
5036
6072
  console.log(`No deploy tokens for ${options.org}/${options.site}`);
5037
6073
  return;
@@ -5058,19 +6094,18 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
5058
6094
  return;
5059
6095
  }
5060
6096
  try {
6097
+ const client = getClient(token);
5061
6098
  let fullTokenId = tokenId;
5062
6099
  if (tokenId.length < 36) {
5063
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
5064
- headers: {
5065
- Authorization: `Bearer ${token}`
5066
- }
6100
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6101
+ param: { orgSlug: options.org, siteSlug: options.site }
5067
6102
  });
5068
6103
  if (!listRes.ok) {
5069
6104
  const error = await listRes.json();
5070
6105
  console.log(`Error: ${error.error || "Failed to find token"}`);
5071
6106
  return;
5072
6107
  }
5073
- const tokens = await listRes.json();
6108
+ const { data: tokens } = await listRes.json();
5074
6109
  const match = tokens.find((t) => t.id.startsWith(tokenId));
5075
6110
  if (!match) {
5076
6111
  console.log(`Error: No token found starting with "${tokenId}"`);
@@ -5078,11 +6113,8 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
5078
6113
  }
5079
6114
  fullTokenId = match.id;
5080
6115
  }
5081
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens/${fullTokenId}`, {
5082
- method: "DELETE",
5083
- headers: {
5084
- Authorization: `Bearer ${token}`
5085
- }
6116
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens[":tokenId"].$delete({
6117
+ param: { orgSlug: options.org, siteSlug: options.site, tokenId: fullTokenId }
5086
6118
  });
5087
6119
  if (!res.ok) {
5088
6120
  const error = await res.json();
@@ -5150,18 +6182,121 @@ var initCommand = new Command2("init").description("Create a zerodeploy.json con
5150
6182
  console.log(" zerodeploy deploy --build # build + deploy");
5151
6183
  });
5152
6184
 
6185
+ // src/commands/account-delete.ts
6186
+ import * as readline5 from "readline";
6187
+ function prompt4(question) {
6188
+ const rl = readline5.createInterface({
6189
+ input: process.stdin,
6190
+ output: process.stdout
6191
+ });
6192
+ return new Promise((resolve5) => {
6193
+ rl.question(question, (answer) => {
6194
+ rl.close();
6195
+ resolve5(answer);
6196
+ });
6197
+ });
6198
+ }
6199
+ var accountCommand = new Command2("account").description("Manage your ZeroDeploy account").addCommand(new Command2("email").description("Set your email address for notifications").argument("<email>", "Email address").action(async (email) => {
6200
+ const token = loadToken();
6201
+ if (!token) {
6202
+ displayAuthError();
6203
+ return;
6204
+ }
6205
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6206
+ if (!emailRegex.test(email)) {
6207
+ displayError({ code: "validation_error", message: "Invalid email address format" });
6208
+ return;
6209
+ }
6210
+ try {
6211
+ const client = getClient(token);
6212
+ const res = await client.auth.me.$patch({
6213
+ json: { email }
6214
+ });
6215
+ if (!res.ok) {
6216
+ const body = await res.json();
6217
+ displayError(parseApiError(body));
6218
+ return;
6219
+ }
6220
+ console.log(`
6221
+ ✅ Email updated to: ${email}`);
6222
+ console.log(` You will now receive deployment notifications at this address.
6223
+ `);
6224
+ } catch (err) {
6225
+ if (err instanceof Error) {
6226
+ displayNetworkError(err);
6227
+ } else {
6228
+ displayError({ code: "unknown_error", message: "Failed to update email" });
6229
+ }
6230
+ }
6231
+ })).addCommand(new Command2("delete").description("Permanently delete your account and all data").option("--force", "Skip confirmation prompts").action(async (options) => {
6232
+ const token = loadToken();
6233
+ if (!token) {
6234
+ displayAuthError();
6235
+ return;
6236
+ }
6237
+ if (!options.force) {
6238
+ console.log(`
6239
+ ⚠️ WARNING: This will permanently delete:`);
6240
+ console.log(" - Your account");
6241
+ console.log(" - All organizations you own");
6242
+ console.log(" - All sites and deployments");
6243
+ console.log(" - All custom domains");
6244
+ console.log(" - All deploy tokens");
6245
+ console.log(`
6246
+ This action CANNOT be undone.
6247
+ `);
6248
+ const answer1 = await prompt4('Type "delete my account" to confirm: ');
6249
+ if (answer1 !== "delete my account") {
6250
+ console.log("Cancelled.");
6251
+ return;
6252
+ }
6253
+ const answer2 = await prompt4("Are you absolutely sure? (y/N) ");
6254
+ if (answer2.toLowerCase() !== "y") {
6255
+ console.log("Cancelled.");
6256
+ return;
6257
+ }
6258
+ }
6259
+ try {
6260
+ const client = getClient(token);
6261
+ const res = await client.auth.me.$delete();
6262
+ if (!res.ok) {
6263
+ const body = await res.json();
6264
+ displayError(parseApiError(body));
6265
+ return;
6266
+ }
6267
+ const result = await res.json();
6268
+ deleteToken();
6269
+ console.log(`
6270
+ ✅ Account deleted successfully.`);
6271
+ console.log(` Deleted: ${result.deleted.orgs} org(s), ${result.deleted.sites} site(s), ${result.deleted.deployments} deployment(s)`);
6272
+ console.log(`
6273
+ Your local authentication token has been removed.`);
6274
+ console.log(` Thank you for using ZeroDeploy.
6275
+ `);
6276
+ } catch (err) {
6277
+ if (err instanceof Error) {
6278
+ displayNetworkError(err);
6279
+ } else {
6280
+ displayError({ code: "unknown_error", message: "Failed to delete account" });
6281
+ }
6282
+ }
6283
+ }));
6284
+
5153
6285
  // src/index.ts
5154
6286
  var program3 = new Command;
5155
6287
  program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.0").enablePositionalOptions();
5156
6288
  program3.addCommand(loginCommand);
5157
6289
  program3.addCommand(logoutCommand);
5158
6290
  program3.addCommand(whoamiCommand);
6291
+ program3.addCommand(usageCommand);
5159
6292
  program3.addCommand(orgCommand);
5160
6293
  program3.addCommand(siteCommand);
5161
6294
  program3.addCommand(domainCommand);
6295
+ program3.addCommand(formCommand);
5162
6296
  program3.addCommand(deployCommand);
5163
6297
  program3.addCommand(deploymentsCommand);
5164
6298
  program3.addCommand(rollbackCommand);
5165
6299
  program3.addCommand(tokenCommand);
5166
6300
  program3.addCommand(initCommand);
6301
+ program3.addCommand(accountCommand);
5167
6302
  program3.parse(process.argv);