@zerodeploy/cli 0.1.1 → 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 +246 -16
  2. package/dist/cli.js +1527 -179
  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();
@@ -3453,7 +3914,7 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3453
3914
  console.log(` Value: ${data.verification.recordValue}`);
3454
3915
  console.log();
3455
3916
  console.log("After adding the record, run:");
3456
- console.log(` zerodeploy domain verify ${data.id} --org ${options.org} --site ${options.site}`);
3917
+ console.log(` zerodeploy domain verify ${data.domain} --org ${options.org} --site ${options.site}`);
3457
3918
  console.log();
3458
3919
  } catch (err) {
3459
3920
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -3474,6 +3935,16 @@ function formatStatus(status) {
3474
3935
  return status;
3475
3936
  }
3476
3937
  }
3938
+ function formatRedirect(mode) {
3939
+ switch (mode) {
3940
+ case "www_to_apex":
3941
+ return "www→apex";
3942
+ case "apex_to_www":
3943
+ return "apex→www";
3944
+ default:
3945
+ return "";
3946
+ }
3947
+ }
3477
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) => {
3478
3949
  const token = loadToken();
3479
3950
  if (!token) {
@@ -3481,16 +3952,15 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3481
3952
  return;
3482
3953
  }
3483
3954
  try {
3484
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3485
- headers: {
3486
- Authorization: `Bearer ${token}`
3487
- }
3955
+ const client = getClient(token);
3956
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
3957
+ param: { orgSlug: options.org, siteSlug: options.site }
3488
3958
  });
3489
3959
  if (!res.ok) {
3490
3960
  const error = await res.json();
3491
3961
  throw new Error(error.error || `API Error ${res.status}`);
3492
3962
  }
3493
- const domains = await res.json();
3963
+ const { data: domains } = await res.json();
3494
3964
  if (domains.length === 0) {
3495
3965
  console.log("No custom domains configured.");
3496
3966
  console.log();
@@ -3501,7 +3971,9 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3501
3971
  console.log("Custom Domains:");
3502
3972
  console.log();
3503
3973
  for (const d of domains) {
3504
- console.log(` ${d.domain.padEnd(30)} ${formatStatus(d.verification_status).padEnd(15)} ${d.id}`);
3974
+ const redirect = formatRedirect(d.redirect_mode);
3975
+ const redirectStr = redirect ? ` [${redirect}]` : "";
3976
+ console.log(` ${d.domain.padEnd(30)} ${formatStatus(d.verification_status).padEnd(15)}${redirectStr}`);
3505
3977
  }
3506
3978
  console.log();
3507
3979
  } catch (err) {
@@ -3511,24 +3983,38 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3511
3983
  });
3512
3984
 
3513
3985
  // src/commands/domain/verify.ts
3514
- var domainVerifyCommand = new Command2("verify").description("Verify ownership of a custom domain").argument("<domainId>", "Domain ID to verify").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainId, options) => {
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) => {
3515
3987
  const token = loadToken();
3516
3988
  if (!token) {
3517
3989
  console.log("Not logged in. Run: zerodeploy login");
3518
3990
  return;
3519
3991
  }
3520
3992
  try {
3521
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domainId}/verify`, {
3522
- method: "POST",
3523
- headers: {
3524
- Authorization: `Bearer ${token}`,
3525
- "Content-Type": "application/json"
3526
- }
3993
+ const client = getClient(token);
3994
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
3995
+ param: { orgSlug: options.org, siteSlug: options.site }
3996
+ });
3997
+ if (!listRes.ok) {
3998
+ const error = await listRes.json();
3999
+ throw new Error(error.error || `API Error ${listRes.status}`);
4000
+ }
4001
+ const { data: domains } = await listRes.json();
4002
+ const domain = domains.find((d) => d.domain === domainName);
4003
+ 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;
4010
+ }
4011
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].verify.$post({
4012
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3527
4013
  });
3528
4014
  const data = await res.json();
3529
4015
  if (!res.ok) {
3530
4016
  console.error(`
3531
- ❌ Verification failed for domain`);
4017
+ ❌ Verification failed for ${domainName}`);
3532
4018
  if (data.message) {
3533
4019
  console.log();
3534
4020
  console.log(data.message);
@@ -3536,7 +4022,7 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3536
4022
  console.log();
3537
4023
  console.log("Tips:");
3538
4024
  console.log(" • DNS changes can take up to 48 hours to propagate");
3539
- console.log(" • Verify the TXT record is set correctly using: dig TXT _zerodeploy.<domain>");
4025
+ console.log(` • Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}`);
3540
4026
  console.log(" • Try again in a few minutes");
3541
4027
  return;
3542
4028
  }
@@ -3587,18 +4073,33 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3587
4073
  });
3588
4074
 
3589
4075
  // src/commands/domain/remove.ts
3590
- var domainRemoveCommand = new Command2("remove").description("Remove a custom domain from a site").argument("<domainId>", "Domain ID to remove").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainId, options) => {
4076
+ var domainRemoveCommand = new Command2("remove").description("Remove a custom domain from a site").argument("<domain>", "Domain to remove (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainName, options) => {
3591
4077
  const token = loadToken();
3592
4078
  if (!token) {
3593
4079
  console.log("Not logged in. Run: zerodeploy login");
3594
4080
  return;
3595
4081
  }
3596
4082
  try {
3597
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domainId}`, {
3598
- method: "DELETE",
3599
- headers: {
3600
- Authorization: `Bearer ${token}`
3601
- }
4083
+ const client = getClient(token);
4084
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4085
+ param: { orgSlug: options.org, siteSlug: options.site }
4086
+ });
4087
+ if (!listRes.ok) {
4088
+ const error = await listRes.json();
4089
+ throw new Error(error.error || `API Error ${listRes.status}`);
4090
+ }
4091
+ const { data: domains } = await listRes.json();
4092
+ const domain = domains.find((d) => d.domain === domainName);
4093
+ 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;
4100
+ }
4101
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].$delete({
4102
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3602
4103
  });
3603
4104
  if (!res.ok) {
3604
4105
  const error = await res.json();
@@ -3612,12 +4113,304 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
3612
4113
  }
3613
4114
  });
3614
4115
 
4116
+ // src/commands/domain/redirect.ts
4117
+ function formatRedirectMode(mode) {
4118
+ switch (mode) {
4119
+ case "none":
4120
+ return "No redirect";
4121
+ case "www_to_apex":
4122
+ return "www → apex (e.g., www.example.com → example.com)";
4123
+ case "apex_to_www":
4124
+ return "apex → www (e.g., example.com → www.example.com)";
4125
+ default:
4126
+ return mode;
4127
+ }
4128
+ }
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) => {
4130
+ const token = loadToken();
4131
+ if (!token) {
4132
+ console.log("Not logged in. Run: zerodeploy login");
4133
+ return;
4134
+ }
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
+
3615
4176
  // src/commands/domain/index.ts
3616
- var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand);
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`);
4382
+ return;
4383
+ }
4384
+ try {
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 }
4390
+ });
4391
+ if (!res.ok) {
4392
+ const error = await res.json();
4393
+ throw new Error(error.error || `API Error ${res.status}`);
4394
+ }
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
+ }
4402
+ } catch (err) {
4403
+ const message = err instanceof Error ? err.message : "Unknown error";
4404
+ console.error("Failed to update form notifications:", message);
4405
+ }
4406
+ });
4407
+
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);
3617
4410
 
3618
4411
  // src/commands/deploy/index.ts
3619
- import { resolve as resolve3 } from "node:path";
3620
- import { stat as stat2 } 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";
3621
4414
  import { spawn } from "node:child_process";
3622
4415
 
3623
4416
  // src/utils/files.ts
@@ -3657,7 +4450,7 @@ async function scanDirectory(rootDir) {
3657
4450
  await scan(rootDir);
3658
4451
  return files;
3659
4452
  }
3660
- function formatBytes(bytes) {
4453
+ function formatBytes2(bytes) {
3661
4454
  if (bytes === 0)
3662
4455
  return "0 B";
3663
4456
  const k = 1024;
@@ -3668,7 +4461,7 @@ function formatBytes(bytes) {
3668
4461
 
3669
4462
  // src/utils/framework.ts
3670
4463
  import { readFile as readFile2 } from "node:fs/promises";
3671
- import { resolve } from "node:path";
4464
+ import { resolve as resolve2 } from "node:path";
3672
4465
  var FRAMEWORKS = [
3673
4466
  {
3674
4467
  name: "Vite",
@@ -3742,7 +4535,7 @@ function hasDep(pkg, dep) {
3742
4535
  }
3743
4536
  async function detectFramework(cwd) {
3744
4537
  try {
3745
- const pkgPath = resolve(cwd, "package.json");
4538
+ const pkgPath = resolve2(cwd, "package.json");
3746
4539
  const pkgContent = await readFile2(pkgPath, "utf-8");
3747
4540
  const pkg = JSON.parse(pkgContent);
3748
4541
  for (const framework of FRAMEWORKS) {
@@ -4285,9 +5078,12 @@ function padToBlock(data) {
4285
5078
  padded.set(data);
4286
5079
  return padded;
4287
5080
  }
4288
- async function createTarArchive(files) {
5081
+ async function createTarArchive(files, onProgress) {
4289
5082
  const parts = [];
4290
- 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);
4291
5087
  const content = await readFile3(file.absolutePath);
4292
5088
  const header = createTarHeader(file.path, content.length);
4293
5089
  const paddedContent = padToBlock(new Uint8Array(content));
@@ -4304,8 +5100,8 @@ async function createTarArchive(files) {
4304
5100
  }
4305
5101
  return tar;
4306
5102
  }
4307
- async function createTarGz(files) {
4308
- const tar = await createTarArchive(files);
5103
+ async function createTarGz(files, onProgress) {
5104
+ const tar = await createTarArchive(files, onProgress);
4309
5105
  return gzipSync(tar, { level: 6 });
4310
5106
  }
4311
5107
  function formatCompression(original, compressed) {
@@ -4315,10 +5111,10 @@ function formatCompression(original, compressed) {
4315
5111
 
4316
5112
  // src/utils/project-config.ts
4317
5113
  import { existsSync, readFileSync } from "node:fs";
4318
- import { resolve as resolve2 } from "node:path";
5114
+ import { resolve as resolve3 } from "node:path";
4319
5115
  var CONFIG_FILENAME = "zerodeploy.json";
4320
5116
  function loadProjectConfig(cwd = process.cwd()) {
4321
- const configPath = resolve2(cwd, CONFIG_FILENAME);
5117
+ const configPath = resolve3(cwd, CONFIG_FILENAME);
4322
5118
  if (!existsSync(configPath)) {
4323
5119
  return {};
4324
5120
  }
@@ -4332,7 +5128,137 @@ function loadProjectConfig(cwd = process.cwd()) {
4332
5128
  }
4333
5129
  }
4334
5130
  function getConfigPath(cwd = process.cwd()) {
4335
- return resolve2(cwd, CONFIG_FILENAME);
5131
+ return resolve3(cwd, CONFIG_FILENAME);
5132
+ }
5133
+
5134
+ // src/utils/prompt.ts
5135
+ import * as readline4 from "node:readline";
5136
+ async function confirm(message, defaultValue = true) {
5137
+ const rl = readline4.createInterface({
5138
+ input: process.stdin,
5139
+ output: process.stdout
5140
+ });
5141
+ const hint = defaultValue ? "[Y/n]" : "[y/N]";
5142
+ return new Promise((resolve4) => {
5143
+ rl.question(`${message} ${hint} `, (answer) => {
5144
+ rl.close();
5145
+ const normalized = answer.trim().toLowerCase();
5146
+ if (normalized === "") {
5147
+ resolve4(defaultValue);
5148
+ } else if (normalized === "y" || normalized === "yes") {
5149
+ resolve4(true);
5150
+ } else {
5151
+ resolve4(false);
5152
+ }
5153
+ });
5154
+ });
5155
+ }
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
+ };
4336
5262
  }
4337
5263
 
4338
5264
  // src/commands/deploy/list.ts
@@ -4352,7 +5278,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4352
5278
  console.error(`❌ ${error.error}`);
4353
5279
  return;
4354
5280
  }
4355
- const deployments = await res.json();
5281
+ const { data: deployments } = await res.json();
4356
5282
  const limit = parseInt(options.limit, 10);
4357
5283
  const shown = deployments.slice(0, limit);
4358
5284
  if (shown.length === 0) {
@@ -4363,7 +5289,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4363
5289
  Deployments for ${options.org}/${siteSlug}:
4364
5290
  `);
4365
5291
  for (const d of shown) {
4366
- const current = d.isCurrent ? " ← current" : "";
5292
+ const current = d.is_current ? " ← current" : "";
4367
5293
  const status = formatStatus2(d.status);
4368
5294
  const date = new Date(d.created_at).toLocaleString();
4369
5295
  console.log(` ${d.id.slice(0, 8)} ${status} ${date}${current}`);
@@ -4414,22 +5340,113 @@ var deployRollbackCommand = new Command2("rollback").description("Rollback to a
4414
5340
  }
4415
5341
  });
4416
5342
 
5343
+ // src/commands/deploy/promote.ts
5344
+ var deployPromoteCommand = new Command2("promote").description("Promote a preview deployment to production").argument("<deploymentId>", "Deployment ID (or first 8 chars) to promote").action(async (deploymentId) => {
5345
+ const token = loadToken();
5346
+ if (!token) {
5347
+ console.log("Not logged in. Run: zerodeploy login");
5348
+ return;
5349
+ }
5350
+ try {
5351
+ const client = getClient(token);
5352
+ const res = await client.deployments[":id"].rollback.$post({
5353
+ param: { id: deploymentId }
5354
+ });
5355
+ if (!res.ok) {
5356
+ const error = await res.json();
5357
+ console.error(`Error: ${error.error}`);
5358
+ return;
5359
+ }
5360
+ const result = await res.json();
5361
+ console.log("Deployment promoted to production!");
5362
+ console.log(` Deployment: ${result.deployment.id}`);
5363
+ console.log(` URL: ${result.deployment.url}`);
5364
+ } catch (err) {
5365
+ const message = err instanceof Error ? err.message : "Unknown error";
5366
+ console.error("Failed to promote deployment:", message);
5367
+ }
5368
+ });
5369
+
4417
5370
  // src/commands/deploy/index.ts
5371
+ function slugify(input) {
5372
+ return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
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
+ }
4418
5429
  async function runCommand(command, cwd) {
4419
5430
  return new Promise((promiseResolve) => {
4420
- 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);
4421
5438
  const proc = spawn(cmd, args, {
4422
5439
  cwd,
4423
5440
  shell: true,
4424
5441
  stdio: ["inherit", "pipe", "pipe"]
4425
5442
  });
4426
5443
  let output = "";
4427
- proc.stdout?.on("data", (data) => {
5444
+ proc.stdout.on("data", (data) => {
4428
5445
  const text = data.toString();
4429
5446
  output += text;
4430
5447
  process.stdout.write(text);
4431
5448
  });
4432
- proc.stderr?.on("data", (data) => {
5449
+ proc.stderr.on("data", (data) => {
4433
5450
  const text = data.toString();
4434
5451
  output += text;
4435
5452
  process.stderr.write(text);
@@ -4445,12 +5462,12 @@ async function runCommand(command, cwd) {
4445
5462
  async function findBuildDirectory(cwd) {
4446
5463
  const candidates = ["dist", "build", "out", "public", "."];
4447
5464
  for (const dir of candidates) {
4448
- const fullPath = resolve3(cwd, dir);
5465
+ const fullPath = resolve4(cwd, dir);
4449
5466
  try {
4450
5467
  const stats = await stat2(fullPath);
4451
5468
  if (stats.isDirectory()) {
4452
5469
  try {
4453
- await stat2(resolve3(fullPath, "index.html"));
5470
+ await stat2(resolve4(fullPath, "index.html"));
4454
5471
  return fullPath;
4455
5472
  } catch {
4456
5473
  if (dir !== ".")
@@ -4462,42 +5479,126 @@ async function findBuildDirectory(cwd) {
4462
5479
  }
4463
5480
  return null;
4464
5481
  }
4465
- async function uploadArchive(token, uploadUrl, archive) {
4466
- const res = await fetch(uploadUrl, {
5482
+ async function uploadArchive(token, uploadUrl, archive, onRetry) {
5483
+ const res = await fetchWithRetry(uploadUrl, {
4467
5484
  method: "POST",
4468
5485
  headers: {
4469
5486
  "Content-Type": "application/gzip",
4470
5487
  Authorization: `Bearer ${token}`
4471
5488
  },
4472
5489
  body: archive
5490
+ }, {
5491
+ maxRetries: 3,
5492
+ onRetry
4473
5493
  });
4474
5494
  return res.ok;
4475
5495
  }
4476
- var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).action(async (siteSlugArg, options) => {
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) => {
4477
5497
  const cwd = process.cwd();
4478
- const config = loadProjectConfig(cwd);
4479
- const siteSlug = siteSlugArg || config.site;
4480
- const orgSlug = options.org || config.org;
4481
- const dirOption = options.dir || config.dir;
4482
- if (!siteSlug) {
4483
- console.log("Error: Site is required. Provide as argument or in zerodeploy.json");
4484
- deployCommand.help();
4485
- return;
4486
- }
4487
- if (!orgSlug) {
4488
- console.log('Error: --org is required (or set "org" in zerodeploy.json)');
4489
- return;
4490
- }
4491
5498
  const token = loadToken();
4492
5499
  if (!token) {
4493
- console.log("Not logged in. Run: zerodeploy login");
5500
+ displayAuthError();
4494
5501
  return;
4495
5502
  }
5503
+ const config = loadProjectConfig(cwd);
5504
+ let siteSlug = siteSlugArg || config.site;
5505
+ let orgSlug = options.org || config.org;
5506
+ const dirOption = options.dir || config.dir;
5507
+ if (!siteSlug || !orgSlug) {
5508
+ const client = getClient(token);
5509
+ const meRes = await client.auth.me.$get();
5510
+ if (!meRes.ok) {
5511
+ console.log("Error: Failed to fetch user info");
5512
+ return;
5513
+ }
5514
+ const userInfo = await meRes.json();
5515
+ if (!orgSlug) {
5516
+ if (!userInfo.personalOrg) {
5517
+ console.log("Error: No personal org found. Please create one with: zerodeploy org create <name>");
5518
+ return;
5519
+ }
5520
+ orgSlug = userInfo.personalOrg.slug;
5521
+ }
5522
+ if (!siteSlug) {
5523
+ const folderName = basename(cwd);
5524
+ const suggestedName = slugify(folderName) || "my-site";
5525
+ console.log("");
5526
+ const shouldCreate = await confirm(`No site configured. Create site "${suggestedName}"?`, true);
5527
+ if (!shouldCreate) {
5528
+ console.log("Deploy cancelled. Create a site first with: zerodeploy site create");
5529
+ return;
5530
+ }
5531
+ console.log("");
5532
+ console.log(`Creating site "${suggestedName}"...`);
5533
+ const createRes = await client.orgs[":orgSlug"].sites.$post({
5534
+ param: { orgSlug },
5535
+ json: { name: suggestedName, subdomain: suggestedName }
5536
+ });
5537
+ if (!createRes.ok) {
5538
+ const error = await createRes.json();
5539
+ console.log(`Error: ${error.error || "Failed to create site"}`);
5540
+ return;
5541
+ }
5542
+ const site = await createRes.json();
5543
+ siteSlug = site.slug;
5544
+ console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
5545
+ const configPath = getConfigPath(cwd);
5546
+ const newConfig = { org: orgSlug, site: siteSlug, dir: dirOption || config.dir };
5547
+ await writeFile2(configPath, JSON.stringify(newConfig, null, 2) + `
5548
+ `);
5549
+ console.log(`Saved config to zerodeploy.json`);
5550
+ console.log("");
5551
+ }
5552
+ }
4496
5553
  const framework = await detectFramework(cwd);
4497
5554
  if (framework) {
4498
5555
  console.log(`Detected: ${framework.name}`);
4499
5556
  }
4500
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
+ }
4501
5602
  if (options.install) {
4502
5603
  const installCmd = config.install || framework?.installCommand || "npm install";
4503
5604
  console.log(`
@@ -4528,7 +5629,7 @@ Error: Build failed`);
4528
5629
  }
4529
5630
  let deployDir;
4530
5631
  if (dirOption) {
4531
- deployDir = resolve3(cwd, dirOption);
5632
+ deployDir = resolve4(cwd, dirOption);
4532
5633
  try {
4533
5634
  const stats = await stat2(deployDir);
4534
5635
  if (!stats.isDirectory()) {
@@ -4540,7 +5641,7 @@ Error: Build failed`);
4540
5641
  return;
4541
5642
  }
4542
5643
  } else {
4543
- const detected = framework ? resolve3(cwd, framework.outputDir) : await findBuildDirectory(cwd);
5644
+ const detected = framework ? resolve4(cwd, framework.outputDir) : await findBuildDirectory(cwd);
4544
5645
  if (!detected) {
4545
5646
  console.log("Error: Could not find build directory. Use --dir to specify.");
4546
5647
  return;
@@ -4565,7 +5666,7 @@ Error: Build failed`);
4565
5666
  return;
4566
5667
  }
4567
5668
  const totalSize = files.reduce((sum, f) => sum + f.size, 0);
4568
- console.log(`Found ${files.length} files (${formatBytes(totalSize)})`);
5669
+ console.log(`Found ${files.length} files (${formatBytes2(totalSize)})`);
4569
5670
  try {
4570
5671
  const client = getClient(token);
4571
5672
  const deployPayload = { siteSlug, orgSlug };
@@ -4589,62 +5690,106 @@ Error: Build failed`);
4589
5690
  });
4590
5691
  if (!createRes.ok) {
4591
5692
  const body = await createRes.json();
4592
- let message = "Failed to create deployment";
4593
- if (typeof body.error === "string") {
4594
- message = body.error;
4595
- } else if (typeof body.message === "string") {
4596
- message = body.message;
4597
- } else if (body.success === false && body.error && typeof body.error === "object") {
4598
- const zodError = body.error;
4599
- if (zodError.issues?.[0]) {
4600
- const issue = zodError.issues[0];
4601
- const field = issue.path?.join(".") || "";
4602
- message = field ? `${field}: ${issue.message}` : issue.message;
4603
- } else if (zodError.message) {
4604
- try {
4605
- const issues = JSON.parse(zodError.message);
4606
- if (issues[0]) {
4607
- const field = issues[0].path?.join(".") || "";
4608
- message = field ? `${field}: ${issues[0].message}` : issues[0].message;
4609
- }
4610
- } catch {
4611
- message = zodError.message;
4612
- }
4613
- }
4614
- }
4615
- console.log(`Error: ${message}`);
5693
+ displayError(parseApiError(body));
4616
5694
  return;
4617
5695
  }
4618
5696
  const deployment = await createRes.json();
4619
5697
  console.log(`Created deployment: ${deployment.id}`);
4620
- console.log("Creating archive...");
4621
- const archive = await createTarGz(files);
4622
- console.log(` ${formatBytes(totalSize)} -> ${formatBytes(archive.length)} (${formatCompression(totalSize, archive.length)})`);
4623
- 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();
4624
5706
  const uploadUrl = deployment.uploadUrl || `${API_URL}/deployments/${deployment.id}/upload`;
4625
- 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
+ });
4626
5710
  if (!uploadSuccess) {
4627
- console.log("Error: Failed to upload archive");
5711
+ uploadSpinner.stop("Error: Failed to upload archive");
4628
5712
  return;
4629
5713
  }
4630
- console.log(` Uploaded ${formatBytes(archive.length)}`);
4631
- console.log("Finalizing...");
4632
- 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`, {
4633
5718
  method: "POST",
4634
5719
  headers: {
4635
5720
  "Content-Type": "application/json",
4636
5721
  Authorization: `Bearer ${token}`
4637
5722
  },
4638
- body: JSON.stringify({})
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
+ }
4639
5729
  });
4640
5730
  if (!finalizeRes.ok) {
4641
- console.log("Error: Failed to finalize deployment");
5731
+ finalizeSpinner.stop("Error: Failed to finalize deployment");
4642
5732
  return;
4643
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
+ }
4644
5773
  console.log("");
4645
- console.log("Deployment successful!");
4646
- console.log(`URL: ${deployment.url}`);
4647
- console.log(`Preview: ${deployment.previewUrl}`);
5774
+ if (options.preview) {
5775
+ console.log("Preview deployment created!");
5776
+ if (verified) {
5777
+ console.log(`Preview: ${deployment.previewUrl} (verified)`);
5778
+ } else {
5779
+ console.log(`Preview: ${deployment.previewUrl}`);
5780
+ }
5781
+ console.log("");
5782
+ console.log(`To make this deployment live, run:`);
5783
+ console.log(` zerodeploy deploy promote ${deployment.id.slice(0, 8)}`);
5784
+ } else {
5785
+ console.log("Deployment successful!");
5786
+ if (verified) {
5787
+ console.log(`URL: ${deployment.url} (verified)`);
5788
+ } else {
5789
+ console.log(`URL: ${deployment.url}`);
5790
+ }
5791
+ console.log(`Preview: ${deployment.previewUrl}`);
5792
+ }
4648
5793
  if (options.githubOutput) {
4649
5794
  const githubOutputFile = process.env.GITHUB_OUTPUT;
4650
5795
  if (githubOutputFile) {
@@ -4663,8 +5808,14 @@ Error: Build failed`);
4663
5808
  }
4664
5809
  }
4665
5810
  } catch (err) {
4666
- const message = err instanceof Error ? err.message : "Unknown error";
4667
- 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
+ }
4668
5819
  }
4669
5820
  });
4670
5821
 
@@ -4682,7 +5833,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4682
5833
  });
4683
5834
  if (!res.ok)
4684
5835
  throw new Error(`API Error ${res.status}`);
4685
- const deployments = await res.json();
5836
+ const { data: deployments } = await res.json();
4686
5837
  if (deployments.length === 0) {
4687
5838
  console.log("No deployments found.");
4688
5839
  return;
@@ -4690,7 +5841,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4690
5841
  console.log("Deployments:");
4691
5842
  console.log();
4692
5843
  for (const d of deployments) {
4693
- const current = d.isCurrent ? " (current)" : "";
5844
+ const current = d.is_current ? " (current)" : "";
4694
5845
  const date = new Date(d.created_at).toLocaleString();
4695
5846
  const shortId = d.id.slice(0, 8);
4696
5847
  const commit = d.commit_sha ? ` [${d.commit_sha.slice(0, 7)}]` : "";
@@ -4698,8 +5849,9 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4698
5849
  const message = d.commit_message ? ` - ${(d.commit_message.split(`
4699
5850
  `)[0] ?? "").slice(0, 50)}` : "";
4700
5851
  console.log(` ${shortId} ${d.status.padEnd(10)} ${date}${current}`);
5852
+ console.log(` ${d.preview_url}`);
4701
5853
  if (commit || message) {
4702
- console.log(` ${commit}${branch}${message}`);
5854
+ console.log(` ${commit}${branch}${message}`);
4703
5855
  }
4704
5856
  console.log();
4705
5857
  }
@@ -4709,8 +5861,109 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4709
5861
  }
4710
5862
  });
4711
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
+
4712
5965
  // src/commands/deployments/index.ts
4713
- var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand);
5966
+ var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand).addCommand(deploymentsShowCommand);
4714
5967
 
4715
5968
  // src/commands/rollback.ts
4716
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) => {
@@ -4728,13 +5981,13 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
4728
5981
  });
4729
5982
  if (!listRes.ok)
4730
5983
  throw new Error(`API Error ${listRes.status}`);
4731
- const deployments = await listRes.json();
5984
+ const { data: deployments } = await listRes.json();
4732
5985
  const readyDeployments = deployments.filter((d) => d.status === "ready");
4733
5986
  if (readyDeployments.length < 2) {
4734
5987
  console.log("No previous deployment to rollback to.");
4735
5988
  return;
4736
5989
  }
4737
- const currentIndex = readyDeployments.findIndex((d) => d.isCurrent);
5990
+ const currentIndex = readyDeployments.findIndex((d) => d.is_current);
4738
5991
  if (currentIndex === -1 || currentIndex >= readyDeployments.length - 1) {
4739
5992
  deploymentId = readyDeployments[1].id;
4740
5993
  } else {
@@ -4767,13 +6020,10 @@ var tokenCreateCommand = new Command2("create").description("Create a deploy tok
4767
6020
  return;
4768
6021
  }
4769
6022
  try {
4770
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
4771
- method: "POST",
4772
- headers: {
4773
- "Content-Type": "application/json",
4774
- Authorization: `Bearer ${token}`
4775
- },
4776
- 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 }
4777
6027
  });
4778
6028
  if (!res.ok) {
4779
6029
  const error = await res.json();
@@ -4808,17 +6058,16 @@ var tokenListCommand = new Command2("list").description("List deploy tokens for
4808
6058
  return;
4809
6059
  }
4810
6060
  try {
4811
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
4812
- headers: {
4813
- Authorization: `Bearer ${token}`
4814
- }
6061
+ const client = getClient(token);
6062
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6063
+ param: { orgSlug: options.org, siteSlug: options.site }
4815
6064
  });
4816
6065
  if (!res.ok) {
4817
6066
  const error = await res.json();
4818
6067
  console.log(`Error: ${error.error || "Failed to list tokens"}`);
4819
6068
  return;
4820
6069
  }
4821
- const tokens = await res.json();
6070
+ const { data: tokens } = await res.json();
4822
6071
  if (tokens.length === 0) {
4823
6072
  console.log(`No deploy tokens for ${options.org}/${options.site}`);
4824
6073
  return;
@@ -4845,19 +6094,18 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
4845
6094
  return;
4846
6095
  }
4847
6096
  try {
6097
+ const client = getClient(token);
4848
6098
  let fullTokenId = tokenId;
4849
6099
  if (tokenId.length < 36) {
4850
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
4851
- headers: {
4852
- Authorization: `Bearer ${token}`
4853
- }
6100
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
6101
+ param: { orgSlug: options.org, siteSlug: options.site }
4854
6102
  });
4855
6103
  if (!listRes.ok) {
4856
6104
  const error = await listRes.json();
4857
6105
  console.log(`Error: ${error.error || "Failed to find token"}`);
4858
6106
  return;
4859
6107
  }
4860
- const tokens = await listRes.json();
6108
+ const { data: tokens } = await listRes.json();
4861
6109
  const match = tokens.find((t) => t.id.startsWith(tokenId));
4862
6110
  if (!match) {
4863
6111
  console.log(`Error: No token found starting with "${tokenId}"`);
@@ -4865,11 +6113,8 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
4865
6113
  }
4866
6114
  fullTokenId = match.id;
4867
6115
  }
4868
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens/${fullTokenId}`, {
4869
- method: "DELETE",
4870
- headers: {
4871
- Authorization: `Bearer ${token}`
4872
- }
6116
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens[":tokenId"].$delete({
6117
+ param: { orgSlug: options.org, siteSlug: options.site, tokenId: fullTokenId }
4873
6118
  });
4874
6119
  if (!res.ok) {
4875
6120
  const error = await res.json();
@@ -4937,18 +6182,121 @@ var initCommand = new Command2("init").description("Create a zerodeploy.json con
4937
6182
  console.log(" zerodeploy deploy --build # build + deploy");
4938
6183
  });
4939
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
+
4940
6285
  // src/index.ts
4941
6286
  var program3 = new Command;
4942
6287
  program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.0").enablePositionalOptions();
4943
6288
  program3.addCommand(loginCommand);
4944
6289
  program3.addCommand(logoutCommand);
4945
6290
  program3.addCommand(whoamiCommand);
6291
+ program3.addCommand(usageCommand);
4946
6292
  program3.addCommand(orgCommand);
4947
6293
  program3.addCommand(siteCommand);
4948
6294
  program3.addCommand(domainCommand);
6295
+ program3.addCommand(formCommand);
4949
6296
  program3.addCommand(deployCommand);
4950
6297
  program3.addCommand(deploymentsCommand);
4951
6298
  program3.addCommand(rollbackCommand);
4952
6299
  program3.addCommand(tokenCommand);
4953
6300
  program3.addCommand(initCommand);
6301
+ program3.addCommand(accountCommand);
4954
6302
  program3.parse(process.argv);