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