@zerodeploy/cli 0.1.2 → 0.1.4

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 (4) hide show
  1. package/README.md +184 -16
  2. package/dist/cli.js +2225 -429
  3. package/dist/index.js +6915 -0
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2743,13 +2743,168 @@ 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
+ var ExitCode = {
2749
+ SUCCESS: 0,
2750
+ AUTH_ERROR: 1,
2751
+ NOT_FOUND: 2,
2752
+ VALIDATION_ERROR: 3,
2753
+ RATE_LIMIT: 4,
2754
+ SERVER_ERROR: 5,
2755
+ NETWORK_ERROR: 6,
2756
+ BILLING_ERROR: 7
2757
+ };
2758
+ function getExitCode(status, errorCode) {
2759
+ if (errorCode) {
2760
+ if (errorCode === "billing_required" || errorCode.includes("billing")) {
2761
+ return ExitCode.BILLING_ERROR;
2762
+ }
2763
+ if (errorCode === "validation_error" || errorCode.includes("invalid")) {
2764
+ return ExitCode.VALIDATION_ERROR;
2765
+ }
2766
+ if (errorCode === "rate_limit_exceeded" || errorCode.includes("rate_limit")) {
2767
+ return ExitCode.RATE_LIMIT;
2768
+ }
2769
+ }
2770
+ if (status === 402) {
2771
+ return ExitCode.BILLING_ERROR;
2772
+ }
2773
+ if (status === 401 || status === 403) {
2774
+ return ExitCode.AUTH_ERROR;
2775
+ }
2776
+ if (status === 404) {
2777
+ return ExitCode.NOT_FOUND;
2778
+ }
2779
+ if (status === 422 || status === 400) {
2780
+ return ExitCode.VALIDATION_ERROR;
2781
+ }
2782
+ if (status === 429) {
2783
+ return ExitCode.RATE_LIMIT;
2784
+ }
2785
+ if (status >= 500) {
2786
+ return ExitCode.SERVER_ERROR;
2787
+ }
2788
+ return ExitCode.AUTH_ERROR;
2789
+ }
2790
+ function parseApiError(body) {
2791
+ if (!body || typeof body !== "object") {
2792
+ return {
2793
+ code: "unknown_error",
2794
+ message: "Unknown error occurred"
2795
+ };
2796
+ }
2797
+ const response = body;
2798
+ if (typeof response.error === "object" && response.error !== null) {
2799
+ return response.error;
2800
+ }
2801
+ if (typeof response.error === "string") {
2802
+ return {
2803
+ code: "api_error",
2804
+ message: response.error
2805
+ };
2806
+ }
2807
+ if ("message" in response && typeof response.message === "string") {
2808
+ return {
2809
+ code: "api_error",
2810
+ message: response.message
2811
+ };
2812
+ }
2813
+ if ("success" in response && response.success === false) {
2814
+ const zodError = response;
2815
+ if (zodError.error?.issues?.[0]) {
2816
+ const issue = zodError.error.issues[0];
2817
+ const field = issue.path?.join(".") || "";
2818
+ return {
2819
+ code: "validation_error",
2820
+ message: field ? `${field}: ${issue.message}` : issue.message
2821
+ };
2822
+ }
2823
+ }
2824
+ return {
2825
+ code: "unknown_error",
2826
+ message: "Unknown error occurred"
2827
+ };
2828
+ }
2829
+ function displayError(error) {
2830
+ if (typeof error === "string") {
2831
+ console.error(`
2832
+ ❌ ${error}
2833
+ `);
2834
+ return;
2835
+ }
2836
+ console.error(`
2837
+ ❌ ${error.message}`);
2838
+ if (error.details && Object.keys(error.details).length > 0) {
2839
+ console.error();
2840
+ for (const [key, value] of Object.entries(error.details)) {
2841
+ const formattedKey = key.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
2842
+ console.error(` ${formattedKey}: ${value}`);
2843
+ }
2844
+ }
2845
+ if (error.hint) {
2846
+ console.error();
2847
+ console.error(` Hint: ${error.hint}`);
2848
+ }
2849
+ if (error.docs) {
2850
+ console.error(` Docs: ${error.docs}`);
2851
+ }
2852
+ console.error();
2853
+ }
2854
+ async function handleApiError(response) {
2855
+ let body;
2856
+ try {
2857
+ body = await response.json();
2858
+ } catch {
2859
+ body = null;
2860
+ }
2861
+ const error = parseApiError(body);
2862
+ displayError(error);
2863
+ const exitCode = getExitCode(response.status, error.code);
2864
+ process.exit(exitCode);
2865
+ }
2866
+ function displayNetworkError(error) {
2867
+ if (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED")) {
2868
+ displayError({
2869
+ code: "connection_error",
2870
+ message: "Could not connect to ZeroDeploy API",
2871
+ hint: "Check your internet connection and try again"
2872
+ });
2873
+ } else if (error.message.includes("timeout")) {
2874
+ displayError({
2875
+ code: "timeout_error",
2876
+ message: "Request timed out",
2877
+ hint: "Check your internet connection and try again"
2878
+ });
2879
+ } else {
2880
+ displayError({
2881
+ code: "network_error",
2882
+ message: error.message || "Network error occurred",
2883
+ hint: "Check your internet connection and try again"
2884
+ });
2885
+ }
2886
+ }
2887
+ function displayAuthError() {
2888
+ displayError({
2889
+ code: "unauthorized",
2890
+ message: "Not logged in",
2891
+ hint: "For local use: Run `zerodeploy login`\nFor CI/CD: Set ZERODEPLOY_TOKEN environment variable",
2892
+ docs: "https://zerodeploy.dev/docs/cli/auth"
2893
+ });
2894
+ }
2895
+ function handleAuthError() {
2896
+ displayAuthError();
2897
+ process.exit(ExitCode.AUTH_ERROR);
2898
+ }
2899
+
2900
+ // src/auth/token.ts
2746
2901
  var DIR = path2.join(os2.homedir(), ".zerodeploy");
2747
2902
  var TOKEN_PATH = path2.join(DIR, "token");
2748
2903
  function saveToken(token) {
2749
2904
  if (!fs6.existsSync(DIR)) {
2750
- fs6.mkdirSync(DIR);
2905
+ fs6.mkdirSync(DIR, { mode: 448 });
2751
2906
  }
2752
- fs6.writeFileSync(TOKEN_PATH, token, "utf8");
2907
+ fs6.writeFileSync(TOKEN_PATH, token, { encoding: "utf8", mode: 384 });
2753
2908
  }
2754
2909
  function loadToken() {
2755
2910
  const envToken = process.env.ZERODEPLOY_TOKEN;
@@ -2780,16 +2935,28 @@ var loginCommand = new Command2("login").description("Login via GitHub OAuth wit
2780
2935
  if (req.url?.startsWith(CALLBACK_PATH)) {
2781
2936
  const url = new URL(req.url, `http://localhost:${PORT}`);
2782
2937
  const token = url.searchParams.get("token");
2938
+ const pending = url.searchParams.get("pending") === "true";
2783
2939
  if (!token) {
2784
2940
  res.writeHead(400);
2785
2941
  res.end("Login failed: missing token");
2786
- console.error("Login failed: no token received");
2787
- process.exit(1);
2942
+ displayError({ code: "auth_error", message: "Login failed: no token received" });
2943
+ process.exit(ExitCode.AUTH_ERROR);
2788
2944
  }
2789
2945
  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.");
2946
+ if (pending) {
2947
+ res.writeHead(200, { "Content-Type": "text/plain" });
2948
+ res.end("You're on the waitlist! We'll email you when your account is approved.");
2949
+ console.log("");
2950
+ console.log("\uD83D\uDD50 You're on the waitlist!");
2951
+ console.log("");
2952
+ console.log("Thanks for signing up. Your account is pending approval.");
2953
+ console.log("We'll send you an email when you're approved.");
2954
+ console.log("");
2955
+ } else {
2956
+ res.writeHead(200, { "Content-Type": "text/plain" });
2957
+ res.end("Login successful! You can close this window.");
2958
+ console.log("Login successful! Token saved locally.");
2959
+ }
2793
2960
  server.close();
2794
2961
  } else {
2795
2962
  res.writeHead(404);
@@ -3091,89 +3258,364 @@ var hc = (baseUrl, options) => createProxy(function proxyCallback(opts) {
3091
3258
  return req;
3092
3259
  }, []);
3093
3260
 
3261
+ // ../../packages/api-client/src/retry.ts
3262
+ var DEFAULT_OPTIONS = {
3263
+ maxRetries: 3,
3264
+ baseDelayMs: 1000,
3265
+ maxDelayMs: 1e4,
3266
+ timeoutMs: 30000
3267
+ };
3268
+ var RETRYABLE_STATUS_CODES = new Set([
3269
+ 408,
3270
+ 429,
3271
+ 502,
3272
+ 503,
3273
+ 504
3274
+ ]);
3275
+ function isRetryableError(error) {
3276
+ if (!(error instanceof Error))
3277
+ return false;
3278
+ const message = error.message.toLowerCase();
3279
+ if (message.includes("fetch failed"))
3280
+ return true;
3281
+ if (message.includes("econnrefused"))
3282
+ return true;
3283
+ if (message.includes("econnreset"))
3284
+ return true;
3285
+ if (message.includes("etimedout"))
3286
+ return true;
3287
+ if (message.includes("enotfound"))
3288
+ return true;
3289
+ if (message.includes("enetunreach"))
3290
+ return true;
3291
+ if (message.includes("timeout"))
3292
+ return true;
3293
+ if (message.includes("aborted"))
3294
+ return true;
3295
+ if (message.includes("ssl"))
3296
+ return true;
3297
+ if (message.includes("certificate"))
3298
+ return true;
3299
+ return false;
3300
+ }
3301
+ function isRetryableStatus(status) {
3302
+ return RETRYABLE_STATUS_CODES.has(status);
3303
+ }
3304
+ function calculateDelay(attempt, baseDelayMs, maxDelayMs) {
3305
+ const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
3306
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
3307
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
3308
+ return Math.round(cappedDelay + jitter);
3309
+ }
3310
+ function getDelayFromHeaders(response) {
3311
+ const retryAfter = response.headers.get("Retry-After");
3312
+ if (retryAfter) {
3313
+ const seconds = parseInt(retryAfter, 10);
3314
+ if (!isNaN(seconds)) {
3315
+ return seconds * 1000;
3316
+ }
3317
+ const date = Date.parse(retryAfter);
3318
+ if (!isNaN(date)) {
3319
+ return Math.max(0, date - Date.now());
3320
+ }
3321
+ }
3322
+ const resetAt = response.headers.get("X-RateLimit-Reset");
3323
+ if (resetAt) {
3324
+ const resetTimestamp = parseInt(resetAt, 10);
3325
+ if (!isNaN(resetTimestamp)) {
3326
+ const delayMs = resetTimestamp * 1000 - Date.now();
3327
+ return Math.max(1000, delayMs);
3328
+ }
3329
+ }
3330
+ return null;
3331
+ }
3332
+ function sleep(ms) {
3333
+ return new Promise((resolve) => setTimeout(resolve, ms));
3334
+ }
3335
+ async function fetchWithRetry(input, init, options) {
3336
+ const opts = { ...DEFAULT_OPTIONS, ...options };
3337
+ let lastError;
3338
+ for (let attempt = 0;attempt <= opts.maxRetries; attempt++) {
3339
+ let controller;
3340
+ let timeoutId;
3341
+ if (opts.timeoutMs > 0) {
3342
+ controller = new AbortController;
3343
+ timeoutId = setTimeout(() => controller.abort(), opts.timeoutMs);
3344
+ }
3345
+ try {
3346
+ const fetchInit = controller ? { ...init, signal: controller.signal } : init ?? {};
3347
+ const response = await fetch(input, fetchInit);
3348
+ if (timeoutId)
3349
+ clearTimeout(timeoutId);
3350
+ if (isRetryableStatus(response.status) && attempt < opts.maxRetries) {
3351
+ const headerDelay = response.status === 429 ? getDelayFromHeaders(response) : null;
3352
+ const delayMs = headerDelay ?? calculateDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
3353
+ const cappedDelay = Math.min(delayMs, opts.maxDelayMs);
3354
+ opts.onRetry?.(attempt + 1, response, cappedDelay);
3355
+ await sleep(cappedDelay);
3356
+ continue;
3357
+ }
3358
+ return response;
3359
+ } catch (error) {
3360
+ if (timeoutId)
3361
+ clearTimeout(timeoutId);
3362
+ lastError = error instanceof Error ? error : new Error(String(error));
3363
+ if (!isRetryableError(error) || attempt >= opts.maxRetries) {
3364
+ throw lastError;
3365
+ }
3366
+ const delayMs = calculateDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
3367
+ opts.onRetry?.(attempt + 1, lastError, delayMs);
3368
+ await sleep(delayMs);
3369
+ }
3370
+ }
3371
+ throw lastError ?? new Error("Max retries exceeded");
3372
+ }
3373
+ function createRetryFetch(options) {
3374
+ return (input, init) => {
3375
+ return fetchWithRetry(input, init, options);
3376
+ };
3377
+ }
3378
+ function formatRetryMessage(attempt, maxRetries, error) {
3379
+ const errorDesc = error instanceof Response ? `HTTP ${error.status}` : error.message.split(`
3380
+ `)[0];
3381
+ return `Retry ${attempt}/${maxRetries} (${errorDesc})`;
3382
+ }
3383
+
3384
+ // ../../packages/api-client/src/interceptors.ts
3385
+ async function applyRequestInterceptors(request, interceptors) {
3386
+ let req = request;
3387
+ for (const interceptor of interceptors) {
3388
+ req = await interceptor(req);
3389
+ }
3390
+ return req;
3391
+ }
3392
+ async function applyResponseInterceptors(response, interceptors) {
3393
+ let res = response;
3394
+ for (const interceptor of interceptors) {
3395
+ res = await interceptor(res);
3396
+ }
3397
+ return res;
3398
+ }
3399
+ function createInterceptedFetch(baseFetch, interceptors) {
3400
+ return async (input, init) => {
3401
+ let request = new Request(input, init);
3402
+ if (interceptors.request?.length) {
3403
+ request = await applyRequestInterceptors(request, interceptors.request);
3404
+ }
3405
+ let response = await baseFetch(request);
3406
+ if (interceptors.response?.length) {
3407
+ response = await applyResponseInterceptors(response, interceptors.response);
3408
+ }
3409
+ return response;
3410
+ };
3411
+ }
3412
+
3094
3413
  // ../../packages/api-client/src/index.ts
3095
- function createClient(baseUrl, token) {
3414
+ var DEFAULT_RETRY_OPTIONS = {
3415
+ maxRetries: 3,
3416
+ baseDelayMs: 1000,
3417
+ maxDelayMs: 1e4
3418
+ };
3419
+ function createClient(baseUrl, token, options) {
3420
+ let fetchFn = options?.fetch;
3421
+ if (!fetchFn && options?.retry !== false) {
3422
+ const retryOpts = typeof options?.retry === "object" ? options.retry : DEFAULT_RETRY_OPTIONS;
3423
+ fetchFn = createRetryFetch(retryOpts);
3424
+ }
3425
+ if (options?.useCredentials && fetchFn) {
3426
+ const baseFetch = fetchFn;
3427
+ fetchFn = (input, init) => baseFetch(input, { ...init, credentials: "include" });
3428
+ } else if (options?.useCredentials) {
3429
+ fetchFn = (input, init) => fetch(input, { ...init, credentials: "include" });
3430
+ }
3431
+ if (options?.interceptors && (options.interceptors.request?.length || options.interceptors.response?.length)) {
3432
+ const baseFetch = fetchFn || fetch;
3433
+ fetchFn = createInterceptedFetch(baseFetch, options.interceptors);
3434
+ }
3096
3435
  return hc(baseUrl, {
3097
- headers: token ? { Authorization: `Bearer ${token}` } : undefined
3436
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
3437
+ fetch: fetchFn
3098
3438
  });
3099
3439
  }
3100
3440
 
3101
3441
  // src/auth/http.ts
3102
- function getClient(token) {
3103
- return createClient(API_URL, token);
3442
+ var DEFAULT_RETRY_OPTIONS2 = {
3443
+ maxRetries: 3,
3444
+ baseDelayMs: 1000,
3445
+ maxDelayMs: 1e4,
3446
+ onRetry: (attempt, error, delayMs) => {
3447
+ if (process.stderr.isTTY) {
3448
+ const msg = formatRetryMessage(attempt, 3, error);
3449
+ process.stderr.write(`\r\x1B[K ${msg}, waiting ${Math.round(delayMs / 1000)}s...`);
3450
+ }
3451
+ }
3452
+ };
3453
+ function getClient(token, retryOptions) {
3454
+ const opts = { ...DEFAULT_RETRY_OPTIONS2, ...retryOptions };
3455
+ return createClient(API_URL, token, { retry: opts });
3104
3456
  }
3105
3457
 
3106
3458
  // src/commands/whoami.ts
3107
- var whoamiCommand = new Command2("whoami").description("Show the currently logged-in user").action(async () => {
3459
+ var whoamiCommand = new Command2("whoami").description("Show the currently logged-in user").option("--json", "Output as JSON").action(async (options) => {
3108
3460
  const token = loadToken();
3109
3461
  if (!token) {
3110
- console.log("❌ You are not logged in. Run `zerodeploy login` first.");
3111
- process.exit(1);
3462
+ handleAuthError();
3112
3463
  }
3113
3464
  try {
3114
3465
  const client = getClient(token);
3115
3466
  const res = await client.auth.me.$get();
3116
- if (!res.ok)
3117
- throw new Error(`API Error ${res.status}`);
3467
+ if (!res.ok) {
3468
+ await handleApiError(res);
3469
+ }
3118
3470
  const data = await res.json();
3471
+ if (options.json) {
3472
+ console.log(JSON.stringify(data, null, 2));
3473
+ return;
3474
+ }
3119
3475
  console.log("Logged in as:");
3120
3476
  console.log(` Username: ${data.username}`);
3477
+ console.log(` Email: ${data.email || "(not set)"}`);
3121
3478
  console.log(` User ID: ${data.id}`);
3122
3479
  console.log(` Admin: ${data.isAdmin ? "Yes" : "No"}`);
3480
+ if (data.approved === false) {
3481
+ console.log("");
3482
+ console.log("\uD83D\uDD50 Status: Pending approval (waitlist)");
3483
+ console.log(" We'll email you when your account is approved.");
3484
+ }
3485
+ } catch (err) {
3486
+ const message = err instanceof Error ? err.message : String(err);
3487
+ displayError({ code: "network_error", message: `Failed to fetch user info: ${message}` });
3488
+ process.exit(ExitCode.NETWORK_ERROR);
3489
+ }
3490
+ });
3491
+
3492
+ // src/commands/usage.ts
3493
+ function progressBar(current, max, width = 20) {
3494
+ const percentage = Math.min(current / max, 1);
3495
+ const filled = Math.round(percentage * width);
3496
+ const empty = width - filled;
3497
+ const bar = "█".repeat(filled) + "░".repeat(empty);
3498
+ const color = percentage >= 0.9 ? "\x1B[31m" : percentage >= 0.7 ? "\x1B[33m" : "\x1B[32m";
3499
+ const reset = "\x1B[0m";
3500
+ return `${color}${bar}${reset}`;
3501
+ }
3502
+ function formatUsageLine(label, current, max, suffix = "") {
3503
+ const bar = progressBar(current, max);
3504
+ const percentage = Math.round(current / max * 100);
3505
+ return ` ${label.padEnd(24)} ${bar} ${current}/${max}${suffix} (${percentage}%)`;
3506
+ }
3507
+ var usageCommand = new Command2("usage").description("Show current usage and limits").option("--json", "Output as JSON").action(async (options) => {
3508
+ const token = loadToken();
3509
+ if (!token) {
3510
+ handleAuthError();
3511
+ }
3512
+ try {
3513
+ const client = getClient(token);
3514
+ const res = await client.auth.me.usage.$get();
3515
+ if (!res.ok) {
3516
+ await handleApiError(res);
3517
+ }
3518
+ const data = await res.json();
3519
+ if (options.json) {
3520
+ console.log(JSON.stringify(data, null, 2));
3521
+ return;
3522
+ }
3523
+ console.log();
3524
+ console.log("Account Usage:");
3525
+ console.log(formatUsageLine("Organizations", data.usage.orgs, data.limits.max_orgs));
3526
+ console.log(formatUsageLine("Total Sites", data.usage.sites, data.limits.max_orgs * data.limits.max_sites_per_org));
3527
+ console.log(formatUsageLine("Deployments (month)", data.usage.deployments_this_month, data.limits.max_deployments_per_month));
3528
+ console.log();
3529
+ console.log("Limits:");
3530
+ console.log(` Sites per org: ${data.limits.max_sites_per_org}`);
3531
+ console.log(` Deployments per day: ${data.limits.max_deployments_per_day}`);
3532
+ console.log(` Deployments per month: ${data.limits.max_deployments_per_month}`);
3533
+ console.log(` Max deployment size: ${data.limits.max_deployment_size}`);
3534
+ console.log(` Storage per org: ${data.limits.max_storage_per_org}`);
3535
+ console.log(` Deploy tokens per site: ${data.limits.max_deploy_tokens_per_site}`);
3536
+ console.log(` Domains per site: ${data.limits.max_domains_per_site}`);
3537
+ console.log(` API requests/min: ${data.limits.api_requests_per_minute}`);
3538
+ console.log(` Deploy requests/min: ${data.limits.deploy_requests_per_minute}`);
3539
+ console.log();
3540
+ if (data.orgs.length > 0) {
3541
+ console.log("Organization Usage:");
3542
+ for (const org of data.orgs) {
3543
+ console.log();
3544
+ console.log(` ${org.name} (${org.slug}):`);
3545
+ console.log(formatUsageLine("Sites", org.sites_count, org.limits.max_sites));
3546
+ console.log(formatUsageLine("Deployments (month)", org.deployments_this_month, org.limits.max_deployments_per_month));
3547
+ console.log(` Storage: ${org.storage_used} / ${org.limits.max_storage}`);
3548
+ }
3549
+ console.log();
3550
+ }
3123
3551
  } catch (err) {
3124
- console.error("❌ Failed to fetch user info:", err.message || err);
3125
- process.exit(1);
3552
+ const message = err instanceof Error ? err.message : String(err);
3553
+ displayError({
3554
+ code: "network_error",
3555
+ message: `Failed to fetch usage info: ${message}`
3556
+ });
3557
+ process.exit(ExitCode.NETWORK_ERROR);
3126
3558
  }
3127
3559
  });
3128
3560
 
3129
3561
  // src/commands/org/list.ts
3130
- var orgListCommand = new Command2("list").description("List your organizations").action(async () => {
3562
+ var orgListCommand = new Command2("list").description("List your organizations").option("--json", "Output as JSON").action(async (options) => {
3131
3563
  const token = loadToken();
3132
3564
  if (!token) {
3133
- console.log("❌ Not logged in. Run `zerodeploy login` first.");
3134
- return;
3565
+ handleAuthError();
3135
3566
  }
3136
3567
  try {
3137
3568
  const client = getClient(token);
3138
3569
  const res = await client.orgs.$get();
3139
- if (!res.ok)
3140
- throw new Error(`API Error ${res.status}`);
3141
- const orgs = await res.json();
3570
+ if (!res.ok) {
3571
+ await handleApiError(res);
3572
+ }
3573
+ const { data: orgs } = await res.json();
3574
+ if (options.json) {
3575
+ console.log(JSON.stringify(orgs, null, 2));
3576
+ return;
3577
+ }
3142
3578
  if (orgs.length === 0) {
3143
3579
  console.log("No organizations found.");
3144
3580
  } else {
3145
3581
  console.log("Organizations:");
3146
3582
  for (const o of orgs) {
3147
- console.log(` ${o.slug.padEnd(20)} ${o.name} [${o.plan}]`);
3583
+ console.log(` ${o.slug.padEnd(20)} ${o.name}`);
3148
3584
  }
3149
3585
  }
3150
3586
  } catch (err) {
3151
- console.error("Failed to fetch orgs:", err.message || err);
3587
+ const message = err instanceof Error ? err.message : String(err);
3588
+ displayError({ code: "network_error", message: `Failed to fetch orgs: ${message}` });
3589
+ process.exit(ExitCode.NETWORK_ERROR);
3152
3590
  }
3153
3591
  });
3154
3592
 
3155
3593
  // src/commands/org/create.ts
3156
- var orgCreateCommand = new Command2("create").description("Create a new organization").argument("<name>", "Organization name").action(async (name) => {
3594
+ var orgCreateCommand = new Command2("create").description("Create a new organization").argument("<name>", "Organization name").option("--json", "Output as JSON").action(async (name, options) => {
3157
3595
  const token = loadToken();
3158
3596
  if (!token) {
3159
- console.log("❌ Not logged in. Run `zerodeploy login` first.");
3160
- return;
3597
+ handleAuthError();
3161
3598
  }
3162
3599
  try {
3163
3600
  const client = getClient(token);
3164
3601
  const res = await client.orgs.$post({ json: { name } });
3165
3602
  if (!res.ok) {
3166
- const body = await res.json();
3167
- const message = body.error || `API Error ${res.status}`;
3168
- console.error(`❌ ${message}`);
3603
+ await handleApiError(res);
3604
+ }
3605
+ const { data: org } = await res.json();
3606
+ if (options.json) {
3607
+ console.log(JSON.stringify(org, null, 2));
3169
3608
  return;
3170
3609
  }
3171
- const org = await res.json();
3172
- console.log(`✅ Created org: ${org.name}`);
3610
+ console.log(`
3611
+ Created org: ${org.name}`);
3173
3612
  console.log(` Slug: ${org.slug}`);
3174
- console.log(` ID: ${org.id}`);
3613
+ console.log(` ID: ${org.id}
3614
+ `);
3175
3615
  } catch (err) {
3176
- console.error("Failed to create org:", err.message || err);
3616
+ const message = err instanceof Error ? err.message : String(err);
3617
+ displayError({ code: "network_error", message: `Failed to create org: ${message}` });
3618
+ process.exit(ExitCode.NETWORK_ERROR);
3177
3619
  }
3178
3620
  });
3179
3621
 
@@ -3194,8 +3636,7 @@ function prompt(question) {
3194
3636
  var orgDeleteCommand = new Command2("delete").description("Delete an organization (must have no sites)").argument("<orgSlug>", "Organization slug").option("--force", "Skip confirmation prompt").action(async (orgSlug, options) => {
3195
3637
  const token = loadToken();
3196
3638
  if (!token) {
3197
- console.log("Not logged in. Run: zerodeploy login");
3198
- return;
3639
+ handleAuthError();
3199
3640
  }
3200
3641
  if (!options.force) {
3201
3642
  const answer = await prompt(`Are you sure you want to delete organization "${orgSlug}"? This cannot be undone. (y/N) `);
@@ -3210,19 +3651,16 @@ var orgDeleteCommand = new Command2("delete").description("Delete an organizatio
3210
3651
  param: { orgSlug }
3211
3652
  });
3212
3653
  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}`);
3654
+ await handleApiError(res);
3220
3655
  }
3221
3656
  const result = await res.json();
3222
- console.log(`✅ ${result.message}`);
3657
+ console.log(`
3658
+ ${result.message}
3659
+ `);
3223
3660
  } catch (err) {
3224
- const message = err instanceof Error ? err.message : "Unknown error";
3225
- console.error("Failed to delete organization:", message);
3661
+ const message = err instanceof Error ? err.message : String(err);
3662
+ displayError({ code: "network_error", message: `Failed to delete organization: ${message}` });
3663
+ process.exit(ExitCode.NETWORK_ERROR);
3226
3664
  }
3227
3665
  });
3228
3666
 
@@ -3230,18 +3668,22 @@ var orgDeleteCommand = new Command2("delete").description("Delete an organizatio
3230
3668
  var orgCommand = new Command2("org").description("Manage organizations").addCommand(orgListCommand).addCommand(orgCreateCommand).addCommand(orgDeleteCommand);
3231
3669
 
3232
3670
  // src/commands/site/list.ts
3233
- var siteListCommand = new Command2("list").description("List sites in an organization").argument("<orgSlug>", "Organization slug").action(async (orgSlug) => {
3671
+ var siteListCommand = new Command2("list").description("List sites in an organization").argument("<orgSlug>", "Organization slug").option("--json", "Output as JSON").action(async (orgSlug, options) => {
3234
3672
  const token = loadToken();
3235
3673
  if (!token) {
3236
- console.log("Not logged in. Run: zerodeploy login");
3237
- return;
3674
+ handleAuthError();
3238
3675
  }
3239
3676
  try {
3240
3677
  const client = getClient(token);
3241
3678
  const res = await client.orgs[":orgSlug"].sites.$get({ param: { orgSlug } });
3242
- if (!res.ok)
3243
- throw new Error(`API Error ${res.status}`);
3244
- const sites = await res.json();
3679
+ if (!res.ok) {
3680
+ await handleApiError(res);
3681
+ }
3682
+ const { data: sites } = await res.json();
3683
+ if (options.json) {
3684
+ console.log(JSON.stringify(sites, null, 2));
3685
+ return;
3686
+ }
3245
3687
  if (sites.length === 0) {
3246
3688
  console.log("No sites found.");
3247
3689
  return;
@@ -3252,17 +3694,17 @@ var siteListCommand = new Command2("list").description("List sites in an organiz
3252
3694
  console.log(` ${s.slug.padEnd(20)} ${s.name}${repo}`);
3253
3695
  }
3254
3696
  } catch (err) {
3255
- const message = err instanceof Error ? err.message : "Unknown error";
3256
- console.error("Failed to list sites:", message);
3697
+ const message = err instanceof Error ? err.message : String(err);
3698
+ displayError({ code: "network_error", message: `Failed to list sites: ${message}` });
3699
+ process.exit(ExitCode.NETWORK_ERROR);
3257
3700
  }
3258
3701
  });
3259
3702
 
3260
3703
  // src/commands/site/create.ts
3261
- var siteCreateCommand = new Command2("create").description("Create a site in an organization").argument("<orgSlug>", "Organization slug").argument("<name>", "Site name").requiredOption("--subdomain <subdomain>", 'Subdomain for the site (e.g., "my-site" for my-site.zerodeploy.app)').option("--repo <owner/repo>", 'Link to GitHub repository (e.g., "vercel/next.js")').action(async (orgSlug, name, options) => {
3704
+ var siteCreateCommand = new Command2("create").description("Create a site in an organization").argument("<orgSlug>", "Organization slug").argument("<name>", "Site name").option("--subdomain <subdomain>", "Subdomain for the site (defaults to slugified name)").option("--repo <owner/repo>", 'Link to GitHub repository (e.g., "vercel/next.js")').option("--json", "Output as JSON").action(async (orgSlug, name, options) => {
3262
3705
  const token = loadToken();
3263
3706
  if (!token) {
3264
- console.log("Not logged in. Run: zerodeploy login");
3265
- return;
3707
+ handleAuthError();
3266
3708
  }
3267
3709
  try {
3268
3710
  const client = getClient(token);
@@ -3271,11 +3713,14 @@ var siteCreateCommand = new Command2("create").description("Create a site in an
3271
3713
  json: { name, subdomain: options.subdomain, githubRepo: options.repo }
3272
3714
  });
3273
3715
  if (!res.ok) {
3274
- const error = await res.json();
3275
- throw new Error(error.error || `API Error ${res.status}`);
3716
+ await handleApiError(res);
3717
+ }
3718
+ const { data: site } = await res.json();
3719
+ if (options.json) {
3720
+ console.log(JSON.stringify(site, null, 2));
3721
+ return;
3276
3722
  }
3277
- const site = await res.json();
3278
- console.log(`✅ Created site: ${site.name}`);
3723
+ console.log(`Created site: ${site.name}`);
3279
3724
  console.log(` Subdomain: ${site.subdomain}.zerodeploy.app`);
3280
3725
  console.log(` Slug: ${site.slug}`);
3281
3726
  console.log(` ID: ${site.id}`);
@@ -3283,8 +3728,9 @@ var siteCreateCommand = new Command2("create").description("Create a site in an
3283
3728
  console.log(` Repo: ${site.github_repo}`);
3284
3729
  }
3285
3730
  } catch (err) {
3286
- const message = err instanceof Error ? err.message : "Unknown error";
3287
- console.error("Failed to create site:", message);
3731
+ const message = err instanceof Error ? err.message : String(err);
3732
+ displayError({ code: "network_error", message: `Failed to create site: ${message}` });
3733
+ process.exit(ExitCode.NETWORK_ERROR);
3288
3734
  }
3289
3735
  });
3290
3736
 
@@ -3305,8 +3751,7 @@ function prompt2(question) {
3305
3751
  var siteDeleteCommand = new Command2("delete").description("Delete a site and all its deployments").argument("<siteSlug>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--force", "Skip confirmation prompt").action(async (siteSlug, options) => {
3306
3752
  const token = loadToken();
3307
3753
  if (!token) {
3308
- console.log("Not logged in. Run: zerodeploy login");
3309
- return;
3754
+ handleAuthError();
3310
3755
  }
3311
3756
  if (!options.force) {
3312
3757
  const answer = await prompt2(`Are you sure you want to delete site "${siteSlug}" and all its deployments? This cannot be undone. (y/N) `);
@@ -3321,70 +3766,64 @@ var siteDeleteCommand = new Command2("delete").description("Delete a site and al
3321
3766
  param: { orgSlug: options.org, siteSlug }
3322
3767
  });
3323
3768
  if (!res.ok) {
3324
- const error = await res.json();
3325
- throw new Error(error.error || `API Error ${res.status}`);
3769
+ await handleApiError(res);
3326
3770
  }
3327
3771
  const result = await res.json();
3328
- console.log(`✅ ${result.message}`);
3772
+ console.log(result.message);
3329
3773
  } catch (err) {
3330
- const message = err instanceof Error ? err.message : "Unknown error";
3331
- console.error("Failed to delete site:", message);
3774
+ const message = err instanceof Error ? err.message : String(err);
3775
+ displayError({ code: "network_error", message: `Failed to delete site: ${message}` });
3776
+ process.exit(ExitCode.NETWORK_ERROR);
3332
3777
  }
3333
3778
  });
3334
3779
 
3335
3780
  // src/commands/site/link.ts
3336
- var siteLinkCommand = new Command2("link").description("Link a site to a GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<repo>", 'GitHub repository (e.g., "owner/repo")').action(async (orgSlug, siteSlug, repo) => {
3781
+ var siteLinkCommand = new Command2("link").description("Link a site to a GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<repo>", 'GitHub repository (e.g., "owner/repo")').option("--json", "Output as JSON").action(async (orgSlug, siteSlug, repo, options) => {
3337
3782
  const token = loadToken();
3338
3783
  if (!token) {
3339
- console.log("Not logged in. Run: zerodeploy login");
3340
- return;
3784
+ handleAuthError();
3341
3785
  }
3342
3786
  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 })
3787
+ const client = getClient(token);
3788
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3789
+ param: { orgSlug, siteSlug },
3790
+ json: { githubRepo: repo }
3350
3791
  });
3351
3792
  if (!res.ok) {
3352
- const error = await res.json();
3353
- console.log(`Error: ${error.error || "Failed to link repository"}`);
3793
+ await handleApiError(res);
3794
+ }
3795
+ const { data: site } = await res.json();
3796
+ if (options.json) {
3797
+ console.log(JSON.stringify(site, null, 2));
3354
3798
  return;
3355
3799
  }
3356
- const site = await res.json();
3357
3800
  console.log(`Site "${site.name}" linked to ${site.github_repo}`);
3358
3801
  } catch (err) {
3359
- const message = err instanceof Error ? err.message : "Unknown error";
3360
- console.error("Failed to link repository:", message);
3802
+ const message = err instanceof Error ? err.message : String(err);
3803
+ displayError({ code: "network_error", message: `Failed to link repository: ${message}` });
3804
+ process.exit(ExitCode.NETWORK_ERROR);
3361
3805
  }
3362
3806
  });
3363
3807
  var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from its GitHub repository").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").action(async (orgSlug, siteSlug) => {
3364
3808
  const token = loadToken();
3365
3809
  if (!token) {
3366
- console.log("Not logged in. Run: zerodeploy login");
3367
- return;
3810
+ handleAuthError();
3368
3811
  }
3369
3812
  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 })
3813
+ const client = getClient(token);
3814
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3815
+ param: { orgSlug, siteSlug },
3816
+ json: { githubRepo: null }
3377
3817
  });
3378
3818
  if (!res.ok) {
3379
- const error = await res.json();
3380
- console.log(`Error: ${error.error || "Failed to unlink repository"}`);
3381
- return;
3819
+ await handleApiError(res);
3382
3820
  }
3383
- const site = await res.json();
3821
+ const { data: site } = await res.json();
3384
3822
  console.log(`Site "${site.name}" unlinked from GitHub repository`);
3385
3823
  } catch (err) {
3386
- const message = err instanceof Error ? err.message : "Unknown error";
3387
- console.error("Failed to unlink repository:", message);
3824
+ const message = err instanceof Error ? err.message : String(err);
3825
+ displayError({ code: "network_error", message: `Failed to unlink repository: ${message}` });
3826
+ process.exit(ExitCode.NETWORK_ERROR);
3388
3827
  }
3389
3828
  });
3390
3829
 
@@ -3392,8 +3831,7 @@ var siteUnlinkCommand = new Command2("unlink").description("Unlink a site from i
3392
3831
  var siteSubdomainCommand = new Command2("subdomain").description("Update the subdomain for a site").argument("<orgSlug>", "Organization slug").argument("<siteSlug>", "Site slug").argument("<subdomain>", "New subdomain").action(async (orgSlug, siteSlug, subdomain) => {
3393
3832
  const token = loadToken();
3394
3833
  if (!token) {
3395
- console.log("Not logged in. Run: zerodeploy login");
3396
- return;
3834
+ handleAuthError();
3397
3835
  }
3398
3836
  try {
3399
3837
  const client = getClient(token);
@@ -3402,49 +3840,185 @@ var siteSubdomainCommand = new Command2("subdomain").description("Update the sub
3402
3840
  json: { subdomain }
3403
3841
  });
3404
3842
  if (!res.ok) {
3405
- const error = await res.json();
3406
- throw new Error(error.error || `API Error ${res.status}`);
3843
+ await handleApiError(res);
3407
3844
  }
3408
- const site = await res.json();
3409
- console.log(`✅ Updated subdomain for ${site.name}`);
3845
+ const { data: site } = await res.json();
3846
+ console.log(`Updated subdomain for ${site.name}`);
3410
3847
  console.log(` New URL: ${site.subdomain}.zerodeploy.app`);
3411
3848
  } catch (err) {
3412
- const message = err instanceof Error ? err.message : "Unknown error";
3413
- console.error("Failed to update subdomain:", message);
3849
+ const message = err instanceof Error ? err.message : String(err);
3850
+ displayError({ code: "network_error", message: `Failed to update subdomain: ${message}` });
3851
+ process.exit(ExitCode.NETWORK_ERROR);
3852
+ }
3853
+ });
3854
+
3855
+ // src/commands/site/rename.ts
3856
+ var siteRenameCommand = new Command2("rename").description("Rename a site").argument("<siteSlug>", "Site slug").argument("<newName>", "New name for the site").requiredOption("--org <orgSlug>", "Organization slug").action(async (siteSlug, newName, options) => {
3857
+ const token = loadToken();
3858
+ if (!token) {
3859
+ handleAuthError();
3860
+ }
3861
+ try {
3862
+ const client = getClient(token);
3863
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].$patch({
3864
+ param: { orgSlug: options.org, siteSlug },
3865
+ json: { name: newName }
3866
+ });
3867
+ if (!res.ok) {
3868
+ await handleApiError(res);
3869
+ }
3870
+ const { data: site } = await res.json();
3871
+ console.log(`Renamed site to: ${site.name}`);
3872
+ } catch (err) {
3873
+ const message = err instanceof Error ? err.message : String(err);
3874
+ displayError({ code: "network_error", message: `Failed to rename site: ${message}` });
3875
+ process.exit(ExitCode.NETWORK_ERROR);
3876
+ }
3877
+ });
3878
+
3879
+ // src/commands/site/stats.ts
3880
+ function formatBytes(bytes) {
3881
+ if (bytes === 0)
3882
+ return "0 B";
3883
+ const k = 1024;
3884
+ const sizes = ["B", "KB", "MB", "GB"];
3885
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3886
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
3887
+ }
3888
+ function formatNumber(n) {
3889
+ if (n >= 1e6)
3890
+ return (n / 1e6).toFixed(1) + "M";
3891
+ if (n >= 1000)
3892
+ return (n / 1000).toFixed(1) + "K";
3893
+ return n.toString();
3894
+ }
3895
+ var siteStatsCommand = new Command2("stats").description("View site traffic analytics").argument("<site>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--period <period>", "Time period: 24h, 7d, or 30d", "7d").option("--json", "Output as JSON").action(async (site, options) => {
3896
+ const token = loadToken();
3897
+ if (!token) {
3898
+ handleAuthError();
3899
+ }
3900
+ const validPeriods = ["24h", "7d", "30d"];
3901
+ if (!validPeriods.includes(options.period)) {
3902
+ displayError({
3903
+ code: "validation_error",
3904
+ message: `Invalid period: ${options.period}`,
3905
+ hint: `Valid periods: ${validPeriods.join(", ")}`
3906
+ });
3907
+ process.exit(ExitCode.VALIDATION_ERROR);
3908
+ }
3909
+ try {
3910
+ const client = getClient(token);
3911
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].analytics.$get({
3912
+ param: { orgSlug: options.org, siteSlug: site },
3913
+ query: { period: options.period }
3914
+ });
3915
+ if (!res.ok) {
3916
+ await handleApiError(res);
3917
+ }
3918
+ const { data } = await res.json();
3919
+ if (options.json) {
3920
+ console.log(JSON.stringify(data, null, 2));
3921
+ return;
3922
+ }
3923
+ if (!data.configured) {
3924
+ console.log("Analytics not configured for this site.");
3925
+ return;
3926
+ }
3927
+ const { overview, topPages, countries, statusCodes } = data;
3928
+ const periodLabel = options.period === "24h" ? "Last 24 hours" : options.period === "7d" ? "Last 7 days" : "Last 30 days";
3929
+ console.log(`Analytics for ${options.org}/${site}`);
3930
+ console.log(`Period: ${periodLabel}`);
3931
+ console.log();
3932
+ console.log("Overview");
3933
+ console.log(" " + "-".repeat(40));
3934
+ console.log(` Requests: ${formatNumber(overview.totalRequests).padStart(10)}`);
3935
+ console.log(` Bandwidth: ${formatBytes(overview.totalBytes).padStart(10)}`);
3936
+ console.log(` Unique visitors: ${formatNumber(overview.uniqueVisitors).padStart(10)}`);
3937
+ console.log(` Unique pages: ${formatNumber(overview.uniquePaths).padStart(10)}`);
3938
+ if (overview.formSubmissions > 0) {
3939
+ console.log(` Form submissions:${formatNumber(overview.formSubmissions).padStart(10)}`);
3940
+ }
3941
+ if (overview.bounceRate > 0) {
3942
+ console.log(` Bounce rate: ${overview.bounceRate.toFixed(1).padStart(9)}%`);
3943
+ }
3944
+ console.log();
3945
+ if (topPages.length > 0) {
3946
+ console.log("Top Pages");
3947
+ console.log(" " + "-".repeat(50));
3948
+ const maxPages = 5;
3949
+ for (const page of topPages.slice(0, maxPages)) {
3950
+ const path3 = page.path.length > 30 ? page.path.slice(0, 27) + "..." : page.path;
3951
+ console.log(` ${path3.padEnd(32)} ${formatNumber(page.requests).padStart(8)} reqs`);
3952
+ }
3953
+ console.log();
3954
+ }
3955
+ if (countries.length > 0) {
3956
+ console.log("Top Countries");
3957
+ console.log(" " + "-".repeat(40));
3958
+ const maxCountries = 5;
3959
+ for (const c of countries.slice(0, maxCountries)) {
3960
+ console.log(` ${c.country.padEnd(20)} ${formatNumber(c.requests).padStart(8)} reqs`);
3961
+ }
3962
+ console.log();
3963
+ }
3964
+ if (statusCodes.length > 0) {
3965
+ const grouped = { success: 0, redirect: 0, clientError: 0, serverError: 0 };
3966
+ for (const s of statusCodes) {
3967
+ if (s.statusCode >= 200 && s.statusCode < 300)
3968
+ grouped.success += s.requests;
3969
+ else if (s.statusCode >= 300 && s.statusCode < 400)
3970
+ grouped.redirect += s.requests;
3971
+ else if (s.statusCode >= 400 && s.statusCode < 500)
3972
+ grouped.clientError += s.requests;
3973
+ else if (s.statusCode >= 500)
3974
+ grouped.serverError += s.requests;
3975
+ }
3976
+ console.log("Response Codes");
3977
+ console.log(" " + "-".repeat(40));
3978
+ if (grouped.success > 0)
3979
+ console.log(` 2xx (success): ${formatNumber(grouped.success).padStart(10)}`);
3980
+ if (grouped.redirect > 0)
3981
+ console.log(` 3xx (redirect): ${formatNumber(grouped.redirect).padStart(10)}`);
3982
+ if (grouped.clientError > 0)
3983
+ console.log(` 4xx (client err):${formatNumber(grouped.clientError).padStart(10)}`);
3984
+ if (grouped.serverError > 0)
3985
+ console.log(` 5xx (server err):${formatNumber(grouped.serverError).padStart(10)}`);
3986
+ console.log();
3987
+ }
3988
+ console.log("View detailed analytics in the dashboard.");
3989
+ } catch (err) {
3990
+ const message = err instanceof Error ? err.message : String(err);
3991
+ displayError({ code: "network_error", message: `Failed to get analytics: ${message}` });
3992
+ process.exit(ExitCode.NETWORK_ERROR);
3414
3993
  }
3415
3994
  });
3416
3995
 
3417
3996
  // 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);
3997
+ 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
3998
 
3420
3999
  // src/commands/domain/add.ts
3421
4000
  var domainAddCommand = new Command2("add").description("Add a custom domain to a site").argument("<domain>", "Domain to add (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domain, options) => {
3422
4001
  const token = loadToken();
3423
4002
  if (!token) {
3424
- console.log("Not logged in. Run: zerodeploy login");
3425
- return;
4003
+ handleAuthError();
3426
4004
  }
3427
4005
  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 })
4006
+ const client = getClient(token);
4007
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$post({
4008
+ param: { orgSlug: options.org, siteSlug: options.site },
4009
+ json: { domain }
3435
4010
  });
3436
4011
  if (!res.ok) {
3437
- const error = await res.json();
3438
- throw new Error(error.error || `API Error ${res.status}`);
4012
+ await handleApiError(res);
3439
4013
  }
3440
- const data = await res.json();
4014
+ const { data } = await res.json();
3441
4015
  console.log(`
3442
- Domain added: ${data.domain}`);
4016
+ Domain added: ${data.domain}`);
3443
4017
  console.log(` ID: ${data.id}`);
3444
4018
  console.log(` Status: ${data.verification_status}`);
3445
4019
  console.log();
3446
- console.log("\uD83D\uDCCB DNS Verification Required");
3447
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
4020
+ console.log("DNS Verification Required");
4021
+ console.log("-------------------------");
3448
4022
  console.log();
3449
4023
  console.log("Add the following TXT record to your DNS:");
3450
4024
  console.log();
@@ -3456,8 +4030,9 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3456
4030
  console.log(` zerodeploy domain verify ${data.domain} --org ${options.org} --site ${options.site}`);
3457
4031
  console.log();
3458
4032
  } catch (err) {
3459
- const message = err instanceof Error ? err.message : "Unknown error";
3460
- console.error("Failed to add domain:", message);
4033
+ const message = err instanceof Error ? err.message : String(err);
4034
+ displayError({ code: "network_error", message: `Failed to add domain: ${message}` });
4035
+ process.exit(ExitCode.NETWORK_ERROR);
3461
4036
  }
3462
4037
  });
3463
4038
 
@@ -3465,11 +4040,11 @@ var domainAddCommand = new Command2("add").description("Add a custom domain to a
3465
4040
  function formatStatus(status) {
3466
4041
  switch (status) {
3467
4042
  case "verified":
3468
- return "verified";
4043
+ return "verified";
3469
4044
  case "pending":
3470
- return "pending";
4045
+ return "pending";
3471
4046
  case "failed":
3472
- return "failed";
4047
+ return "failed";
3473
4048
  default:
3474
4049
  return status;
3475
4050
  }
@@ -3477,30 +4052,31 @@ function formatStatus(status) {
3477
4052
  function formatRedirect(mode) {
3478
4053
  switch (mode) {
3479
4054
  case "www_to_apex":
3480
- return "wwwapex";
4055
+ return "www->apex";
3481
4056
  case "apex_to_www":
3482
- return "apexwww";
4057
+ return "apex->www";
3483
4058
  default:
3484
4059
  return "";
3485
4060
  }
3486
4061
  }
3487
- var domainListCommand = new Command2("list").description("List custom domains for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
4062
+ var domainListCommand = new Command2("list").description("List custom domains for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--json", "Output as JSON").action(async (options) => {
3488
4063
  const token = loadToken();
3489
4064
  if (!token) {
3490
- console.log("Not logged in. Run: zerodeploy login");
3491
- return;
4065
+ handleAuthError();
3492
4066
  }
3493
4067
  try {
3494
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3495
- headers: {
3496
- Authorization: `Bearer ${token}`
3497
- }
4068
+ const client = getClient(token);
4069
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4070
+ param: { orgSlug: options.org, siteSlug: options.site }
3498
4071
  });
3499
4072
  if (!res.ok) {
3500
- const error = await res.json();
3501
- throw new Error(error.error || `API Error ${res.status}`);
4073
+ await handleApiError(res);
4074
+ }
4075
+ const { data: domains } = await res.json();
4076
+ if (options.json) {
4077
+ console.log(JSON.stringify(domains, null, 2));
4078
+ return;
3502
4079
  }
3503
- const domains = await res.json();
3504
4080
  if (domains.length === 0) {
3505
4081
  console.log("No custom domains configured.");
3506
4082
  console.log();
@@ -3517,62 +4093,58 @@ var domainListCommand = new Command2("list").description("List custom domains fo
3517
4093
  }
3518
4094
  console.log();
3519
4095
  } catch (err) {
3520
- const message = err instanceof Error ? err.message : "Unknown error";
3521
- console.error("Failed to list domains:", message);
4096
+ const message = err instanceof Error ? err.message : String(err);
4097
+ displayError({ code: "network_error", message: `Failed to list domains: ${message}` });
4098
+ process.exit(ExitCode.NETWORK_ERROR);
3522
4099
  }
3523
4100
  });
3524
4101
 
3525
4102
  // src/commands/domain/verify.ts
3526
- var domainVerifyCommand = new Command2("verify").description("Verify ownership of a custom domain").argument("<domain>", "Domain to verify (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainName, options) => {
4103
+ var domainVerifyCommand = new Command2("verify").description("Verify ownership of a custom domain").argument("<domain>", "Domain to verify (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--json", "Output as JSON").action(async (domainName, options) => {
3527
4104
  const token = loadToken();
3528
4105
  if (!token) {
3529
- console.log("Not logged in. Run: zerodeploy login");
3530
- return;
4106
+ handleAuthError();
3531
4107
  }
3532
4108
  try {
3533
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3534
- headers: {
3535
- Authorization: `Bearer ${token}`
3536
- }
4109
+ const client = getClient(token);
4110
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4111
+ param: { orgSlug: options.org, siteSlug: options.site }
3537
4112
  });
3538
4113
  if (!listRes.ok) {
3539
- const error = await listRes.json();
3540
- throw new Error(error.error || `API Error ${listRes.status}`);
4114
+ await handleApiError(listRes);
3541
4115
  }
3542
- const domains = await listRes.json();
4116
+ const { data: domains } = await listRes.json();
3543
4117
  const domain = domains.find((d) => d.domain === domainName);
3544
4118
  if (!domain) {
3545
- console.error(`
3546
- ❌ Domain not found: ${domainName}`);
3547
- console.log();
3548
- console.log("Add it first with:");
3549
- console.log(` zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`);
3550
- return;
4119
+ displayError({
4120
+ code: "not_found",
4121
+ message: `Domain not found: ${domainName}`,
4122
+ hint: `Add it first with:
4123
+ zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`
4124
+ });
4125
+ process.exit(ExitCode.NOT_FOUND);
3551
4126
  }
3552
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}/verify`, {
3553
- method: "POST",
3554
- headers: {
3555
- Authorization: `Bearer ${token}`,
3556
- "Content-Type": "application/json"
3557
- }
4127
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].verify.$post({
4128
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3558
4129
  });
3559
- const data = await res.json();
4130
+ const { data } = await res.json();
3560
4131
  if (!res.ok) {
3561
- console.error(`
3562
- Verification failed for ${domainName}`);
3563
- if (data.message) {
3564
- console.log();
3565
- console.log(data.message);
3566
- }
3567
- console.log();
3568
- console.log("Tips:");
3569
- console.log(" • DNS changes can take up to 48 hours to propagate");
3570
- console.log(` • Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}`);
3571
- console.log(" • Try again in a few minutes");
4132
+ displayError({
4133
+ code: "verification_failed",
4134
+ message: `Verification failed for ${domainName}`,
4135
+ hint: `Tips:
4136
+ - DNS changes can take up to 48 hours to propagate
4137
+ - Verify the TXT record is set correctly using: dig TXT _zerodeploy.${domainName}
4138
+ - Try again in a few minutes`
4139
+ });
4140
+ process.exit(ExitCode.VALIDATION_ERROR);
4141
+ }
4142
+ if (options.json) {
4143
+ console.log(JSON.stringify(data, null, 2));
3572
4144
  return;
3573
4145
  }
3574
4146
  console.log(`
3575
- Domain verified: ${data.domain}`);
4147
+ Domain verified: ${data.domain}`);
3576
4148
  console.log();
3577
4149
  if (data.cloudflare) {
3578
4150
  console.log(` Cloudflare SSL: ${data.cloudflare.status}`);
@@ -3581,8 +4153,8 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3581
4153
  const domainParts = data.domain.split(".");
3582
4154
  const isApexDomain = domainParts.length === 2;
3583
4155
  const target = `${data.siteSubdomain || "your-site"}.zerodeploy.app`;
3584
- console.log("\uD83D\uDCCB Final DNS Setup");
3585
- console.log("━━━━━━━━━━━━━━━━━━━");
4156
+ console.log("Final DNS Setup");
4157
+ console.log("---------------");
3586
4158
  console.log();
3587
4159
  if (isApexDomain) {
3588
4160
  console.log("For apex domains, DNS setup depends on your provider:");
@@ -3612,8 +4184,9 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3612
4184
  console.log(` https://${data.domain}`);
3613
4185
  console.log();
3614
4186
  } catch (err) {
3615
- const message = err instanceof Error ? err.message : "Unknown error";
3616
- console.error("Failed to verify domain:", message);
4187
+ const message = err instanceof Error ? err.message : String(err);
4188
+ displayError({ code: "network_error", message: `Failed to verify domain: ${message}` });
4189
+ process.exit(ExitCode.NETWORK_ERROR);
3617
4190
  }
3618
4191
  });
3619
4192
 
@@ -3621,44 +4194,39 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
3621
4194
  var domainRemoveCommand = new Command2("remove").description("Remove a custom domain from a site").argument("<domain>", "Domain to remove (e.g., www.example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (domainName, options) => {
3622
4195
  const token = loadToken();
3623
4196
  if (!token) {
3624
- console.log("Not logged in. Run: zerodeploy login");
3625
- return;
4197
+ handleAuthError();
3626
4198
  }
3627
4199
  try {
3628
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3629
- headers: {
3630
- Authorization: `Bearer ${token}`
3631
- }
4200
+ const client = getClient(token);
4201
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4202
+ param: { orgSlug: options.org, siteSlug: options.site }
3632
4203
  });
3633
4204
  if (!listRes.ok) {
3634
- const error = await listRes.json();
3635
- throw new Error(error.error || `API Error ${listRes.status}`);
4205
+ await handleApiError(listRes);
3636
4206
  }
3637
- const domains = await listRes.json();
4207
+ const { data: domains } = await listRes.json();
3638
4208
  const domain = domains.find((d) => d.domain === domainName);
3639
4209
  if (!domain) {
3640
- console.error(`
3641
- ❌ Domain not found: ${domainName}`);
3642
- console.log();
3643
- console.log("List domains with:");
3644
- console.log(` zerodeploy domain list --org ${options.org} --site ${options.site}`);
3645
- return;
4210
+ displayError({
4211
+ code: "not_found",
4212
+ message: `Domain not found: ${domainName}`,
4213
+ hint: `List domains with:
4214
+ zerodeploy domain list --org ${options.org} --site ${options.site}`
4215
+ });
4216
+ process.exit(ExitCode.NOT_FOUND);
3646
4217
  }
3647
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${domain.id}`, {
3648
- method: "DELETE",
3649
- headers: {
3650
- Authorization: `Bearer ${token}`
3651
- }
4218
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].$delete({
4219
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
3652
4220
  });
3653
4221
  if (!res.ok) {
3654
- const error = await res.json();
3655
- throw new Error(error.error || `API Error ${res.status}`);
4222
+ await handleApiError(res);
3656
4223
  }
3657
4224
  const data = await res.json();
3658
- console.log(`✅ ${data.message}`);
4225
+ console.log(data.message);
3659
4226
  } catch (err) {
3660
- const message = err instanceof Error ? err.message : "Unknown error";
3661
- console.error("Failed to remove domain:", message);
4227
+ const message = err instanceof Error ? err.message : String(err);
4228
+ displayError({ code: "network_error", message: `Failed to remove domain: ${message}` });
4229
+ process.exit(ExitCode.NETWORK_ERROR);
3662
4230
  }
3663
4231
  });
3664
4232
 
@@ -3668,71 +4236,311 @@ function formatRedirectMode(mode) {
3668
4236
  case "none":
3669
4237
  return "No redirect";
3670
4238
  case "www_to_apex":
3671
- return "www apex (e.g., www.example.com example.com)";
4239
+ return "www -> apex (e.g., www.example.com -> example.com)";
3672
4240
  case "apex_to_www":
3673
- return "apex www (e.g., example.com www.example.com)";
4241
+ return "apex -> www (e.g., example.com -> www.example.com)";
3674
4242
  default:
3675
4243
  return mode;
3676
4244
  }
3677
4245
  }
3678
- var domainRedirectCommand = new Command2("redirect").description("Set redirect mode for a custom domain").argument("<domain>", "Domain name (e.g., example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").requiredOption("--mode <mode>", "Redirect mode: none, www_to_apex, or apex_to_www").action(async (domain, options) => {
4246
+ var domainRedirectCommand = new Command2("redirect").description("Set redirect mode for a custom domain").argument("<domain>", "Domain name (e.g., example.com)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").requiredOption("--mode <mode>", "Redirect mode: none, www_to_apex, or apex_to_www").option("--json", "Output as JSON").action(async (domain, options) => {
3679
4247
  const token = loadToken();
3680
4248
  if (!token) {
3681
- console.log("Not logged in. Run: zerodeploy login");
3682
- return;
4249
+ handleAuthError();
3683
4250
  }
3684
4251
  const validModes = ["none", "www_to_apex", "apex_to_www"];
3685
4252
  if (!validModes.includes(options.mode)) {
3686
- console.error(`Invalid mode: ${options.mode}`);
3687
- console.error("Valid modes: none, www_to_apex, apex_to_www");
3688
- return;
4253
+ displayError({
4254
+ code: "validation_error",
4255
+ message: `Invalid mode: ${options.mode}`,
4256
+ hint: "Valid modes: none, www_to_apex, apex_to_www"
4257
+ });
4258
+ process.exit(ExitCode.VALIDATION_ERROR);
3689
4259
  }
3690
4260
  try {
3691
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains`, {
3692
- headers: {
3693
- Authorization: `Bearer ${token}`
3694
- }
4261
+ const client = getClient(token);
4262
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
4263
+ param: { orgSlug: options.org, siteSlug: options.site }
3695
4264
  });
3696
4265
  if (!listRes.ok) {
3697
- const error = await listRes.json();
3698
- throw new Error(error.error || `API Error ${listRes.status}`);
4266
+ await handleApiError(listRes);
3699
4267
  }
3700
- const domains = await listRes.json();
4268
+ const { data: domains } = await listRes.json();
3701
4269
  const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
3702
4270
  if (!targetDomain) {
3703
- console.error(`Domain not found: ${domain}`);
3704
- console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
3705
- return;
4271
+ displayError({
4272
+ code: "not_found",
4273
+ message: `Domain not found: ${domain}`,
4274
+ hint: `Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`
4275
+ });
4276
+ process.exit(ExitCode.NOT_FOUND);
3706
4277
  }
3707
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${targetDomain.id}/redirect`, {
3708
- method: "PATCH",
3709
- headers: {
3710
- Authorization: `Bearer ${token}`,
3711
- "Content-Type": "application/json"
3712
- },
3713
- body: JSON.stringify({ redirectMode: options.mode })
4278
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].redirect.$patch({
4279
+ param: { orgSlug: options.org, siteSlug: options.site, domainId: targetDomain.id },
4280
+ json: { redirectMode: options.mode }
3714
4281
  });
3715
4282
  if (!res.ok) {
3716
- const error = await res.json();
3717
- throw new Error(error.error || `API Error ${res.status}`);
4283
+ await handleApiError(res);
4284
+ }
4285
+ const { data } = await res.json();
4286
+ if (options.json) {
4287
+ console.log(JSON.stringify(data, null, 2));
4288
+ return;
3718
4289
  }
3719
- const data = await res.json();
3720
4290
  console.log(`
3721
- Redirect mode updated for ${data.domain}`);
4291
+ Redirect mode updated for ${data.domain}`);
3722
4292
  console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
3723
4293
  console.log();
3724
4294
  } catch (err) {
3725
- const message = err instanceof Error ? err.message : "Unknown error";
3726
- console.error("Failed to update redirect mode:", message);
4295
+ const message = err instanceof Error ? err.message : String(err);
4296
+ displayError({ code: "network_error", message: `Failed to update redirect mode: ${message}` });
4297
+ process.exit(ExitCode.NETWORK_ERROR);
3727
4298
  }
3728
4299
  });
3729
4300
 
3730
4301
  // src/commands/domain/index.ts
3731
4302
  var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
3732
4303
 
4304
+ // src/commands/form/list.ts
4305
+ var formListCommand = new Command2("list").description("List forms for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--json", "Output as JSON").action(async (options) => {
4306
+ const token = loadToken();
4307
+ if (!token) {
4308
+ handleAuthError();
4309
+ }
4310
+ try {
4311
+ const client = getClient(token);
4312
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms.$get({
4313
+ param: { orgSlug: options.org, siteSlug: options.site }
4314
+ });
4315
+ if (!res.ok) {
4316
+ await handleApiError(res);
4317
+ }
4318
+ const { data: forms } = await res.json();
4319
+ if (options.json) {
4320
+ console.log(JSON.stringify(forms, null, 2));
4321
+ return;
4322
+ }
4323
+ if (forms.length === 0) {
4324
+ console.log("No forms found.");
4325
+ console.log();
4326
+ console.log("Forms are created automatically when visitors submit to /_forms/<name>");
4327
+ return;
4328
+ }
4329
+ console.log("Forms:");
4330
+ console.log();
4331
+ console.log(" NAME SUBMISSIONS CREATED");
4332
+ console.log(" " + "-".repeat(60));
4333
+ for (const f of forms) {
4334
+ const created = new Date(f.created_at).toLocaleDateString();
4335
+ console.log(` ${f.name.padEnd(30)} ${String(f.submission_count).padStart(11)} ${created}`);
4336
+ }
4337
+ console.log();
4338
+ console.log("Export submissions with:");
4339
+ console.log(` zerodeploy form export <name> --org ${options.org} --site ${options.site}`);
4340
+ } catch (err) {
4341
+ const message = err instanceof Error ? err.message : String(err);
4342
+ displayError({ code: "network_error", message: `Failed to list forms: ${message}` });
4343
+ process.exit(ExitCode.NETWORK_ERROR);
4344
+ }
4345
+ });
4346
+
4347
+ // src/commands/form/submissions.ts
4348
+ var formSubmissionsCommand = new Command2("submissions").description("View recent form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--limit <n>", "Number of submissions to show", "10").option("--offset <n>", "Offset for pagination", "0").option("--json", "Output as JSON").action(async (name, options) => {
4349
+ const token = loadToken();
4350
+ if (!token) {
4351
+ handleAuthError();
4352
+ }
4353
+ try {
4354
+ const client = getClient(token);
4355
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].submissions.$get({
4356
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name },
4357
+ query: { limit: options.limit, offset: options.offset }
4358
+ });
4359
+ if (!res.ok) {
4360
+ await handleApiError(res);
4361
+ }
4362
+ const data = await res.json();
4363
+ const { form, submissions, total, limit, offset } = data;
4364
+ if (options.json) {
4365
+ console.log(JSON.stringify(data, null, 2));
4366
+ return;
4367
+ }
4368
+ if (submissions.length === 0) {
4369
+ console.log(`No submissions found for form "${name}".`);
4370
+ return;
4371
+ }
4372
+ console.log(`Form: ${form.name}`);
4373
+ console.log(`Total submissions: ${total}`);
4374
+ if (form.notification_email) {
4375
+ console.log(`Notifications: ${form.notification_email}`);
4376
+ }
4377
+ console.log();
4378
+ if (total > limit) {
4379
+ const start = offset + 1;
4380
+ const end = Math.min(offset + submissions.length, total);
4381
+ console.log(`Showing ${start}-${end} of ${total}`);
4382
+ console.log();
4383
+ }
4384
+ for (const sub of submissions) {
4385
+ const date = new Date(sub.created_at).toLocaleString();
4386
+ const shortId = sub.id.slice(0, 8);
4387
+ console.log(` ${shortId} ${date}`);
4388
+ const entries = Object.entries(sub.data);
4389
+ for (const [key, value] of entries) {
4390
+ const displayValue = typeof value === "string" ? value : JSON.stringify(value);
4391
+ const truncated = displayValue.length > 60 ? displayValue.slice(0, 57) + "..." : displayValue;
4392
+ console.log(` ${key}: ${truncated}`);
4393
+ }
4394
+ const meta = [];
4395
+ if (sub.ip_address)
4396
+ meta.push(`IP: ${sub.ip_address}`);
4397
+ if (sub.referrer)
4398
+ meta.push(`from: ${sub.referrer}`);
4399
+ if (meta.length > 0) {
4400
+ console.log(` [${meta.join(", ")}]`);
4401
+ }
4402
+ console.log();
4403
+ }
4404
+ if (total > offset + submissions.length) {
4405
+ const nextOffset = offset + parseInt(options.limit);
4406
+ console.log(`View more with: --offset ${nextOffset}`);
4407
+ }
4408
+ console.log("Export all to CSV with:");
4409
+ console.log(` zerodeploy form export ${name} --org ${options.org} --site ${options.site}`);
4410
+ } catch (err) {
4411
+ const message = err instanceof Error ? err.message : String(err);
4412
+ displayError({ code: "network_error", message: `Failed to get submissions: ${message}` });
4413
+ process.exit(ExitCode.NETWORK_ERROR);
4414
+ }
4415
+ });
4416
+
4417
+ // src/commands/form/export.ts
4418
+ import { writeFile } from "fs/promises";
4419
+ import { resolve } from "path";
4420
+ var formExportCommand = new Command2("export").description("Export form submissions as CSV").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("-o, --output <file>", "Output file path (default: <name>-submissions.csv)").action(async (name, options) => {
4421
+ const token = loadToken();
4422
+ if (!token) {
4423
+ handleAuthError();
4424
+ }
4425
+ try {
4426
+ const client = getClient(token);
4427
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].export.$get({
4428
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name }
4429
+ });
4430
+ if (res.status === 204) {
4431
+ console.log("No submissions to export.");
4432
+ return;
4433
+ }
4434
+ if (!res.ok) {
4435
+ await handleApiError(res);
4436
+ }
4437
+ const csv = await res.text();
4438
+ const outputPath = options.output || `${name}-submissions.csv`;
4439
+ const fullPath = resolve(process.cwd(), outputPath);
4440
+ await writeFile(fullPath, csv, "utf-8");
4441
+ const lineCount = csv.split(`
4442
+ `).length - 1;
4443
+ console.log(`Exported ${lineCount} submissions to ${outputPath}`);
4444
+ } catch (err) {
4445
+ const message = err instanceof Error ? err.message : String(err);
4446
+ displayError({ code: "network_error", message: `Failed to export form: ${message}` });
4447
+ process.exit(ExitCode.NETWORK_ERROR);
4448
+ }
4449
+ });
4450
+
4451
+ // src/commands/form/delete.ts
4452
+ import * as readline3 from "readline";
4453
+ function prompt3(question) {
4454
+ const rl = readline3.createInterface({
4455
+ input: process.stdin,
4456
+ output: process.stdout
4457
+ });
4458
+ return new Promise((resolve2) => {
4459
+ rl.question(question, (answer) => {
4460
+ rl.close();
4461
+ resolve2(answer);
4462
+ });
4463
+ });
4464
+ }
4465
+ var formDeleteCommand = new Command2("delete").description("Delete a form and all its submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--force", "Skip confirmation prompt").action(async (name, options) => {
4466
+ const token = loadToken();
4467
+ if (!token) {
4468
+ handleAuthError();
4469
+ }
4470
+ if (!options.force) {
4471
+ const answer = await prompt3(`Are you sure you want to delete form "${name}" and all its submissions? This cannot be undone. (y/N) `);
4472
+ if (answer.toLowerCase() !== "y") {
4473
+ console.log("Cancelled.");
4474
+ return;
4475
+ }
4476
+ }
4477
+ try {
4478
+ const client = getClient(token);
4479
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$delete({
4480
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name }
4481
+ });
4482
+ if (!res.ok) {
4483
+ await handleApiError(res);
4484
+ }
4485
+ const result = await res.json();
4486
+ console.log(`${result.message}`);
4487
+ } catch (err) {
4488
+ const message = err instanceof Error ? err.message : String(err);
4489
+ displayError({ code: "network_error", message: `Failed to delete form: ${message}` });
4490
+ process.exit(ExitCode.NETWORK_ERROR);
4491
+ }
4492
+ });
4493
+
4494
+ // src/commands/form/notify.ts
4495
+ var formNotifyCommand = new Command2("notify").description("Configure email notifications for form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--email <email>", "Email address to receive notifications").option("--disable", "Disable email notifications").action(async (name, options) => {
4496
+ const token = loadToken();
4497
+ if (!token) {
4498
+ handleAuthError();
4499
+ }
4500
+ if (options.email && options.disable) {
4501
+ displayError({ code: "validation_error", message: "Cannot use both --email and --disable options" });
4502
+ process.exit(ExitCode.VALIDATION_ERROR);
4503
+ }
4504
+ if (!options.email && !options.disable) {
4505
+ displayError({
4506
+ code: "validation_error",
4507
+ message: "Please specify --email <email> or --disable",
4508
+ hint: `Examples:
4509
+ zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --email alerts@example.com
4510
+ zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --disable`
4511
+ });
4512
+ process.exit(ExitCode.VALIDATION_ERROR);
4513
+ }
4514
+ try {
4515
+ const client = getClient(token);
4516
+ const notificationEmail = options.disable ? null : options.email;
4517
+ const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$patch({
4518
+ param: { orgSlug: options.org, siteSlug: options.site, formName: name },
4519
+ json: { notification_email: notificationEmail }
4520
+ });
4521
+ if (!res.ok) {
4522
+ await handleApiError(res);
4523
+ }
4524
+ const { form } = await res.json();
4525
+ if (form.notification_email) {
4526
+ console.log(`Email notifications enabled for form "${name}"`);
4527
+ console.log(` Notifications will be sent to: ${form.notification_email}`);
4528
+ } else {
4529
+ console.log(`Email notifications disabled for form "${name}"`);
4530
+ }
4531
+ } catch (err) {
4532
+ const message = err instanceof Error ? err.message : String(err);
4533
+ displayError({ code: "network_error", message: `Failed to update form notifications: ${message}` });
4534
+ process.exit(ExitCode.NETWORK_ERROR);
4535
+ }
4536
+ });
4537
+
4538
+ // src/commands/form/index.ts
4539
+ var formCommand = new Command2("form").description("Manage form submissions").addCommand(formListCommand).addCommand(formSubmissionsCommand).addCommand(formExportCommand).addCommand(formDeleteCommand).addCommand(formNotifyCommand);
4540
+
3733
4541
  // src/commands/deploy/index.ts
3734
- import { resolve as resolve3, basename } from "node:path";
3735
- import { stat as stat2, writeFile } from "node:fs/promises";
4542
+ import { resolve as resolve4, basename } from "node:path";
4543
+ import { stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
3736
4544
  import { spawn } from "node:child_process";
3737
4545
 
3738
4546
  // src/utils/files.ts
@@ -3772,7 +4580,7 @@ async function scanDirectory(rootDir) {
3772
4580
  await scan(rootDir);
3773
4581
  return files;
3774
4582
  }
3775
- function formatBytes(bytes) {
4583
+ function formatBytes2(bytes) {
3776
4584
  if (bytes === 0)
3777
4585
  return "0 B";
3778
4586
  const k = 1024;
@@ -3783,7 +4591,7 @@ function formatBytes(bytes) {
3783
4591
 
3784
4592
  // src/utils/framework.ts
3785
4593
  import { readFile as readFile2 } from "node:fs/promises";
3786
- import { resolve } from "node:path";
4594
+ import { resolve as resolve2 } from "node:path";
3787
4595
  var FRAMEWORKS = [
3788
4596
  {
3789
4597
  name: "Vite",
@@ -3857,7 +4665,7 @@ function hasDep(pkg, dep) {
3857
4665
  }
3858
4666
  async function detectFramework(cwd) {
3859
4667
  try {
3860
- const pkgPath = resolve(cwd, "package.json");
4668
+ const pkgPath = resolve2(cwd, "package.json");
3861
4669
  const pkgContent = await readFile2(pkgPath, "utf-8");
3862
4670
  const pkg = JSON.parse(pkgContent);
3863
4671
  for (const framework of FRAMEWORKS) {
@@ -4400,9 +5208,12 @@ function padToBlock(data) {
4400
5208
  padded.set(data);
4401
5209
  return padded;
4402
5210
  }
4403
- async function createTarArchive(files) {
5211
+ async function createTarArchive(files, onProgress) {
4404
5212
  const parts = [];
4405
- for (const file of files) {
5213
+ const total = files.length;
5214
+ for (let i2 = 0;i2 < files.length; i2++) {
5215
+ const file = files[i2];
5216
+ onProgress?.(i2 + 1, total, file.path);
4406
5217
  const content = await readFile3(file.absolutePath);
4407
5218
  const header = createTarHeader(file.path, content.length);
4408
5219
  const paddedContent = padToBlock(new Uint8Array(content));
@@ -4419,8 +5230,8 @@ async function createTarArchive(files) {
4419
5230
  }
4420
5231
  return tar;
4421
5232
  }
4422
- async function createTarGz(files) {
4423
- const tar = await createTarArchive(files);
5233
+ async function createTarGz(files, onProgress) {
5234
+ const tar = await createTarArchive(files, onProgress);
4424
5235
  return gzipSync(tar, { level: 6 });
4425
5236
  }
4426
5237
  function formatCompression(original, compressed) {
@@ -4430,10 +5241,10 @@ function formatCompression(original, compressed) {
4430
5241
 
4431
5242
  // src/utils/project-config.ts
4432
5243
  import { existsSync, readFileSync } from "node:fs";
4433
- import { resolve as resolve2 } from "node:path";
5244
+ import { resolve as resolve3 } from "node:path";
4434
5245
  var CONFIG_FILENAME = "zerodeploy.json";
4435
5246
  function loadProjectConfig(cwd = process.cwd()) {
4436
- const configPath = resolve2(cwd, CONFIG_FILENAME);
5247
+ const configPath = resolve3(cwd, CONFIG_FILENAME);
4437
5248
  if (!existsSync(configPath)) {
4438
5249
  return {};
4439
5250
  }
@@ -4447,32 +5258,242 @@ function loadProjectConfig(cwd = process.cwd()) {
4447
5258
  }
4448
5259
  }
4449
5260
  function getConfigPath(cwd = process.cwd()) {
4450
- return resolve2(cwd, CONFIG_FILENAME);
5261
+ return resolve3(cwd, CONFIG_FILENAME);
4451
5262
  }
4452
5263
 
4453
5264
  // src/utils/prompt.ts
4454
- import * as readline3 from "node:readline";
5265
+ import * as readline4 from "node:readline";
4455
5266
  async function confirm(message, defaultValue = true) {
4456
- const rl = readline3.createInterface({
5267
+ const rl = readline4.createInterface({
4457
5268
  input: process.stdin,
4458
5269
  output: process.stdout
4459
5270
  });
4460
5271
  const hint = defaultValue ? "[Y/n]" : "[y/N]";
4461
- return new Promise((resolve3) => {
5272
+ return new Promise((resolve4) => {
4462
5273
  rl.question(`${message} ${hint} `, (answer) => {
4463
5274
  rl.close();
4464
5275
  const normalized = answer.trim().toLowerCase();
4465
5276
  if (normalized === "") {
4466
- resolve3(defaultValue);
5277
+ resolve4(defaultValue);
4467
5278
  } else if (normalized === "y" || normalized === "yes") {
4468
- resolve3(true);
5279
+ resolve4(true);
4469
5280
  } else {
4470
- resolve3(false);
5281
+ resolve4(false);
4471
5282
  }
4472
5283
  });
4473
5284
  });
4474
5285
  }
4475
5286
 
5287
+ // src/utils/progress.ts
5288
+ var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5289
+ var SPINNER_INTERVAL = 80;
5290
+ var isTTY = process.stdout.isTTY;
5291
+ function clearLine() {
5292
+ if (isTTY) {
5293
+ process.stdout.write("\r\x1B[K");
5294
+ }
5295
+ }
5296
+ function createSpinner(message) {
5297
+ let frameIndex = 0;
5298
+ let interval = null;
5299
+ let currentMessage = message;
5300
+ function render() {
5301
+ if (!isTTY)
5302
+ return;
5303
+ const frame = SPINNER_FRAMES[frameIndex];
5304
+ clearLine();
5305
+ process.stdout.write(`${frame} ${currentMessage}`);
5306
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
5307
+ }
5308
+ return {
5309
+ start() {
5310
+ if (!isTTY) {
5311
+ console.log(currentMessage);
5312
+ return;
5313
+ }
5314
+ render();
5315
+ interval = setInterval(render, SPINNER_INTERVAL);
5316
+ },
5317
+ stop(finalMessage) {
5318
+ if (interval) {
5319
+ clearInterval(interval);
5320
+ interval = null;
5321
+ }
5322
+ clearLine();
5323
+ if (finalMessage) {
5324
+ console.log(finalMessage);
5325
+ }
5326
+ },
5327
+ update(message2) {
5328
+ currentMessage = message2;
5329
+ if (!isTTY) {
5330
+ console.log(message2);
5331
+ }
5332
+ },
5333
+ succeed(message2) {
5334
+ if (interval) {
5335
+ clearInterval(interval);
5336
+ interval = null;
5337
+ }
5338
+ clearLine();
5339
+ console.log(`✓ ${message2}`);
5340
+ },
5341
+ fail(message2) {
5342
+ if (interval) {
5343
+ clearInterval(interval);
5344
+ interval = null;
5345
+ }
5346
+ clearLine();
5347
+ console.log(`✗ ${message2}`);
5348
+ },
5349
+ warn(message2) {
5350
+ if (interval) {
5351
+ clearInterval(interval);
5352
+ interval = null;
5353
+ }
5354
+ clearLine();
5355
+ console.log(`⚠ ${message2}`);
5356
+ }
5357
+ };
5358
+ }
5359
+ function createProgressBar(options) {
5360
+ const { total, label, width = 20 } = options;
5361
+ let started = false;
5362
+ function render(current, suffix) {
5363
+ const percent = Math.min(current / total, 1);
5364
+ const filled = Math.round(percent * width);
5365
+ const empty = width - filled;
5366
+ const bar = "█".repeat(filled) + "░".repeat(empty);
5367
+ const countStr = `${current}/${total}`;
5368
+ const suffixStr = suffix ? ` ${suffix}` : "";
5369
+ if (isTTY) {
5370
+ clearLine();
5371
+ process.stdout.write(`${label} [${bar}] ${countStr}${suffixStr}`);
5372
+ }
5373
+ }
5374
+ return {
5375
+ update(current, suffix) {
5376
+ if (!isTTY) {
5377
+ if (!started) {
5378
+ console.log(`${label}... ${total} files`);
5379
+ started = true;
5380
+ }
5381
+ return;
5382
+ }
5383
+ render(current, suffix);
5384
+ },
5385
+ done(message) {
5386
+ clearLine();
5387
+ if (message) {
5388
+ console.log(message);
5389
+ }
5390
+ }
5391
+ };
5392
+ }
5393
+
5394
+ // src/utils/ci.ts
5395
+ function detectCI() {
5396
+ if (process.env.GITHUB_ACTIONS === "true") {
5397
+ return {
5398
+ name: "github-actions",
5399
+ prNumber: extractGitHubPRNumber(),
5400
+ prTitle: null,
5401
+ commitSha: process.env.GITHUB_SHA || null,
5402
+ branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || null,
5403
+ commitMessage: null
5404
+ };
5405
+ }
5406
+ if (process.env.CIRCLECI === "true") {
5407
+ return {
5408
+ name: "circleci",
5409
+ prNumber: extractCircleCIPRNumber(),
5410
+ prTitle: null,
5411
+ commitSha: process.env.CIRCLE_SHA1 || null,
5412
+ branch: process.env.CIRCLE_BRANCH || null,
5413
+ commitMessage: null
5414
+ };
5415
+ }
5416
+ if (process.env.GITLAB_CI === "true") {
5417
+ return {
5418
+ name: "gitlab-ci",
5419
+ prNumber: process.env.CI_MERGE_REQUEST_IID ? parseInt(process.env.CI_MERGE_REQUEST_IID, 10) : null,
5420
+ prTitle: process.env.CI_MERGE_REQUEST_TITLE || null,
5421
+ commitSha: process.env.CI_COMMIT_SHA || null,
5422
+ branch: process.env.CI_COMMIT_REF_NAME || null,
5423
+ commitMessage: process.env.CI_COMMIT_MESSAGE || null
5424
+ };
5425
+ }
5426
+ if (process.env.BITBUCKET_BUILD_NUMBER) {
5427
+ return {
5428
+ name: "bitbucket-pipelines",
5429
+ prNumber: process.env.BITBUCKET_PR_ID ? parseInt(process.env.BITBUCKET_PR_ID, 10) : null,
5430
+ prTitle: null,
5431
+ commitSha: process.env.BITBUCKET_COMMIT || null,
5432
+ branch: process.env.BITBUCKET_BRANCH || null,
5433
+ commitMessage: process.env.BITBUCKET_COMMIT_MESSAGE || null
5434
+ };
5435
+ }
5436
+ if (process.env.TF_BUILD === "True") {
5437
+ return {
5438
+ name: "azure-pipelines",
5439
+ prNumber: process.env.SYSTEM_PULLREQUEST_PULLREQUESTID ? parseInt(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID, 10) : null,
5440
+ prTitle: null,
5441
+ commitSha: process.env.BUILD_SOURCEVERSION || null,
5442
+ branch: process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH || process.env.BUILD_SOURCEBRANCHNAME || null,
5443
+ commitMessage: process.env.BUILD_SOURCEVERSIONMESSAGE || null
5444
+ };
5445
+ }
5446
+ if (process.env.TRAVIS === "true") {
5447
+ const travisPR = process.env.TRAVIS_PULL_REQUEST;
5448
+ return {
5449
+ name: "travis-ci",
5450
+ prNumber: travisPR && travisPR !== "false" ? parseInt(travisPR, 10) : null,
5451
+ prTitle: null,
5452
+ commitSha: process.env.TRAVIS_COMMIT || null,
5453
+ branch: process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH || null,
5454
+ commitMessage: process.env.TRAVIS_COMMIT_MESSAGE || null
5455
+ };
5456
+ }
5457
+ if (process.env.JENKINS_URL) {
5458
+ return {
5459
+ name: "jenkins",
5460
+ prNumber: extractJenkinsPRNumber(),
5461
+ prTitle: null,
5462
+ commitSha: process.env.GIT_COMMIT || null,
5463
+ branch: process.env.CHANGE_BRANCH || process.env.GIT_BRANCH || null,
5464
+ commitMessage: null
5465
+ };
5466
+ }
5467
+ return null;
5468
+ }
5469
+ function extractGitHubPRNumber() {
5470
+ const ref = process.env.GITHUB_REF;
5471
+ if (process.env.GITHUB_EVENT_NAME === "pull_request" && ref) {
5472
+ const match = ref.match(/refs\/pull\/(\d+)/);
5473
+ if (match?.[1])
5474
+ return parseInt(match[1], 10);
5475
+ }
5476
+ return null;
5477
+ }
5478
+ function extractCircleCIPRNumber() {
5479
+ const url = process.env.CIRCLE_PULL_REQUEST;
5480
+ if (url) {
5481
+ const match = url.match(/\/pull\/(\d+)/);
5482
+ if (match?.[1])
5483
+ return parseInt(match[1], 10);
5484
+ }
5485
+ return null;
5486
+ }
5487
+ function extractJenkinsPRNumber() {
5488
+ const changeId = process.env.CHANGE_ID;
5489
+ if (changeId) {
5490
+ const num = parseInt(changeId, 10);
5491
+ if (!isNaN(num))
5492
+ return num;
5493
+ }
5494
+ return null;
5495
+ }
5496
+
4476
5497
  // src/commands/deploy/list.ts
4477
5498
  var deployListCommand = new Command2("list").description("List deployments for a site").argument("<siteSlug>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--limit <number>", "Number of deployments to show", "10").action(async (siteSlug, options) => {
4478
5499
  const token = loadToken();
@@ -4490,7 +5511,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4490
5511
  console.error(`❌ ${error.error}`);
4491
5512
  return;
4492
5513
  }
4493
- const deployments = await res.json();
5514
+ const { data: deployments } = await res.json();
4494
5515
  const limit = parseInt(options.limit, 10);
4495
5516
  const shown = deployments.slice(0, limit);
4496
5517
  if (shown.length === 0) {
@@ -4501,7 +5522,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
4501
5522
  Deployments for ${options.org}/${siteSlug}:
4502
5523
  `);
4503
5524
  for (const d of shown) {
4504
- const current = d.isCurrent ? " ← current" : "";
5525
+ const current = d.is_current ? " ← current" : "";
4505
5526
  const status = formatStatus2(d.status);
4506
5527
  const date = new Date(d.created_at).toLocaleString();
4507
5528
  console.log(` ${d.id.slice(0, 8)} ${status} ${date}${current}`);
@@ -4545,8 +5566,8 @@ var deployRollbackCommand = new Command2("rollback").description("Rollback to a
4545
5566
  }
4546
5567
  const result = await res.json();
4547
5568
  console.log(`✅ ${result.message}`);
4548
- console.log(` Deployment: ${result.deployment.id}`);
4549
- console.log(` URL: ${result.deployment.url}`);
5569
+ console.log(` Deployment: ${result.data.deployment.id}`);
5570
+ console.log(` URL: ${result.data.deployment.url}`);
4550
5571
  } catch (err) {
4551
5572
  console.error("Failed to rollback:", err.message);
4552
5573
  }
@@ -4571,8 +5592,8 @@ var deployPromoteCommand = new Command2("promote").description("Promote a previe
4571
5592
  }
4572
5593
  const result = await res.json();
4573
5594
  console.log("Deployment promoted to production!");
4574
- console.log(` Deployment: ${result.deployment.id}`);
4575
- console.log(` URL: ${result.deployment.url}`);
5595
+ console.log(` Deployment: ${result.data.deployment.id}`);
5596
+ console.log(` URL: ${result.data.deployment.url}`);
4576
5597
  } catch (err) {
4577
5598
  const message = err instanceof Error ? err.message : "Unknown error";
4578
5599
  console.error("Failed to promote deployment:", message);
@@ -4583,21 +5604,82 @@ var deployPromoteCommand = new Command2("promote").description("Promote a previe
4583
5604
  function slugify(input) {
4584
5605
  return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
4585
5606
  }
5607
+ async function verifyDeployment(url, maxRetries = 3, delayMs = 2000) {
5608
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
5609
+ try {
5610
+ const controller = new AbortController;
5611
+ const timeout = setTimeout(() => controller.abort(), 1e4);
5612
+ const res = await fetch(url, {
5613
+ method: "GET",
5614
+ signal: controller.signal
5615
+ });
5616
+ clearTimeout(timeout);
5617
+ if (res.ok) {
5618
+ return { success: true, status: res.status };
5619
+ }
5620
+ if (attempt < maxRetries) {
5621
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
5622
+ continue;
5623
+ }
5624
+ return { success: false, status: res.status };
5625
+ } catch (err) {
5626
+ if (attempt < maxRetries) {
5627
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
5628
+ continue;
5629
+ }
5630
+ const error = err instanceof Error ? err.message : "Unknown error";
5631
+ return { success: false, error };
5632
+ }
5633
+ }
5634
+ return { success: false, error: "Max retries exceeded" };
5635
+ }
5636
+ async function autoRollback(token, orgSlug, siteSlug, currentDeploymentId) {
5637
+ try {
5638
+ const client = getClient(token);
5639
+ const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
5640
+ param: { orgSlug, siteSlug }
5641
+ });
5642
+ if (!listRes.ok) {
5643
+ return { success: false, error: "Failed to fetch deployments" };
5644
+ }
5645
+ const { data: deployments } = await listRes.json();
5646
+ const previousDeployment = deployments.find((d) => d.id !== currentDeploymentId && d.status === "ready");
5647
+ if (!previousDeployment) {
5648
+ return { success: false, error: "No previous deployment to rollback to" };
5649
+ }
5650
+ const rollbackRes = await client.deployments[":id"].rollback.$post({
5651
+ param: { id: previousDeployment.id }
5652
+ });
5653
+ if (!rollbackRes.ok) {
5654
+ return { success: false, error: "Rollback request failed" };
5655
+ }
5656
+ return { success: true, deploymentId: previousDeployment.id };
5657
+ } catch (err) {
5658
+ const error = err instanceof Error ? err.message : "Unknown error";
5659
+ return { success: false, error };
5660
+ }
5661
+ }
4586
5662
  async function runCommand(command, cwd) {
4587
5663
  return new Promise((promiseResolve) => {
4588
- const [cmd, ...args] = command.split(" ");
5664
+ const parts = command.split(" ");
5665
+ const cmd = parts[0];
5666
+ if (!cmd) {
5667
+ promiseResolve({ success: false, output: "Empty command" });
5668
+ return;
5669
+ }
5670
+ const args = parts.slice(1);
4589
5671
  const proc = spawn(cmd, args, {
4590
5672
  cwd,
4591
5673
  shell: true,
4592
5674
  stdio: ["inherit", "pipe", "pipe"]
4593
5675
  });
4594
5676
  let output = "";
4595
- proc.stdout?.on("data", (data) => {
5677
+ proc.stdout.on("data", (data) => {
4596
5678
  const text = data.toString();
4597
5679
  output += text;
4598
5680
  process.stdout.write(text);
4599
5681
  });
4600
- proc.stderr?.on("data", (data) => {
5682
+ proc.stderr.on("data", (data) => {
4601
5683
  const text = data.toString();
4602
5684
  output += text;
4603
5685
  process.stderr.write(text);
@@ -4613,12 +5695,12 @@ async function runCommand(command, cwd) {
4613
5695
  async function findBuildDirectory(cwd) {
4614
5696
  const candidates = ["dist", "build", "out", "public", "."];
4615
5697
  for (const dir of candidates) {
4616
- const fullPath = resolve3(cwd, dir);
5698
+ const fullPath = resolve4(cwd, dir);
4617
5699
  try {
4618
5700
  const stats = await stat2(fullPath);
4619
5701
  if (stats.isDirectory()) {
4620
5702
  try {
4621
- await stat2(resolve3(fullPath, "index.html"));
5703
+ await stat2(resolve4(fullPath, "index.html"));
4622
5704
  return fullPath;
4623
5705
  } catch {
4624
5706
  if (dir !== ".")
@@ -4630,22 +5712,25 @@ async function findBuildDirectory(cwd) {
4630
5712
  }
4631
5713
  return null;
4632
5714
  }
4633
- async function uploadArchive(token, uploadUrl, archive) {
4634
- const res = await fetch(uploadUrl, {
5715
+ async function uploadArchive(token, uploadUrl, archive, onRetry) {
5716
+ const res = await fetchWithRetry(uploadUrl, {
4635
5717
  method: "POST",
4636
5718
  headers: {
4637
5719
  "Content-Type": "application/gzip",
4638
5720
  Authorization: `Bearer ${token}`
4639
5721
  },
4640
5722
  body: archive
5723
+ }, {
5724
+ maxRetries: 3,
5725
+ onRetry
4641
5726
  });
4642
5727
  return res.ok;
4643
5728
  }
4644
- var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
5729
+ var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--prod", "Force production deploy (override auto-preview for PRs)").option("--no-verify", "Skip deployment verification").option("--no-auto-rollback", "Disable automatic rollback on verification failure").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
4645
5730
  const cwd = process.cwd();
4646
5731
  const token = loadToken();
4647
5732
  if (!token) {
4648
- console.log("Not logged in. Run: zerodeploy login");
5733
+ displayAuthError();
4649
5734
  return;
4650
5735
  }
4651
5736
  const config = loadProjectConfig(cwd);
@@ -4687,12 +5772,12 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
4687
5772
  console.log(`Error: ${error.error || "Failed to create site"}`);
4688
5773
  return;
4689
5774
  }
4690
- const site = await createRes.json();
5775
+ const { data: site } = await createRes.json();
4691
5776
  siteSlug = site.slug;
4692
5777
  console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
4693
5778
  const configPath = getConfigPath(cwd);
4694
5779
  const newConfig = { org: orgSlug, site: siteSlug, dir: dirOption || config.dir };
4695
- await writeFile(configPath, JSON.stringify(newConfig, null, 2) + `
5780
+ await writeFile2(configPath, JSON.stringify(newConfig, null, 2) + `
4696
5781
  `);
4697
5782
  console.log(`Saved config to zerodeploy.json`);
4698
5783
  console.log("");
@@ -4703,13 +5788,57 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
4703
5788
  console.log(`Detected: ${framework.name}`);
4704
5789
  }
4705
5790
  const shouldBuild = options.build === true;
4706
- if (options.install) {
4707
- const installCmd = config.install || framework?.installCommand || "npm install";
4708
- console.log(`
4709
- Installing dependencies...`);
4710
- console.log(`> ${installCmd}
4711
- `);
4712
- const installResult = await runCommand(installCmd, cwd);
5791
+ if (shouldBuild || options.install) {
5792
+ const spinner = createSpinner("Checking deployment limits...");
5793
+ spinner.start();
5794
+ try {
5795
+ const client = getClient(token);
5796
+ const sitesRes = await client.orgs[":orgSlug"].sites.$get({
5797
+ param: { orgSlug }
5798
+ });
5799
+ if (!sitesRes.ok) {
5800
+ spinner.fail("Pre-flight check failed");
5801
+ const body = await sitesRes.json();
5802
+ displayError(parseApiError(body));
5803
+ return;
5804
+ }
5805
+ const { data: sites } = await sitesRes.json();
5806
+ const siteExists = sites.some((s) => s.slug === siteSlug);
5807
+ if (!siteExists) {
5808
+ spinner.fail("Pre-flight check failed");
5809
+ displayError({ code: "site_not_found", message: `Site "${siteSlug}" not found in organization "${orgSlug}"` });
5810
+ return;
5811
+ }
5812
+ const usageRes = await client.auth.me.usage.$get();
5813
+ if (usageRes.ok) {
5814
+ const usage = await usageRes.json();
5815
+ const org = usage.orgs.find((o) => o.slug === orgSlug);
5816
+ if (org) {
5817
+ const monthlyUsage = org.deployments_this_month;
5818
+ const monthlyLimit = usage.limits.max_deployments_per_month;
5819
+ const percentUsed = Math.round(monthlyUsage / monthlyLimit * 100);
5820
+ if (percentUsed >= 90) {
5821
+ spinner.warn(`Approaching monthly deployment limit (${monthlyUsage}/${monthlyLimit})`);
5822
+ } else {
5823
+ spinner.succeed("Pre-flight check passed");
5824
+ }
5825
+ } else {
5826
+ spinner.succeed("Pre-flight check passed");
5827
+ }
5828
+ } else {
5829
+ spinner.succeed("Pre-flight check passed");
5830
+ }
5831
+ } catch (err) {
5832
+ spinner.warn("Could not verify limits (will check during deploy)");
5833
+ }
5834
+ }
5835
+ if (options.install) {
5836
+ const installCmd = config.install || framework?.installCommand || "npm install";
5837
+ console.log(`
5838
+ Installing dependencies...`);
5839
+ console.log(`> ${installCmd}
5840
+ `);
5841
+ const installResult = await runCommand(installCmd, cwd);
4713
5842
  if (!installResult.success) {
4714
5843
  console.log(`
4715
5844
  Error: Install failed`);
@@ -4733,7 +5862,7 @@ Error: Build failed`);
4733
5862
  }
4734
5863
  let deployDir;
4735
5864
  if (dirOption) {
4736
- deployDir = resolve3(cwd, dirOption);
5865
+ deployDir = resolve4(cwd, dirOption);
4737
5866
  try {
4738
5867
  const stats = await stat2(deployDir);
4739
5868
  if (!stats.isDirectory()) {
@@ -4745,7 +5874,7 @@ Error: Build failed`);
4745
5874
  return;
4746
5875
  }
4747
5876
  } else {
4748
- const detected = framework ? resolve3(cwd, framework.outputDir) : await findBuildDirectory(cwd);
5877
+ const detected = framework ? resolve4(cwd, framework.outputDir) : await findBuildDirectory(cwd);
4749
5878
  if (!detected) {
4750
5879
  console.log("Error: Could not find build directory. Use --dir to specify.");
4751
5880
  return;
@@ -4770,92 +5899,150 @@ Error: Build failed`);
4770
5899
  return;
4771
5900
  }
4772
5901
  const totalSize = files.reduce((sum, f) => sum + f.size, 0);
4773
- console.log(`Found ${files.length} files (${formatBytes(totalSize)})`);
5902
+ console.log(`Found ${files.length} files (${formatBytes2(totalSize)})`);
4774
5903
  try {
4775
5904
  const client = getClient(token);
5905
+ const ci = detectCI();
5906
+ if (ci) {
5907
+ console.log(`Detected CI: ${ci.name}`);
5908
+ if (ci.prNumber)
5909
+ console.log(` PR: #${ci.prNumber}`);
5910
+ if (ci.commitSha)
5911
+ console.log(` Commit: ${ci.commitSha.slice(0, 7)}`);
5912
+ if (ci.branch)
5913
+ console.log(` Branch: ${ci.branch}`);
5914
+ console.log("");
5915
+ }
5916
+ const isPreview = options.preview === true || !options.prod && ci?.prNumber != null;
5917
+ if (!options.preview && !options.prod && ci?.prNumber != null) {
5918
+ console.log("Auto-preview enabled for PR deployment");
5919
+ console.log(" Use --prod to deploy to production instead");
5920
+ console.log("");
5921
+ }
4776
5922
  const deployPayload = { siteSlug, orgSlug };
4777
- if (options.pr) {
4778
- deployPayload.prNumber = parseInt(options.pr, 10);
5923
+ const prNumber = options.pr ? parseInt(options.pr, 10) : ci?.prNumber;
5924
+ if (prNumber) {
5925
+ deployPayload.prNumber = prNumber;
4779
5926
  }
4780
- if (options.prTitle) {
4781
- deployPayload.prTitle = options.prTitle;
5927
+ const prTitle = options.prTitle ?? ci?.prTitle;
5928
+ if (prTitle) {
5929
+ deployPayload.prTitle = prTitle;
4782
5930
  }
4783
- if (options.commit) {
4784
- deployPayload.commitSha = options.commit;
5931
+ const commitSha = options.commit ?? ci?.commitSha;
5932
+ if (commitSha) {
5933
+ deployPayload.commitSha = commitSha;
4785
5934
  }
4786
- if (options.commitMessage) {
4787
- deployPayload.commitMessage = options.commitMessage;
5935
+ const commitMessage = options.commitMessage ?? ci?.commitMessage;
5936
+ if (commitMessage) {
5937
+ deployPayload.commitMessage = commitMessage;
4788
5938
  }
4789
- if (options.branch) {
4790
- deployPayload.branch = options.branch;
5939
+ const branch = options.branch ?? ci?.branch;
5940
+ if (branch) {
5941
+ deployPayload.branch = branch;
4791
5942
  }
4792
5943
  const createRes = await client.deployments.$post({
4793
5944
  json: deployPayload
4794
5945
  });
4795
5946
  if (!createRes.ok) {
4796
5947
  const body = await createRes.json();
4797
- let message = "Failed to create deployment";
4798
- if (typeof body.error === "string") {
4799
- message = body.error;
4800
- } else if (typeof body.message === "string") {
4801
- message = body.message;
4802
- } else if (body.success === false && body.error && typeof body.error === "object") {
4803
- const zodError = body.error;
4804
- if (zodError.issues?.[0]) {
4805
- const issue = zodError.issues[0];
4806
- const field = issue.path?.join(".") || "";
4807
- message = field ? `${field}: ${issue.message}` : issue.message;
4808
- } else if (zodError.message) {
4809
- try {
4810
- const issues = JSON.parse(zodError.message);
4811
- if (issues[0]) {
4812
- const field = issues[0].path?.join(".") || "";
4813
- message = field ? `${field}: ${issues[0].message}` : issues[0].message;
4814
- }
4815
- } catch {
4816
- message = zodError.message;
4817
- }
4818
- }
4819
- }
4820
- console.log(`Error: ${message}`);
5948
+ displayError(parseApiError(body));
4821
5949
  return;
4822
5950
  }
4823
- const deployment = await createRes.json();
5951
+ const { data: deployment } = await createRes.json();
4824
5952
  console.log(`Created deployment: ${deployment.id}`);
4825
- console.log("Creating archive...");
4826
- const archive = await createTarGz(files);
4827
- console.log(` ${formatBytes(totalSize)} -> ${formatBytes(archive.length)} (${formatCompression(totalSize, archive.length)})`);
4828
- console.log("Uploading...");
5953
+ const progressBar2 = createProgressBar({ total: files.length, label: "Archiving" });
5954
+ const archive = await createTarGz(files, (current, total) => {
5955
+ progressBar2.update(current);
5956
+ });
5957
+ progressBar2.done();
5958
+ console.log(` ${formatBytes2(totalSize)} -> ${formatBytes2(archive.length)} (${formatCompression(totalSize, archive.length)})`);
5959
+ const uploadSpinner = createSpinner(`Uploading (${formatBytes2(archive.length)})...`);
5960
+ uploadSpinner.start();
4829
5961
  const uploadUrl = deployment.uploadUrl || `${API_URL}/deployments/${deployment.id}/upload`;
4830
- const uploadSuccess = await uploadArchive(token, uploadUrl, archive);
5962
+ const uploadSuccess = await uploadArchive(token, uploadUrl, archive, (attempt, error, delayMs) => {
5963
+ uploadSpinner.update(`Uploading... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
5964
+ });
4831
5965
  if (!uploadSuccess) {
4832
- console.log("Error: Failed to upload archive");
5966
+ uploadSpinner.stop("Error: Failed to upload archive");
4833
5967
  return;
4834
5968
  }
4835
- console.log(` Uploaded ${formatBytes(archive.length)}`);
4836
- console.log("Finalizing...");
4837
- const finalizeRes = await fetch(`${API_URL}/deployments/${deployment.id}/finalize`, {
5969
+ uploadSpinner.stop(` Uploaded ${formatBytes2(archive.length)}`);
5970
+ const finalizeSpinner = createSpinner("Finalizing...");
5971
+ finalizeSpinner.start();
5972
+ const finalizeRes = await fetchWithRetry(`${API_URL}/deployments/${deployment.id}/finalize`, {
4838
5973
  method: "POST",
4839
5974
  headers: {
4840
5975
  "Content-Type": "application/json",
4841
5976
  Authorization: `Bearer ${token}`
4842
5977
  },
4843
- body: JSON.stringify({ preview: options.preview || false })
5978
+ body: JSON.stringify({ preview: isPreview })
5979
+ }, {
5980
+ maxRetries: 3,
5981
+ onRetry: (attempt, error, delayMs) => {
5982
+ finalizeSpinner.update(`Finalizing... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
5983
+ }
4844
5984
  });
4845
5985
  if (!finalizeRes.ok) {
4846
- console.log("Error: Failed to finalize deployment");
5986
+ finalizeSpinner.stop("Error: Failed to finalize deployment");
4847
5987
  return;
4848
5988
  }
5989
+ finalizeSpinner.stop();
5990
+ const verifyUrl = isPreview ? deployment.previewUrl : deployment.url;
5991
+ let verified = false;
5992
+ if (options.verify !== false) {
5993
+ const verifySpinner = createSpinner("Verifying...");
5994
+ verifySpinner.start();
5995
+ const verification = await verifyDeployment(verifyUrl);
5996
+ verified = verification.success;
5997
+ if (!verified) {
5998
+ verifySpinner.stop();
5999
+ console.log("");
6000
+ console.log("Warning: Could not verify deployment");
6001
+ if (verification.status) {
6002
+ console.log(` Received status ${verification.status}`);
6003
+ } else if (verification.error) {
6004
+ console.log(` ${verification.error}`);
6005
+ }
6006
+ if (options.autoRollback !== false && !isPreview) {
6007
+ console.log("");
6008
+ console.log("Auto-rolling back to previous deployment...");
6009
+ const rollback = await autoRollback(token, orgSlug, siteSlug, deployment.id);
6010
+ if (rollback.success) {
6011
+ console.log(`Rolled back to ${rollback.deploymentId?.slice(0, 8)}`);
6012
+ console.log("");
6013
+ console.log("Deployment failed verification and was rolled back.");
6014
+ console.log(`Failed deployment: ${deployment.id.slice(0, 8)}`);
6015
+ console.log(`Check manually: ${verifyUrl}`);
6016
+ return;
6017
+ } else {
6018
+ console.log(` Could not auto-rollback: ${rollback.error}`);
6019
+ console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
6020
+ }
6021
+ } else {
6022
+ console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
6023
+ }
6024
+ } else {
6025
+ verifySpinner.stop();
6026
+ }
6027
+ }
4849
6028
  console.log("");
4850
- if (options.preview) {
6029
+ if (isPreview) {
4851
6030
  console.log("Preview deployment created!");
4852
- console.log(`Preview: ${deployment.previewUrl}`);
6031
+ if (verified) {
6032
+ console.log(`Preview: ${deployment.previewUrl} (verified)`);
6033
+ } else {
6034
+ console.log(`Preview: ${deployment.previewUrl}`);
6035
+ }
4853
6036
  console.log("");
4854
6037
  console.log(`To make this deployment live, run:`);
4855
6038
  console.log(` zerodeploy deploy promote ${deployment.id.slice(0, 8)}`);
4856
6039
  } else {
4857
6040
  console.log("Deployment successful!");
4858
- console.log(`URL: ${deployment.url}`);
6041
+ if (verified) {
6042
+ console.log(`URL: ${deployment.url} (verified)`);
6043
+ } else {
6044
+ console.log(`URL: ${deployment.url}`);
6045
+ }
4859
6046
  console.log(`Preview: ${deployment.previewUrl}`);
4860
6047
  }
4861
6048
  if (options.githubOutput) {
@@ -4867,35 +6054,48 @@ Error: Build failed`);
4867
6054
  appendFileSync(githubOutputFile, `deployment_url=${deployment.url}
4868
6055
  `);
4869
6056
  appendFileSync(githubOutputFile, `preview_url=${deployment.previewUrl}
6057
+ `);
6058
+ appendFileSync(githubOutputFile, `is_preview=${isPreview}
4870
6059
  `);
4871
6060
  } else {
4872
6061
  console.log("");
4873
6062
  console.log("::set-output name=deployment_id::" + deployment.id);
4874
6063
  console.log("::set-output name=deployment_url::" + deployment.url);
4875
6064
  console.log("::set-output name=preview_url::" + deployment.previewUrl);
6065
+ console.log("::set-output name=is_preview::" + isPreview);
4876
6066
  }
4877
6067
  }
4878
6068
  } catch (err) {
4879
- const message = err instanceof Error ? err.message : "Unknown error";
4880
- console.log(`Deploy failed: ${message}`);
6069
+ if (err instanceof Error) {
6070
+ displayNetworkError(err);
6071
+ } else {
6072
+ displayError({
6073
+ code: "unknown_error",
6074
+ message: "Deploy failed: Unknown error"
6075
+ });
6076
+ }
4881
6077
  }
4882
6078
  });
4883
6079
 
4884
6080
  // src/commands/deployments/list.ts
4885
- var deploymentsListCommand = new Command2("list").description("List deployments for a site").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").action(async (site, options) => {
6081
+ var deploymentsListCommand = new Command2("list").description("List deployments for a site").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").option("--json", "Output as JSON").action(async (site, options) => {
4886
6082
  const token = loadToken();
4887
6083
  if (!token) {
4888
- console.log("Not logged in. Run: zerodeploy login");
4889
- return;
6084
+ handleAuthError();
4890
6085
  }
4891
6086
  try {
4892
6087
  const client = getClient(token);
4893
6088
  const res = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
4894
6089
  param: { orgSlug: options.org, siteSlug: site }
4895
6090
  });
4896
- if (!res.ok)
4897
- throw new Error(`API Error ${res.status}`);
4898
- const deployments = await res.json();
6091
+ if (!res.ok) {
6092
+ await handleApiError(res);
6093
+ }
6094
+ const { data: deployments } = await res.json();
6095
+ if (options.json) {
6096
+ console.log(JSON.stringify(deployments, null, 2));
6097
+ return;
6098
+ }
4899
6099
  if (deployments.length === 0) {
4900
6100
  console.log("No deployments found.");
4901
6101
  return;
@@ -4903,7 +6103,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4903
6103
  console.log("Deployments:");
4904
6104
  console.log();
4905
6105
  for (const d of deployments) {
4906
- const current = d.isCurrent ? " (current)" : "";
6106
+ const current = d.is_current ? " (current)" : "";
4907
6107
  const date = new Date(d.created_at).toLocaleString();
4908
6108
  const shortId = d.id.slice(0, 8);
4909
6109
  const commit = d.commit_sha ? ` [${d.commit_sha.slice(0, 7)}]` : "";
@@ -4911,26 +6111,133 @@ var deploymentsListCommand = new Command2("list").description("List deployments
4911
6111
  const message = d.commit_message ? ` - ${(d.commit_message.split(`
4912
6112
  `)[0] ?? "").slice(0, 50)}` : "";
4913
6113
  console.log(` ${shortId} ${d.status.padEnd(10)} ${date}${current}`);
6114
+ console.log(` ${d.preview_url}`);
4914
6115
  if (commit || message) {
4915
- console.log(` ${commit}${branch}${message}`);
6116
+ console.log(` ${commit}${branch}${message}`);
4916
6117
  }
4917
6118
  console.log();
4918
6119
  }
4919
6120
  } catch (err) {
4920
- const message = err instanceof Error ? err.message : "Unknown error";
4921
- console.error("Failed to list deployments:", message);
6121
+ const message = err instanceof Error ? err.message : String(err);
6122
+ displayError({ code: "network_error", message: `Failed to list deployments: ${message}` });
6123
+ process.exit(ExitCode.NETWORK_ERROR);
6124
+ }
6125
+ });
6126
+
6127
+ // src/commands/deployments/show.ts
6128
+ function formatBytes3(bytes) {
6129
+ if (bytes === 0)
6130
+ return "0 B";
6131
+ const k = 1024;
6132
+ const sizes = ["B", "KB", "MB", "GB"];
6133
+ const i2 = Math.floor(Math.log(bytes) / Math.log(k));
6134
+ return parseFloat((bytes / Math.pow(k, i2)).toFixed(1)) + " " + sizes[i2];
6135
+ }
6136
+ function formatStatus3(status) {
6137
+ switch (status) {
6138
+ case "pending":
6139
+ return "Pending";
6140
+ case "uploading":
6141
+ return "Uploading";
6142
+ case "processing":
6143
+ return "Processing";
6144
+ case "ready":
6145
+ return "Ready";
6146
+ case "failed":
6147
+ return "Failed";
6148
+ default:
6149
+ return status;
6150
+ }
6151
+ }
6152
+ var deploymentsShowCommand = new Command2("show").description("View deployment details").argument("<id>", "Deployment ID (full or short)").option("--json", "Output as JSON").action(async (id, options) => {
6153
+ const token = loadToken();
6154
+ if (!token) {
6155
+ handleAuthError();
6156
+ }
6157
+ try {
6158
+ const client = getClient(token);
6159
+ const res = await client.deployments[":id"].$get({
6160
+ param: { id }
6161
+ });
6162
+ if (!res.ok) {
6163
+ if (res.status === 404) {
6164
+ displayError({
6165
+ code: "not_found",
6166
+ message: `Deployment not found: ${id}`,
6167
+ hint: "Use the full deployment ID or at least 8 characters."
6168
+ });
6169
+ process.exit(ExitCode.NOT_FOUND);
6170
+ }
6171
+ await handleApiError(res);
6172
+ }
6173
+ const { data: d } = await res.json();
6174
+ if (options.json) {
6175
+ console.log(JSON.stringify(d, null, 2));
6176
+ return;
6177
+ }
6178
+ console.log("Deployment Details");
6179
+ console.log("=".repeat(50));
6180
+ console.log();
6181
+ console.log(`ID: ${d.id}`);
6182
+ console.log(`Status: ${formatStatus3(d.status)}`);
6183
+ console.log(`Created: ${new Date(d.created_at).toLocaleString()}`);
6184
+ console.log();
6185
+ console.log("URLs");
6186
+ console.log("-".repeat(50));
6187
+ console.log(`Production: ${d.url}`);
6188
+ console.log(`Preview: ${d.preview_url}`);
6189
+ console.log();
6190
+ if (d.commit_sha || d.branch || d.pr_number) {
6191
+ console.log("Git Info");
6192
+ console.log("-".repeat(50));
6193
+ if (d.branch) {
6194
+ console.log(`Branch: ${d.branch}`);
6195
+ }
6196
+ if (d.commit_sha) {
6197
+ console.log(`Commit: ${d.commit_sha}`);
6198
+ }
6199
+ if (d.commit_message) {
6200
+ const firstLine = d.commit_message.split(`
6201
+ `)[0] ?? "";
6202
+ console.log(`Message: ${firstLine.slice(0, 60)}${firstLine.length > 60 ? "..." : ""}`);
6203
+ }
6204
+ if (d.pr_number) {
6205
+ console.log(`PR: #${d.pr_number}${d.pr_title ? ` - ${d.pr_title}` : ""}`);
6206
+ }
6207
+ console.log();
6208
+ }
6209
+ console.log("Files");
6210
+ console.log("-".repeat(50));
6211
+ console.log(`Count: ${d.file_count.toLocaleString()} files`);
6212
+ console.log(`Size: ${formatBytes3(d.total_size_bytes)}`);
6213
+ console.log();
6214
+ if (d.status === "failed" && d.error_message) {
6215
+ console.log("Error");
6216
+ console.log("-".repeat(50));
6217
+ console.log(d.error_message);
6218
+ console.log();
6219
+ }
6220
+ if (d.status === "ready") {
6221
+ console.log("Actions");
6222
+ console.log("-".repeat(50));
6223
+ console.log(`Rollback to this deployment:`);
6224
+ console.log(` zerodeploy rollback <site> --org <org> --to ${d.id.slice(0, 8)}`);
6225
+ }
6226
+ } catch (err) {
6227
+ const message = err instanceof Error ? err.message : String(err);
6228
+ displayError({ code: "network_error", message: `Failed to get deployment: ${message}` });
6229
+ process.exit(ExitCode.NETWORK_ERROR);
4922
6230
  }
4923
6231
  });
4924
6232
 
4925
6233
  // src/commands/deployments/index.ts
4926
- var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand);
6234
+ var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand).addCommand(deploymentsShowCommand);
4927
6235
 
4928
6236
  // src/commands/rollback.ts
4929
6237
  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) => {
4930
6238
  const token = loadToken();
4931
6239
  if (!token) {
4932
- console.log("Not logged in. Run: zerodeploy login");
4933
- return;
6240
+ handleAuthError();
4934
6241
  }
4935
6242
  try {
4936
6243
  const client = getClient(token);
@@ -4939,15 +6246,16 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
4939
6246
  const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
4940
6247
  param: { orgSlug: options.org, siteSlug: site }
4941
6248
  });
4942
- if (!listRes.ok)
4943
- throw new Error(`API Error ${listRes.status}`);
4944
- const deployments = await listRes.json();
6249
+ if (!listRes.ok) {
6250
+ await handleApiError(listRes);
6251
+ }
6252
+ const { data: deployments } = await listRes.json();
4945
6253
  const readyDeployments = deployments.filter((d) => d.status === "ready");
4946
6254
  if (readyDeployments.length < 2) {
4947
- console.log("No previous deployment to rollback to.");
4948
- return;
6255
+ displayError({ code: "validation_error", message: "No previous deployment to rollback to." });
6256
+ process.exit(ExitCode.VALIDATION_ERROR);
4949
6257
  }
4950
- const currentIndex = readyDeployments.findIndex((d) => d.isCurrent);
6258
+ const currentIndex = readyDeployments.findIndex((d) => d.is_current);
4951
6259
  if (currentIndex === -1 || currentIndex >= readyDeployments.length - 1) {
4952
6260
  deploymentId = readyDeployments[1].id;
4953
6261
  } else {
@@ -4958,146 +6266,402 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
4958
6266
  param: { id: deploymentId }
4959
6267
  });
4960
6268
  if (!res.ok) {
4961
- const error = await res.json();
4962
- throw new Error(error.error || `API Error ${res.status}`);
6269
+ await handleApiError(res);
4963
6270
  }
4964
6271
  const result = await res.json();
4965
- const shortId = result.deployment.id.slice(0, 8);
4966
- const commit = result.deployment.commit_sha ? ` (${result.deployment.commit_sha.slice(0, 7)})` : "";
6272
+ const shortId = result.data.deployment.id.slice(0, 8);
6273
+ const commit = result.data.deployment.commit_sha ? ` (${result.data.deployment.commit_sha.slice(0, 7)})` : "";
4967
6274
  console.log(`Rolled back to deployment ${shortId}${commit}`);
4968
- console.log(`URL: ${result.deployment.url}`);
6275
+ console.log(`URL: ${result.data.deployment.url}`);
4969
6276
  } catch (err) {
4970
- const message = err instanceof Error ? err.message : "Unknown error";
4971
- console.error("Failed to rollback:", message);
6277
+ const message = err instanceof Error ? err.message : String(err);
6278
+ displayError({ code: "network_error", message: `Failed to rollback: ${message}` });
6279
+ process.exit(ExitCode.NETWORK_ERROR);
4972
6280
  }
4973
6281
  });
4974
6282
 
4975
6283
  // src/commands/token/create.ts
4976
- var tokenCreateCommand = new Command2("create").description("Create a deploy token for a site").argument("<name>", 'Token name (e.g., "GitHub Actions")').requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (name, options) => {
6284
+ var tokenCreateCommand = new Command2("create").description("Create an API token").argument("<name>", 'Token name (e.g., "GitHub Actions", "CI Pipeline")').option("--site <siteId>", "Create a site-scoped deploy token (instead of a PAT)").option("--org <orgSlug>", "Create an org-scoped deploy token (instead of a PAT)").option("--expires <days>", "Token expiration in days (default: never)", parseInt).option("--json", "Output as JSON").action(async (name, options) => {
4977
6285
  const token = loadToken();
4978
6286
  if (!token) {
4979
- console.log("Not logged in. Run: zerodeploy login");
4980
- return;
6287
+ handleAuthError();
6288
+ }
6289
+ if (options.site && options.org) {
6290
+ displayError({ code: "validation_error", message: "--site and --org cannot be used together" });
6291
+ process.exit(ExitCode.VALIDATION_ERROR);
4981
6292
  }
4982
6293
  try {
4983
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
4984
- method: "POST",
4985
- headers: {
4986
- "Content-Type": "application/json",
4987
- Authorization: `Bearer ${token}`
4988
- },
4989
- body: JSON.stringify({ name })
4990
- });
6294
+ const client = getClient(token);
6295
+ let scopeType = "user";
6296
+ let permissions = ["*"];
6297
+ if (options.site) {
6298
+ scopeType = "site";
6299
+ permissions = ["deploy"];
6300
+ } else if (options.org) {
6301
+ scopeType = "org";
6302
+ permissions = ["deploy"];
6303
+ }
6304
+ const body = {
6305
+ name,
6306
+ scope_type: scopeType,
6307
+ permissions
6308
+ };
6309
+ if (options.site) {
6310
+ body.site_id = options.site;
6311
+ }
6312
+ if (options.org) {
6313
+ body.org_slug = options.org;
6314
+ }
6315
+ if (options.expires) {
6316
+ body.expires_in_days = options.expires;
6317
+ }
6318
+ const res = await client.tokens.$post({ json: body });
4991
6319
  if (!res.ok) {
4992
- const error = await res.json();
4993
- console.log(`Error: ${error.error || "Failed to create token"}`);
6320
+ await handleApiError(res);
6321
+ }
6322
+ const { data: result } = await res.json();
6323
+ if (options.json) {
6324
+ console.log(JSON.stringify(result, null, 2));
4994
6325
  return;
4995
6326
  }
4996
- const result = await res.json();
6327
+ let typeLabel = "Personal Access Token (PAT)";
6328
+ if (result.scope_type === "site") {
6329
+ typeLabel = "Site Deploy Token";
6330
+ } else if (result.scope_type === "org") {
6331
+ typeLabel = "Org Deploy Token";
6332
+ }
4997
6333
  console.log("");
4998
- console.log("Deploy token created successfully!");
6334
+ console.log(`${typeLabel} created successfully!`);
4999
6335
  console.log("");
5000
- console.log(`Name: ${result.name}`);
5001
- console.log(`ID: ${result.id}`);
6336
+ console.log(`Name: ${result.name}`);
6337
+ console.log(`ID: ${result.id}`);
6338
+ console.log(`Prefix: ${result.token_prefix}`);
6339
+ console.log(`Permissions: ${result.permissions.join(", ")}`);
6340
+ if (result.org_slug) {
6341
+ console.log(`Org: ${result.org_slug}`);
6342
+ }
6343
+ if (result.expires_at) {
6344
+ console.log(`Expires: ${new Date(result.expires_at).toLocaleDateString()}`);
6345
+ }
5002
6346
  console.log("");
5003
6347
  console.log("Token (save this - it will not be shown again):");
5004
6348
  console.log("");
5005
6349
  console.log(` ${result.token}`);
5006
6350
  console.log("");
5007
- console.log("Usage in GitHub Actions:");
5008
- console.log(" Add this token as a repository secret named ZERODEPLOY_TOKEN");
6351
+ if (result.scope_type === "site" || result.scope_type === "org") {
6352
+ console.log("Usage in GitHub Actions:");
6353
+ console.log(" Add this token as a repository secret named ZERODEPLOY_TOKEN");
6354
+ if (result.scope_type === "org") {
6355
+ console.log(" This token can deploy to any site in the organization.");
6356
+ }
6357
+ } else {
6358
+ console.log("Usage:");
6359
+ console.log(" Use this token in the Authorization header:");
6360
+ console.log(" Authorization: Bearer <token>");
6361
+ }
5009
6362
  console.log("");
5010
6363
  } catch (err) {
5011
- const message = err instanceof Error ? err.message : "Unknown error";
5012
- console.log(`Failed to create token: ${message}`);
6364
+ const message = err instanceof Error ? err.message : String(err);
6365
+ displayError({ code: "network_error", message: `Failed to create token: ${message}` });
6366
+ process.exit(ExitCode.NETWORK_ERROR);
5013
6367
  }
5014
6368
  });
5015
6369
 
5016
6370
  // src/commands/token/list.ts
5017
- var tokenListCommand = new Command2("list").description("List deploy tokens for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
6371
+ var tokenListCommand = new Command2("list").description("List API tokens").option("--site <siteId>", "Filter by site ID (for site-scoped deploy tokens)").option("--org <orgId>", "Filter by org ID (for org-scoped deploy tokens)").option("--type <type>", "Filter by type: user (PAT), site, or org (deploy tokens)", "all").option("--json", "Output as JSON").action(async (options) => {
5018
6372
  const token = loadToken();
5019
6373
  if (!token) {
5020
- console.log("Not logged in. Run: zerodeploy login");
5021
- return;
6374
+ handleAuthError();
5022
6375
  }
5023
6376
  try {
5024
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
5025
- headers: {
5026
- Authorization: `Bearer ${token}`
5027
- }
5028
- });
6377
+ const client = getClient(token);
6378
+ const query = {};
6379
+ if (options.type === "user") {
6380
+ query.scope_type = "user";
6381
+ } else if (options.type === "site") {
6382
+ query.scope_type = "site";
6383
+ } else if (options.type === "org") {
6384
+ query.scope_type = "org";
6385
+ }
6386
+ if (options.site) {
6387
+ query.site_id = options.site;
6388
+ }
6389
+ if (options.org) {
6390
+ query.org_id = options.org;
6391
+ }
6392
+ const res = await client.tokens.$get({ query });
5029
6393
  if (!res.ok) {
5030
- const error = await res.json();
5031
- console.log(`Error: ${error.error || "Failed to list tokens"}`);
6394
+ await handleApiError(res);
6395
+ }
6396
+ const { data: tokens } = await res.json();
6397
+ if (options.json) {
6398
+ console.log(JSON.stringify(tokens, null, 2));
5032
6399
  return;
5033
6400
  }
5034
- const tokens = await res.json();
5035
6401
  if (tokens.length === 0) {
5036
- console.log(`No deploy tokens for ${options.org}/${options.site}`);
6402
+ console.log("No API tokens found");
6403
+ if (options.type !== "all") {
6404
+ console.log(`(filtered by type: ${options.type})`);
6405
+ }
5037
6406
  return;
5038
6407
  }
5039
- console.log(`Deploy tokens for ${options.org}/${options.site}:`);
6408
+ console.log("API Tokens:");
5040
6409
  console.log("");
5041
6410
  for (const t of tokens) {
5042
6411
  const created = new Date(t.created_at).toLocaleDateString();
5043
6412
  const lastUsed = t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : "Never";
5044
- console.log(` ${t.id.slice(0, 8)} ${t.name.padEnd(20)} Created: ${created} Last used: ${lastUsed}`);
6413
+ const expires = t.expires_at ? new Date(t.expires_at).toLocaleDateString() : "Never";
6414
+ let typeLabel = "PAT";
6415
+ if (t.scope_type === "site") {
6416
+ typeLabel = "Site";
6417
+ } else if (t.scope_type === "org") {
6418
+ typeLabel = "Org";
6419
+ }
6420
+ const permissions = t.permissions.join(", ");
6421
+ console.log(` ${t.token_prefix.padEnd(14)} ${t.name.padEnd(20)} [${typeLabel}]`);
6422
+ console.log(` Created: ${created} Last used: ${lastUsed} Expires: ${expires}`);
6423
+ console.log(` Permissions: ${permissions}`);
6424
+ if (t.site_id) {
6425
+ console.log(` Site ID: ${t.site_id}`);
6426
+ }
6427
+ if (t.org_slug) {
6428
+ console.log(` Org: ${t.org_slug}`);
6429
+ }
6430
+ console.log("");
5045
6431
  }
5046
- console.log("");
5047
6432
  } catch (err) {
5048
- const message = err instanceof Error ? err.message : "Unknown error";
5049
- console.log(`Failed to list tokens: ${message}`);
6433
+ const message = err instanceof Error ? err.message : String(err);
6434
+ displayError({ code: "network_error", message: `Failed to list tokens: ${message}` });
6435
+ process.exit(ExitCode.NETWORK_ERROR);
5050
6436
  }
5051
6437
  });
5052
6438
 
5053
6439
  // src/commands/token/delete.ts
5054
- var tokenDeleteCommand = new Command2("delete").description("Delete a deploy token").argument("<tokenId>", "Token ID (or first 8 characters)").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (tokenId, options) => {
6440
+ var tokenDeleteCommand = new Command2("delete").description("Delete an API token").argument("<tokenId>", 'Token ID or token prefix (e.g., "zd_abc12345")').action(async (tokenIdOrPrefix) => {
5055
6441
  const token = loadToken();
5056
6442
  if (!token) {
5057
- console.log("Not logged in. Run: zerodeploy login");
5058
- return;
6443
+ handleAuthError();
5059
6444
  }
5060
6445
  try {
5061
- let fullTokenId = tokenId;
5062
- if (tokenId.length < 36) {
5063
- const listRes = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens`, {
5064
- headers: {
5065
- Authorization: `Bearer ${token}`
5066
- }
5067
- });
6446
+ const client = getClient(token);
6447
+ let fullTokenId = tokenIdOrPrefix;
6448
+ if (tokenIdOrPrefix.length < 36) {
6449
+ const listRes = await client.tokens.$get({ query: {} });
5068
6450
  if (!listRes.ok) {
5069
- const error = await listRes.json();
5070
- console.log(`Error: ${error.error || "Failed to find token"}`);
5071
- return;
6451
+ await handleApiError(listRes);
5072
6452
  }
5073
- const tokens = await listRes.json();
5074
- const match = tokens.find((t) => t.id.startsWith(tokenId));
6453
+ const { data: tokens } = await listRes.json();
6454
+ const match = tokens.find((t) => t.token_prefix === tokenIdOrPrefix || t.token_prefix.startsWith(tokenIdOrPrefix) || t.id.startsWith(tokenIdOrPrefix));
5075
6455
  if (!match) {
5076
- console.log(`Error: No token found starting with "${tokenId}"`);
5077
- return;
6456
+ displayError({ code: "not_found", message: `No token found matching "${tokenIdOrPrefix}"`, hint: "Use `zerodeploy token list` to see available tokens" });
6457
+ process.exit(ExitCode.NOT_FOUND);
5078
6458
  }
5079
6459
  fullTokenId = match.id;
6460
+ console.log(`Found token: ${match.name} (${match.token_prefix})`);
5080
6461
  }
5081
- const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/tokens/${fullTokenId}`, {
5082
- method: "DELETE",
5083
- headers: {
5084
- Authorization: `Bearer ${token}`
5085
- }
6462
+ const res = await client.tokens[":tokenId"].$delete({
6463
+ param: { tokenId: fullTokenId }
5086
6464
  });
5087
6465
  if (!res.ok) {
5088
- const error = await res.json();
5089
- console.log(`Error: ${error.error || "Failed to delete token"}`);
5090
- return;
6466
+ await handleApiError(res);
5091
6467
  }
5092
6468
  console.log("Token deleted successfully");
5093
6469
  } catch (err) {
5094
- const message = err instanceof Error ? err.message : "Unknown error";
5095
- console.log(`Failed to delete token: ${message}`);
6470
+ const message = err instanceof Error ? err.message : String(err);
6471
+ displayError({ code: "network_error", message: `Failed to delete token: ${message}` });
6472
+ process.exit(ExitCode.NETWORK_ERROR);
5096
6473
  }
5097
6474
  });
5098
6475
 
5099
6476
  // src/commands/token/index.ts
5100
- var tokenCommand = new Command2("token").description("Manage deploy tokens for CI/CD").addCommand(tokenCreateCommand).addCommand(tokenListCommand).addCommand(tokenDeleteCommand);
6477
+ var tokenCommand = new Command2("token").description("Manage API tokens (PATs and deploy tokens)").addCommand(tokenCreateCommand).addCommand(tokenListCommand).addCommand(tokenDeleteCommand);
6478
+
6479
+ // src/commands/billing/index.ts
6480
+ function formatCents(cents) {
6481
+ return `$${(cents / 100).toFixed(2)}`;
6482
+ }
6483
+ function formatDate(dateStr) {
6484
+ return new Date(dateStr).toLocaleDateString("en-US", {
6485
+ year: "numeric",
6486
+ month: "short",
6487
+ day: "numeric"
6488
+ });
6489
+ }
6490
+ function formatMonth(monthStr) {
6491
+ const [year, month] = monthStr.split("-");
6492
+ return new Date(Number(year), Number(month) - 1).toLocaleDateString("en-US", {
6493
+ year: "numeric",
6494
+ month: "long"
6495
+ });
6496
+ }
6497
+ function getReasonLabel(reason) {
6498
+ const labels = {
6499
+ beta_credit: "Beta Credit",
6500
+ promo_credit: "Promotional Credit",
6501
+ support_credit: "Support Credit",
6502
+ refund: "Refund"
6503
+ };
6504
+ return labels[reason] || reason;
6505
+ }
6506
+ var billingUsageCommand = new Command2("usage").description("Show account balance and current period usage").option("--json", "Output as JSON").action(async (options) => {
6507
+ const token = loadToken();
6508
+ if (!token) {
6509
+ handleAuthError();
6510
+ }
6511
+ try {
6512
+ const client = getClient(token);
6513
+ const res = await client.billing.usage.$get();
6514
+ if (!res.ok) {
6515
+ await handleApiError(res);
6516
+ }
6517
+ const { data } = await res.json();
6518
+ if (options.json) {
6519
+ console.log(JSON.stringify(data, null, 2));
6520
+ return;
6521
+ }
6522
+ console.log();
6523
+ console.log("Account Balance");
6524
+ console.log("───────────────");
6525
+ const balanceColor = data.balance_cents > 0 ? "\x1B[32m" : "\x1B[33m";
6526
+ const reset = "\x1B[0m";
6527
+ console.log(` Balance: ${balanceColor}${data.balance_formatted}${reset}`);
6528
+ console.log(` Status: ${data.billing_status}`);
6529
+ console.log(` Can Deploy: ${data.can_deploy ? "\x1B[32mYes\x1B[0m" : "\x1B[31mNo\x1B[0m"}`);
6530
+ if (!data.can_deploy) {
6531
+ console.log();
6532
+ console.log("\x1B[33m ⚠ Add balance to enable deployments\x1B[0m");
6533
+ }
6534
+ console.log();
6535
+ console.log("Current Period Usage");
6536
+ console.log("────────────────────");
6537
+ console.log(` Period: ${formatDate(data.current_period.start)} - ${formatDate(data.current_period.end)}`);
6538
+ console.log();
6539
+ const usage = data.current_period.usage;
6540
+ const included = data.current_period.included;
6541
+ const cost = data.current_period.estimated_cost;
6542
+ console.log(" Resource Used Included Overage");
6543
+ console.log(" ─────────────────────────────────────────────────────");
6544
+ console.log(` Storage ${usage.storage_gb.toFixed(2)} GB ${included.storage_gb} GB ${cost.storage_cents > 0 ? formatCents(cost.storage_cents) : "-"}`);
6545
+ console.log(` Requests ${usage.requests_millions.toFixed(2)}M ${included.requests_millions}M ${cost.requests_cents > 0 ? formatCents(cost.requests_cents) : "-"}`);
6546
+ console.log(` Form Submissions ${usage.form_submissions} ${included.form_submissions} ${cost.forms_cents > 0 ? formatCents(cost.forms_cents) : "-"}`);
6547
+ console.log(` Custom Domains ${usage.domains_count} ${included.domains_count} ${cost.domains_cents > 0 ? formatCents(cost.domains_cents) : "-"}`);
6548
+ console.log();
6549
+ console.log(` Base: ${formatCents(cost.base_cents)}/month`);
6550
+ if (cost.usage_total_cents > 0) {
6551
+ console.log(` Usage: +${formatCents(cost.usage_total_cents)}`);
6552
+ }
6553
+ console.log(` ─────────────────────────────────────────────────────`);
6554
+ console.log(` Estimated Total: ${formatCents(cost.total_cents)}`);
6555
+ console.log();
6556
+ } catch (err) {
6557
+ const message = err instanceof Error ? err.message : String(err);
6558
+ displayError({
6559
+ code: "network_error",
6560
+ message: `Failed to fetch billing info: ${message}`
6561
+ });
6562
+ process.exit(ExitCode.NETWORK_ERROR);
6563
+ }
6564
+ });
6565
+ var billingBillsCommand = new Command2("bills").description("Show billing history").option("--json", "Output as JSON").option("--limit <n>", "Number of bills to show", "10").action(async (options) => {
6566
+ const token = loadToken();
6567
+ if (!token) {
6568
+ handleAuthError();
6569
+ }
6570
+ try {
6571
+ const client = getClient(token);
6572
+ const res = await client.billing.bills.$get({
6573
+ query: { limit: options.limit }
6574
+ });
6575
+ if (!res.ok) {
6576
+ await handleApiError(res);
6577
+ }
6578
+ const { data: bills, pagination } = await res.json();
6579
+ if (options.json) {
6580
+ console.log(JSON.stringify({ bills, pagination }, null, 2));
6581
+ return;
6582
+ }
6583
+ console.log();
6584
+ console.log("Billing History");
6585
+ console.log("───────────────");
6586
+ if (bills.length === 0) {
6587
+ console.log(" No billing history yet");
6588
+ console.log();
6589
+ return;
6590
+ }
6591
+ console.log();
6592
+ console.log(" Period Status Total");
6593
+ console.log(" ─────────────────────────────────────");
6594
+ for (const bill of bills) {
6595
+ const statusColor = bill.status === "paid" ? "\x1B[32m" : bill.status === "finalized" ? "\x1B[33m" : "\x1B[90m";
6596
+ const reset = "\x1B[0m";
6597
+ console.log(` ${formatMonth(bill.billing_month).padEnd(18)} ${statusColor}${bill.status.padEnd(10)}${reset} ${formatCents(bill.final_amount_cents)}`);
6598
+ }
6599
+ console.log();
6600
+ if (pagination.total > bills.length) {
6601
+ console.log(` Showing ${bills.length} of ${pagination.total} bills`);
6602
+ console.log();
6603
+ }
6604
+ } catch (err) {
6605
+ const message = err instanceof Error ? err.message : String(err);
6606
+ displayError({
6607
+ code: "network_error",
6608
+ message: `Failed to fetch bills: ${message}`
6609
+ });
6610
+ process.exit(ExitCode.NETWORK_ERROR);
6611
+ }
6612
+ });
6613
+ var billingAdjustmentsCommand = new Command2("adjustments").description("Show balance adjustment history (credits)").option("--json", "Output as JSON").option("--limit <n>", "Number of adjustments to show", "10").action(async (options) => {
6614
+ const token = loadToken();
6615
+ if (!token) {
6616
+ handleAuthError();
6617
+ }
6618
+ try {
6619
+ const client = getClient(token);
6620
+ const res = await client.billing.adjustments.$get({
6621
+ query: { limit: options.limit }
6622
+ });
6623
+ if (!res.ok) {
6624
+ await handleApiError(res);
6625
+ }
6626
+ const { data: adjustments, pagination } = await res.json();
6627
+ if (options.json) {
6628
+ console.log(JSON.stringify({ adjustments, pagination }, null, 2));
6629
+ return;
6630
+ }
6631
+ console.log();
6632
+ console.log("Balance Adjustments");
6633
+ console.log("───────────────────");
6634
+ if (adjustments.length === 0) {
6635
+ console.log(" No balance adjustments yet");
6636
+ console.log();
6637
+ return;
6638
+ }
6639
+ console.log();
6640
+ console.log(" Date Reason Amount");
6641
+ console.log(" ────────────────────────────────────────");
6642
+ for (const adj of adjustments) {
6643
+ console.log(` ${formatDate(adj.created_at).padEnd(12)} ${getReasonLabel(adj.reason).padEnd(18)} \x1B[32m+${formatCents(adj.amount_cents)}\x1B[0m`);
6644
+ if (adj.description) {
6645
+ console.log(` ${adj.description}`);
6646
+ }
6647
+ }
6648
+ console.log();
6649
+ if (pagination.total > adjustments.length) {
6650
+ console.log(` Showing ${adjustments.length} of ${pagination.total} adjustments`);
6651
+ console.log();
6652
+ }
6653
+ } catch (err) {
6654
+ const message = err instanceof Error ? err.message : String(err);
6655
+ displayError({
6656
+ code: "network_error",
6657
+ message: `Failed to fetch adjustments: ${message}`
6658
+ });
6659
+ process.exit(ExitCode.NETWORK_ERROR);
6660
+ }
6661
+ });
6662
+ var billingCommand = new Command2("billing").description("View account balance and billing information").addCommand(billingUsageCommand).addCommand(billingBillsCommand).addCommand(billingAdjustmentsCommand).action(async () => {
6663
+ await billingUsageCommand.parseAsync(["usage"], { from: "user" });
6664
+ });
5101
6665
 
5102
6666
  // src/commands/init.ts
5103
6667
  import { writeFileSync, existsSync as existsSync2 } from "node:fs";
@@ -5150,18 +6714,250 @@ var initCommand = new Command2("init").description("Create a zerodeploy.json con
5150
6714
  console.log(" zerodeploy deploy --build # build + deploy");
5151
6715
  });
5152
6716
 
6717
+ // src/commands/account-delete.ts
6718
+ import * as readline5 from "readline";
6719
+ function prompt4(question) {
6720
+ const rl = readline5.createInterface({
6721
+ input: process.stdin,
6722
+ output: process.stdout
6723
+ });
6724
+ return new Promise((resolve5) => {
6725
+ rl.question(question, (answer) => {
6726
+ rl.close();
6727
+ resolve5(answer);
6728
+ });
6729
+ });
6730
+ }
6731
+ 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) => {
6732
+ const token = loadToken();
6733
+ if (!token) {
6734
+ displayAuthError();
6735
+ return;
6736
+ }
6737
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6738
+ if (!emailRegex.test(email)) {
6739
+ displayError({ code: "validation_error", message: "Invalid email address format" });
6740
+ return;
6741
+ }
6742
+ try {
6743
+ const client = getClient(token);
6744
+ const res = await client.auth.me.$patch({
6745
+ json: { email }
6746
+ });
6747
+ if (!res.ok) {
6748
+ const body = await res.json();
6749
+ displayError(parseApiError(body));
6750
+ return;
6751
+ }
6752
+ console.log(`
6753
+ ✅ Email updated to: ${email}`);
6754
+ console.log(` You will now receive deployment notifications at this address.
6755
+ `);
6756
+ } catch (err) {
6757
+ if (err instanceof Error) {
6758
+ displayNetworkError(err);
6759
+ } else {
6760
+ displayError({ code: "unknown_error", message: "Failed to update email" });
6761
+ }
6762
+ }
6763
+ })).addCommand(new Command2("delete").description("Permanently delete your account and all data").option("--force", "Skip confirmation prompts").action(async (options) => {
6764
+ const token = loadToken();
6765
+ if (!token) {
6766
+ displayAuthError();
6767
+ return;
6768
+ }
6769
+ if (!options.force) {
6770
+ console.log(`
6771
+ ⚠️ WARNING: This will permanently delete:`);
6772
+ console.log(" - Your account");
6773
+ console.log(" - All organizations you own");
6774
+ console.log(" - All sites and deployments");
6775
+ console.log(" - All custom domains");
6776
+ console.log(" - All deploy tokens");
6777
+ console.log(`
6778
+ This action CANNOT be undone.
6779
+ `);
6780
+ const answer1 = await prompt4('Type "delete my account" to confirm: ');
6781
+ if (answer1 !== "delete my account") {
6782
+ console.log("Cancelled.");
6783
+ return;
6784
+ }
6785
+ const answer2 = await prompt4("Are you absolutely sure? (y/N) ");
6786
+ if (answer2.toLowerCase() !== "y") {
6787
+ console.log("Cancelled.");
6788
+ return;
6789
+ }
6790
+ }
6791
+ try {
6792
+ const client = getClient(token);
6793
+ const res = await client.auth.me.$delete();
6794
+ if (!res.ok) {
6795
+ const body = await res.json();
6796
+ displayError(parseApiError(body));
6797
+ return;
6798
+ }
6799
+ const result = await res.json();
6800
+ deleteToken();
6801
+ console.log(`
6802
+ ✅ Account deleted successfully.`);
6803
+ console.log(` Deleted: ${result.deleted.orgs} org(s), ${result.deleted.sites} site(s), ${result.deleted.deployments} deployment(s)`);
6804
+ console.log(`
6805
+ Your local authentication token has been removed.`);
6806
+ console.log(` Thank you for using ZeroDeploy.
6807
+ `);
6808
+ } catch (err) {
6809
+ if (err instanceof Error) {
6810
+ displayNetworkError(err);
6811
+ } else {
6812
+ displayError({ code: "unknown_error", message: "Failed to delete account" });
6813
+ }
6814
+ }
6815
+ }));
6816
+
6817
+ // src/commands/inspect.ts
6818
+ var VERSION = "0.1.3";
6819
+ function getCommandByPath(rootCommand, path3) {
6820
+ let current = rootCommand;
6821
+ for (const name of path3) {
6822
+ const subcommand = current.commands.find((cmd) => cmd.name() === name);
6823
+ if (!subcommand) {
6824
+ return null;
6825
+ }
6826
+ current = subcommand;
6827
+ }
6828
+ return current;
6829
+ }
6830
+ function extractArguments(cmd) {
6831
+ const args = cmd.registeredArguments || [];
6832
+ return args.map((arg) => {
6833
+ const metadata = {
6834
+ name: arg.name(),
6835
+ description: arg.description || "",
6836
+ required: arg.required
6837
+ };
6838
+ if (arg.defaultValue !== undefined) {
6839
+ metadata.default = String(arg.defaultValue);
6840
+ }
6841
+ return metadata;
6842
+ });
6843
+ }
6844
+ function extractOptions(cmd) {
6845
+ const options = cmd.options || [];
6846
+ return options.filter((opt) => {
6847
+ const longName = opt.long?.replace(/^--/, "") || "";
6848
+ return longName !== "help" && longName !== "version";
6849
+ }).map((opt) => {
6850
+ const longName = opt.long?.replace(/^--/, "") || "";
6851
+ const shortName = opt.short?.replace(/^-/, "");
6852
+ const metadata = {
6853
+ name: longName,
6854
+ description: opt.description || "",
6855
+ required: opt.mandatory || false
6856
+ };
6857
+ if (shortName) {
6858
+ metadata.short = shortName;
6859
+ }
6860
+ if (opt.flags.includes("<") || opt.flags.includes("[")) {
6861
+ metadata.type = "string";
6862
+ } else {
6863
+ metadata.type = "boolean";
6864
+ }
6865
+ if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
6866
+ metadata.default = String(opt.defaultValue);
6867
+ }
6868
+ return metadata;
6869
+ });
6870
+ }
6871
+ function generateExamples(cmd, fullPath) {
6872
+ const examples = [];
6873
+ const cmdPath = ["zerodeploy", ...fullPath].join(" ");
6874
+ const args = cmd.registeredArguments || [];
6875
+ let basicExample = cmdPath;
6876
+ for (const arg of args) {
6877
+ if (arg.required) {
6878
+ basicExample += ` <${arg.name()}>`;
6879
+ }
6880
+ }
6881
+ if (basicExample !== cmdPath || args.length === 0) {
6882
+ examples.push(basicExample);
6883
+ }
6884
+ const cmdName = fullPath[fullPath.length - 1];
6885
+ const parentName = fullPath[fullPath.length - 2];
6886
+ if (cmdName === "list" && parentName === "org") {
6887
+ examples.push("zerodeploy org list");
6888
+ } else if (cmdName === "create" && parentName === "org") {
6889
+ examples.push("zerodeploy org create my-company");
6890
+ } else if (cmdName === "list" && parentName === "site") {
6891
+ examples.push("zerodeploy site list acme");
6892
+ } else if (cmdName === "create" && parentName === "site") {
6893
+ examples.push('zerodeploy site create acme "My Site"');
6894
+ } else if (cmdName === "deploy") {
6895
+ examples.push("zerodeploy deploy");
6896
+ examples.push("zerodeploy deploy ./dist --org acme --site web");
6897
+ }
6898
+ return examples;
6899
+ }
6900
+ function getCommandMetadata(rootCommand, path3) {
6901
+ if (path3.length === 0) {
6902
+ const commands = rootCommand.commands.map((cmd2) => cmd2.name()).filter((name) => name !== "help").sort();
6903
+ return {
6904
+ name: "zerodeploy",
6905
+ version: VERSION,
6906
+ commands
6907
+ };
6908
+ }
6909
+ const cmd = getCommandByPath(rootCommand, path3);
6910
+ if (!cmd) {
6911
+ return { error: `Command not found: ${path3.join(" ")}` };
6912
+ }
6913
+ const metadata = {
6914
+ name: cmd.name(),
6915
+ description: cmd.description() || ""
6916
+ };
6917
+ const subcommands = cmd.commands.map((sub) => sub.name()).filter((name) => name !== "help");
6918
+ if (subcommands.length > 0) {
6919
+ metadata.subcommands = subcommands.sort();
6920
+ return metadata;
6921
+ }
6922
+ metadata.usage = `zerodeploy ${path3.join(" ")}` + (cmd.registeredArguments || []).map((arg) => arg.required ? ` <${arg.name()}>` : ` [${arg.name()}]`).join("");
6923
+ const args = extractArguments(cmd);
6924
+ if (args.length > 0) {
6925
+ metadata.arguments = args;
6926
+ }
6927
+ const options = extractOptions(cmd);
6928
+ if (options.length > 0) {
6929
+ metadata.options = options;
6930
+ }
6931
+ const examples = generateExamples(cmd, path3);
6932
+ if (examples.length > 0) {
6933
+ metadata.examples = [...new Set(examples)];
6934
+ }
6935
+ return metadata;
6936
+ }
6937
+ function createInspectCommand(rootProgram) {
6938
+ return new Command2("inspect").description("Output command metadata as JSON (for LLM/automation use)").argument("[command...]", 'Command path to inspect (e.g., "org create")').action((commandPath) => {
6939
+ const metadata = getCommandMetadata(rootProgram, commandPath || []);
6940
+ console.log(JSON.stringify(metadata, null, 2));
6941
+ });
6942
+ }
6943
+
5153
6944
  // src/index.ts
5154
6945
  var program3 = new Command;
5155
- program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.0").enablePositionalOptions();
6946
+ program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.3").enablePositionalOptions();
5156
6947
  program3.addCommand(loginCommand);
5157
6948
  program3.addCommand(logoutCommand);
5158
6949
  program3.addCommand(whoamiCommand);
6950
+ program3.addCommand(usageCommand);
5159
6951
  program3.addCommand(orgCommand);
5160
6952
  program3.addCommand(siteCommand);
5161
6953
  program3.addCommand(domainCommand);
6954
+ program3.addCommand(formCommand);
5162
6955
  program3.addCommand(deployCommand);
5163
6956
  program3.addCommand(deploymentsCommand);
5164
6957
  program3.addCommand(rollbackCommand);
5165
6958
  program3.addCommand(tokenCommand);
6959
+ program3.addCommand(billingCommand);
5166
6960
  program3.addCommand(initCommand);
6961
+ program3.addCommand(accountCommand);
6962
+ program3.addCommand(createInspectCommand(program3));
5167
6963
  program3.parse(process.argv);