@zerodeploy/cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -16
- package/dist/cli.js +1344 -209
- 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();
|
|
@@ -3491,16 +3952,15 @@ var domainListCommand = new Command2("list").description("List custom domains fo
|
|
|
3491
3952
|
return;
|
|
3492
3953
|
}
|
|
3493
3954
|
try {
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
}
|
|
3955
|
+
const client = getClient(token);
|
|
3956
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
|
|
3957
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
3498
3958
|
});
|
|
3499
3959
|
if (!res.ok) {
|
|
3500
3960
|
const error = await res.json();
|
|
3501
3961
|
throw new Error(error.error || `API Error ${res.status}`);
|
|
3502
3962
|
}
|
|
3503
|
-
const domains = await res.json();
|
|
3963
|
+
const { data: domains } = await res.json();
|
|
3504
3964
|
if (domains.length === 0) {
|
|
3505
3965
|
console.log("No custom domains configured.");
|
|
3506
3966
|
console.log();
|
|
@@ -3530,16 +3990,15 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
|
|
|
3530
3990
|
return;
|
|
3531
3991
|
}
|
|
3532
3992
|
try {
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
}
|
|
3993
|
+
const client = getClient(token);
|
|
3994
|
+
const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
|
|
3995
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
3537
3996
|
});
|
|
3538
3997
|
if (!listRes.ok) {
|
|
3539
3998
|
const error = await listRes.json();
|
|
3540
3999
|
throw new Error(error.error || `API Error ${listRes.status}`);
|
|
3541
4000
|
}
|
|
3542
|
-
const domains = await listRes.json();
|
|
4001
|
+
const { data: domains } = await listRes.json();
|
|
3543
4002
|
const domain = domains.find((d) => d.domain === domainName);
|
|
3544
4003
|
if (!domain) {
|
|
3545
4004
|
console.error(`
|
|
@@ -3549,12 +4008,8 @@ var domainVerifyCommand = new Command2("verify").description("Verify ownership o
|
|
|
3549
4008
|
console.log(` zerodeploy domain add ${domainName} --org ${options.org} --site ${options.site}`);
|
|
3550
4009
|
return;
|
|
3551
4010
|
}
|
|
3552
|
-
const res = await
|
|
3553
|
-
|
|
3554
|
-
headers: {
|
|
3555
|
-
Authorization: `Bearer ${token}`,
|
|
3556
|
-
"Content-Type": "application/json"
|
|
3557
|
-
}
|
|
4011
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].verify.$post({
|
|
4012
|
+
param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
|
|
3558
4013
|
});
|
|
3559
4014
|
const data = await res.json();
|
|
3560
4015
|
if (!res.ok) {
|
|
@@ -3625,16 +4080,15 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
|
|
|
3625
4080
|
return;
|
|
3626
4081
|
}
|
|
3627
4082
|
try {
|
|
3628
|
-
const
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
}
|
|
4083
|
+
const client = getClient(token);
|
|
4084
|
+
const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
|
|
4085
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
3632
4086
|
});
|
|
3633
4087
|
if (!listRes.ok) {
|
|
3634
4088
|
const error = await listRes.json();
|
|
3635
4089
|
throw new Error(error.error || `API Error ${listRes.status}`);
|
|
3636
4090
|
}
|
|
3637
|
-
const domains = await listRes.json();
|
|
4091
|
+
const { data: domains } = await listRes.json();
|
|
3638
4092
|
const domain = domains.find((d) => d.domain === domainName);
|
|
3639
4093
|
if (!domain) {
|
|
3640
4094
|
console.error(`
|
|
@@ -3644,11 +4098,8 @@ var domainRemoveCommand = new Command2("remove").description("Remove a custom do
|
|
|
3644
4098
|
console.log(` zerodeploy domain list --org ${options.org} --site ${options.site}`);
|
|
3645
4099
|
return;
|
|
3646
4100
|
}
|
|
3647
|
-
const res = await
|
|
3648
|
-
|
|
3649
|
-
headers: {
|
|
3650
|
-
Authorization: `Bearer ${token}`
|
|
3651
|
-
}
|
|
4101
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].$delete({
|
|
4102
|
+
param: { orgSlug: options.org, siteSlug: options.site, domainId: domain.id }
|
|
3652
4103
|
});
|
|
3653
4104
|
if (!res.ok) {
|
|
3654
4105
|
const error = await res.json();
|
|
@@ -3681,58 +4132,285 @@ var domainRedirectCommand = new Command2("redirect").description("Set redirect m
|
|
|
3681
4132
|
console.log("Not logged in. Run: zerodeploy login");
|
|
3682
4133
|
return;
|
|
3683
4134
|
}
|
|
3684
|
-
const validModes = ["none", "www_to_apex", "apex_to_www"];
|
|
3685
|
-
if (!validModes.includes(options.mode)) {
|
|
3686
|
-
console.error(`Invalid mode: ${options.mode}`);
|
|
3687
|
-
console.error("Valid modes: none, www_to_apex, apex_to_www");
|
|
4135
|
+
const validModes = ["none", "www_to_apex", "apex_to_www"];
|
|
4136
|
+
if (!validModes.includes(options.mode)) {
|
|
4137
|
+
console.error(`Invalid mode: ${options.mode}`);
|
|
4138
|
+
console.error("Valid modes: none, www_to_apex, apex_to_www");
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
try {
|
|
4142
|
+
const client = getClient(token);
|
|
4143
|
+
const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].domains.$get({
|
|
4144
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
4145
|
+
});
|
|
4146
|
+
if (!listRes.ok) {
|
|
4147
|
+
const error = await listRes.json();
|
|
4148
|
+
throw new Error(error.error || `API Error ${listRes.status}`);
|
|
4149
|
+
}
|
|
4150
|
+
const { data: domains } = await listRes.json();
|
|
4151
|
+
const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
|
|
4152
|
+
if (!targetDomain) {
|
|
4153
|
+
console.error(`Domain not found: ${domain}`);
|
|
4154
|
+
console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].domains[":domainId"].redirect.$patch({
|
|
4158
|
+
param: { orgSlug: options.org, siteSlug: options.site, domainId: targetDomain.id },
|
|
4159
|
+
json: { redirectMode: options.mode }
|
|
4160
|
+
});
|
|
4161
|
+
if (!res.ok) {
|
|
4162
|
+
const error = await res.json();
|
|
4163
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
4164
|
+
}
|
|
4165
|
+
const data = await res.json();
|
|
4166
|
+
console.log(`
|
|
4167
|
+
✅ Redirect mode updated for ${data.domain}`);
|
|
4168
|
+
console.log(` Mode: ${formatRedirectMode(data.redirect_mode)}`);
|
|
4169
|
+
console.log();
|
|
4170
|
+
} catch (err) {
|
|
4171
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
4172
|
+
console.error("Failed to update redirect mode:", message);
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
|
|
4176
|
+
// src/commands/domain/index.ts
|
|
4177
|
+
var domainCommand = new Command2("domain").description("Manage custom domains").addCommand(domainAddCommand).addCommand(domainListCommand).addCommand(domainVerifyCommand).addCommand(domainRemoveCommand).addCommand(domainRedirectCommand);
|
|
4178
|
+
|
|
4179
|
+
// src/commands/form/list.ts
|
|
4180
|
+
var formListCommand = new Command2("list").description("List forms for a site").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").action(async (options) => {
|
|
4181
|
+
const token = loadToken();
|
|
4182
|
+
if (!token) {
|
|
4183
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
try {
|
|
4187
|
+
const client = getClient(token);
|
|
4188
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms.$get({
|
|
4189
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
4190
|
+
});
|
|
4191
|
+
if (!res.ok) {
|
|
4192
|
+
const error = await res.json();
|
|
4193
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
4194
|
+
}
|
|
4195
|
+
const { data: forms } = await res.json();
|
|
4196
|
+
if (forms.length === 0) {
|
|
4197
|
+
console.log("No forms found.");
|
|
4198
|
+
console.log();
|
|
4199
|
+
console.log("Forms are created automatically when visitors submit to /_forms/<name>");
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
console.log("Forms:");
|
|
4203
|
+
console.log();
|
|
4204
|
+
console.log(" NAME SUBMISSIONS CREATED");
|
|
4205
|
+
console.log(" " + "-".repeat(60));
|
|
4206
|
+
for (const f of forms) {
|
|
4207
|
+
const created = new Date(f.created_at).toLocaleDateString();
|
|
4208
|
+
console.log(` ${f.name.padEnd(30)} ${String(f.submission_count).padStart(11)} ${created}`);
|
|
4209
|
+
}
|
|
4210
|
+
console.log();
|
|
4211
|
+
console.log("Export submissions with:");
|
|
4212
|
+
console.log(` zerodeploy form export <name> --org ${options.org} --site ${options.site}`);
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
4215
|
+
console.error("Failed to list forms:", message);
|
|
4216
|
+
}
|
|
4217
|
+
});
|
|
4218
|
+
|
|
4219
|
+
// src/commands/form/submissions.ts
|
|
4220
|
+
var formSubmissionsCommand = new Command2("submissions").description("View recent form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--limit <n>", "Number of submissions to show", "10").option("--offset <n>", "Offset for pagination", "0").action(async (name, options) => {
|
|
4221
|
+
const token = loadToken();
|
|
4222
|
+
if (!token) {
|
|
4223
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
try {
|
|
4227
|
+
const client = getClient(token);
|
|
4228
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].submissions.$get({
|
|
4229
|
+
param: { orgSlug: options.org, siteSlug: options.site, formName: name },
|
|
4230
|
+
query: { limit: options.limit, offset: options.offset }
|
|
4231
|
+
});
|
|
4232
|
+
if (!res.ok) {
|
|
4233
|
+
const error = await res.json();
|
|
4234
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
4235
|
+
}
|
|
4236
|
+
const data = await res.json();
|
|
4237
|
+
const { form, submissions, total, limit, offset } = data;
|
|
4238
|
+
if (submissions.length === 0) {
|
|
4239
|
+
console.log(`No submissions found for form "${name}".`);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
console.log(`Form: ${form.name}`);
|
|
4243
|
+
console.log(`Total submissions: ${total}`);
|
|
4244
|
+
if (form.notification_email) {
|
|
4245
|
+
console.log(`Notifications: ${form.notification_email}`);
|
|
4246
|
+
}
|
|
4247
|
+
console.log();
|
|
4248
|
+
if (total > limit) {
|
|
4249
|
+
const start = offset + 1;
|
|
4250
|
+
const end = Math.min(offset + submissions.length, total);
|
|
4251
|
+
console.log(`Showing ${start}-${end} of ${total}`);
|
|
4252
|
+
console.log();
|
|
4253
|
+
}
|
|
4254
|
+
for (const sub of submissions) {
|
|
4255
|
+
const date = new Date(sub.created_at).toLocaleString();
|
|
4256
|
+
const shortId = sub.id.slice(0, 8);
|
|
4257
|
+
console.log(` ${shortId} ${date}`);
|
|
4258
|
+
const entries = Object.entries(sub.data);
|
|
4259
|
+
for (const [key, value] of entries) {
|
|
4260
|
+
const displayValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
4261
|
+
const truncated = displayValue.length > 60 ? displayValue.slice(0, 57) + "..." : displayValue;
|
|
4262
|
+
console.log(` ${key}: ${truncated}`);
|
|
4263
|
+
}
|
|
4264
|
+
const meta = [];
|
|
4265
|
+
if (sub.ip_address)
|
|
4266
|
+
meta.push(`IP: ${sub.ip_address}`);
|
|
4267
|
+
if (sub.referrer)
|
|
4268
|
+
meta.push(`from: ${sub.referrer}`);
|
|
4269
|
+
if (meta.length > 0) {
|
|
4270
|
+
console.log(` [${meta.join(", ")}]`);
|
|
4271
|
+
}
|
|
4272
|
+
console.log();
|
|
4273
|
+
}
|
|
4274
|
+
if (total > offset + submissions.length) {
|
|
4275
|
+
const nextOffset = offset + parseInt(options.limit);
|
|
4276
|
+
console.log(`View more with: --offset ${nextOffset}`);
|
|
4277
|
+
}
|
|
4278
|
+
console.log("Export all to CSV with:");
|
|
4279
|
+
console.log(` zerodeploy form export ${name} --org ${options.org} --site ${options.site}`);
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
4282
|
+
console.error("Failed to get submissions:", message);
|
|
4283
|
+
}
|
|
4284
|
+
});
|
|
4285
|
+
|
|
4286
|
+
// src/commands/form/export.ts
|
|
4287
|
+
import { writeFile } from "fs/promises";
|
|
4288
|
+
import { resolve } from "path";
|
|
4289
|
+
var formExportCommand = new Command2("export").description("Export form submissions as CSV").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("-o, --output <file>", "Output file path (default: <name>-submissions.csv)").action(async (name, options) => {
|
|
4290
|
+
const token = loadToken();
|
|
4291
|
+
if (!token) {
|
|
4292
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
try {
|
|
4296
|
+
const client = getClient(token);
|
|
4297
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].export.$get({
|
|
4298
|
+
param: { orgSlug: options.org, siteSlug: options.site, formName: name }
|
|
4299
|
+
});
|
|
4300
|
+
if (res.status === 204) {
|
|
4301
|
+
console.log("No submissions to export.");
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
if (!res.ok) {
|
|
4305
|
+
const error = await res.json();
|
|
4306
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
4307
|
+
}
|
|
4308
|
+
const csv = await res.text();
|
|
4309
|
+
const outputPath = options.output || `${name}-submissions.csv`;
|
|
4310
|
+
const fullPath = resolve(process.cwd(), outputPath);
|
|
4311
|
+
await writeFile(fullPath, csv, "utf-8");
|
|
4312
|
+
const lineCount = csv.split(`
|
|
4313
|
+
`).length - 1;
|
|
4314
|
+
console.log(`Exported ${lineCount} submissions to ${outputPath}`);
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
4317
|
+
console.error("Failed to export form:", message);
|
|
4318
|
+
}
|
|
4319
|
+
});
|
|
4320
|
+
|
|
4321
|
+
// src/commands/form/delete.ts
|
|
4322
|
+
import * as readline3 from "readline";
|
|
4323
|
+
function prompt3(question) {
|
|
4324
|
+
const rl = readline3.createInterface({
|
|
4325
|
+
input: process.stdin,
|
|
4326
|
+
output: process.stdout
|
|
4327
|
+
});
|
|
4328
|
+
return new Promise((resolve2) => {
|
|
4329
|
+
rl.question(question, (answer) => {
|
|
4330
|
+
rl.close();
|
|
4331
|
+
resolve2(answer);
|
|
4332
|
+
});
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
var formDeleteCommand = new Command2("delete").description("Delete a form and all its submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--force", "Skip confirmation prompt").action(async (name, options) => {
|
|
4336
|
+
const token = loadToken();
|
|
4337
|
+
if (!token) {
|
|
4338
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
if (!options.force) {
|
|
4342
|
+
const answer = await prompt3(`Are you sure you want to delete form "${name}" and all its submissions? This cannot be undone. (y/N) `);
|
|
4343
|
+
if (answer.toLowerCase() !== "y") {
|
|
4344
|
+
console.log("Cancelled.");
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
try {
|
|
4349
|
+
const client = getClient(token);
|
|
4350
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$delete({
|
|
4351
|
+
param: { orgSlug: options.org, siteSlug: options.site, formName: name }
|
|
4352
|
+
});
|
|
4353
|
+
if (!res.ok) {
|
|
4354
|
+
const error = await res.json();
|
|
4355
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
4356
|
+
}
|
|
4357
|
+
const result = await res.json();
|
|
4358
|
+
console.log(`✅ ${result.message}`);
|
|
4359
|
+
} catch (err) {
|
|
4360
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
4361
|
+
console.error("Failed to delete form:", message);
|
|
4362
|
+
}
|
|
4363
|
+
});
|
|
4364
|
+
|
|
4365
|
+
// src/commands/form/notify.ts
|
|
4366
|
+
var formNotifyCommand = new Command2("notify").description("Configure email notifications for form submissions").argument("<name>", "Form name").requiredOption("--org <orgSlug>", "Organization slug").requiredOption("--site <siteSlug>", "Site slug").option("--email <email>", "Email address to receive notifications").option("--disable", "Disable email notifications").action(async (name, options) => {
|
|
4367
|
+
const token = loadToken();
|
|
4368
|
+
if (!token) {
|
|
4369
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
if (options.email && options.disable) {
|
|
4373
|
+
console.error("Cannot use both --email and --disable options");
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
if (!options.email && !options.disable) {
|
|
4377
|
+
console.error("Please specify --email <email> or --disable");
|
|
4378
|
+
console.log();
|
|
4379
|
+
console.log("Examples:");
|
|
4380
|
+
console.log(` zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --email alerts@example.com`);
|
|
4381
|
+
console.log(` zerodeploy form notify ${name} --org ${options.org} --site ${options.site} --disable`);
|
|
3688
4382
|
return;
|
|
3689
4383
|
}
|
|
3690
4384
|
try {
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
|
-
if (!listRes.ok) {
|
|
3697
|
-
const error = await listRes.json();
|
|
3698
|
-
throw new Error(error.error || `API Error ${listRes.status}`);
|
|
3699
|
-
}
|
|
3700
|
-
const domains = await listRes.json();
|
|
3701
|
-
const targetDomain = domains.find((d) => d.domain === domain.toLowerCase());
|
|
3702
|
-
if (!targetDomain) {
|
|
3703
|
-
console.error(`Domain not found: ${domain}`);
|
|
3704
|
-
console.error(`Run 'zerodeploy domain list --org ${options.org} --site ${options.site}' to see configured domains.`);
|
|
3705
|
-
return;
|
|
3706
|
-
}
|
|
3707
|
-
const res = await fetch(`${API_URL}/orgs/${options.org}/sites/${options.site}/domains/${targetDomain.id}/redirect`, {
|
|
3708
|
-
method: "PATCH",
|
|
3709
|
-
headers: {
|
|
3710
|
-
Authorization: `Bearer ${token}`,
|
|
3711
|
-
"Content-Type": "application/json"
|
|
3712
|
-
},
|
|
3713
|
-
body: JSON.stringify({ redirectMode: options.mode })
|
|
4385
|
+
const client = getClient(token);
|
|
4386
|
+
const notificationEmail = options.disable ? null : options.email;
|
|
4387
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].forms[":formName"].$patch({
|
|
4388
|
+
param: { orgSlug: options.org, siteSlug: options.site, formName: name },
|
|
4389
|
+
json: { notification_email: notificationEmail }
|
|
3714
4390
|
});
|
|
3715
4391
|
if (!res.ok) {
|
|
3716
4392
|
const error = await res.json();
|
|
3717
4393
|
throw new Error(error.error || `API Error ${res.status}`);
|
|
3718
4394
|
}
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
4395
|
+
const { form } = await res.json();
|
|
4396
|
+
if (form.notification_email) {
|
|
4397
|
+
console.log(`Email notifications enabled for form "${name}"`);
|
|
4398
|
+
console.log(` Notifications will be sent to: ${form.notification_email}`);
|
|
4399
|
+
} else {
|
|
4400
|
+
console.log(`Email notifications disabled for form "${name}"`);
|
|
4401
|
+
}
|
|
3724
4402
|
} catch (err) {
|
|
3725
4403
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
3726
|
-
console.error("Failed to update
|
|
4404
|
+
console.error("Failed to update form notifications:", message);
|
|
3727
4405
|
}
|
|
3728
4406
|
});
|
|
3729
4407
|
|
|
3730
|
-
// src/commands/
|
|
3731
|
-
var
|
|
4408
|
+
// src/commands/form/index.ts
|
|
4409
|
+
var formCommand = new Command2("form").description("Manage form submissions").addCommand(formListCommand).addCommand(formSubmissionsCommand).addCommand(formExportCommand).addCommand(formDeleteCommand).addCommand(formNotifyCommand);
|
|
3732
4410
|
|
|
3733
4411
|
// src/commands/deploy/index.ts
|
|
3734
|
-
import { resolve as
|
|
3735
|
-
import { stat as stat2, writeFile } from "node:fs/promises";
|
|
4412
|
+
import { resolve as resolve4, basename } from "node:path";
|
|
4413
|
+
import { stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
3736
4414
|
import { spawn } from "node:child_process";
|
|
3737
4415
|
|
|
3738
4416
|
// src/utils/files.ts
|
|
@@ -3772,7 +4450,7 @@ async function scanDirectory(rootDir) {
|
|
|
3772
4450
|
await scan(rootDir);
|
|
3773
4451
|
return files;
|
|
3774
4452
|
}
|
|
3775
|
-
function
|
|
4453
|
+
function formatBytes2(bytes) {
|
|
3776
4454
|
if (bytes === 0)
|
|
3777
4455
|
return "0 B";
|
|
3778
4456
|
const k = 1024;
|
|
@@ -3783,7 +4461,7 @@ function formatBytes(bytes) {
|
|
|
3783
4461
|
|
|
3784
4462
|
// src/utils/framework.ts
|
|
3785
4463
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
3786
|
-
import { resolve } from "node:path";
|
|
4464
|
+
import { resolve as resolve2 } from "node:path";
|
|
3787
4465
|
var FRAMEWORKS = [
|
|
3788
4466
|
{
|
|
3789
4467
|
name: "Vite",
|
|
@@ -3857,7 +4535,7 @@ function hasDep(pkg, dep) {
|
|
|
3857
4535
|
}
|
|
3858
4536
|
async function detectFramework(cwd) {
|
|
3859
4537
|
try {
|
|
3860
|
-
const pkgPath =
|
|
4538
|
+
const pkgPath = resolve2(cwd, "package.json");
|
|
3861
4539
|
const pkgContent = await readFile2(pkgPath, "utf-8");
|
|
3862
4540
|
const pkg = JSON.parse(pkgContent);
|
|
3863
4541
|
for (const framework of FRAMEWORKS) {
|
|
@@ -4400,9 +5078,12 @@ function padToBlock(data) {
|
|
|
4400
5078
|
padded.set(data);
|
|
4401
5079
|
return padded;
|
|
4402
5080
|
}
|
|
4403
|
-
async function createTarArchive(files) {
|
|
5081
|
+
async function createTarArchive(files, onProgress) {
|
|
4404
5082
|
const parts = [];
|
|
4405
|
-
|
|
5083
|
+
const total = files.length;
|
|
5084
|
+
for (let i2 = 0;i2 < files.length; i2++) {
|
|
5085
|
+
const file = files[i2];
|
|
5086
|
+
onProgress?.(i2 + 1, total, file.path);
|
|
4406
5087
|
const content = await readFile3(file.absolutePath);
|
|
4407
5088
|
const header = createTarHeader(file.path, content.length);
|
|
4408
5089
|
const paddedContent = padToBlock(new Uint8Array(content));
|
|
@@ -4419,8 +5100,8 @@ async function createTarArchive(files) {
|
|
|
4419
5100
|
}
|
|
4420
5101
|
return tar;
|
|
4421
5102
|
}
|
|
4422
|
-
async function createTarGz(files) {
|
|
4423
|
-
const tar = await createTarArchive(files);
|
|
5103
|
+
async function createTarGz(files, onProgress) {
|
|
5104
|
+
const tar = await createTarArchive(files, onProgress);
|
|
4424
5105
|
return gzipSync(tar, { level: 6 });
|
|
4425
5106
|
}
|
|
4426
5107
|
function formatCompression(original, compressed) {
|
|
@@ -4430,10 +5111,10 @@ function formatCompression(original, compressed) {
|
|
|
4430
5111
|
|
|
4431
5112
|
// src/utils/project-config.ts
|
|
4432
5113
|
import { existsSync, readFileSync } from "node:fs";
|
|
4433
|
-
import { resolve as
|
|
5114
|
+
import { resolve as resolve3 } from "node:path";
|
|
4434
5115
|
var CONFIG_FILENAME = "zerodeploy.json";
|
|
4435
5116
|
function loadProjectConfig(cwd = process.cwd()) {
|
|
4436
|
-
const configPath =
|
|
5117
|
+
const configPath = resolve3(cwd, CONFIG_FILENAME);
|
|
4437
5118
|
if (!existsSync(configPath)) {
|
|
4438
5119
|
return {};
|
|
4439
5120
|
}
|
|
@@ -4447,32 +5128,139 @@ function loadProjectConfig(cwd = process.cwd()) {
|
|
|
4447
5128
|
}
|
|
4448
5129
|
}
|
|
4449
5130
|
function getConfigPath(cwd = process.cwd()) {
|
|
4450
|
-
return
|
|
5131
|
+
return resolve3(cwd, CONFIG_FILENAME);
|
|
4451
5132
|
}
|
|
4452
5133
|
|
|
4453
5134
|
// src/utils/prompt.ts
|
|
4454
|
-
import * as
|
|
5135
|
+
import * as readline4 from "node:readline";
|
|
4455
5136
|
async function confirm(message, defaultValue = true) {
|
|
4456
|
-
const rl =
|
|
5137
|
+
const rl = readline4.createInterface({
|
|
4457
5138
|
input: process.stdin,
|
|
4458
5139
|
output: process.stdout
|
|
4459
5140
|
});
|
|
4460
5141
|
const hint = defaultValue ? "[Y/n]" : "[y/N]";
|
|
4461
|
-
return new Promise((
|
|
5142
|
+
return new Promise((resolve4) => {
|
|
4462
5143
|
rl.question(`${message} ${hint} `, (answer) => {
|
|
4463
5144
|
rl.close();
|
|
4464
5145
|
const normalized = answer.trim().toLowerCase();
|
|
4465
5146
|
if (normalized === "") {
|
|
4466
|
-
|
|
5147
|
+
resolve4(defaultValue);
|
|
4467
5148
|
} else if (normalized === "y" || normalized === "yes") {
|
|
4468
|
-
|
|
5149
|
+
resolve4(true);
|
|
4469
5150
|
} else {
|
|
4470
|
-
|
|
5151
|
+
resolve4(false);
|
|
4471
5152
|
}
|
|
4472
5153
|
});
|
|
4473
5154
|
});
|
|
4474
5155
|
}
|
|
4475
5156
|
|
|
5157
|
+
// src/utils/progress.ts
|
|
5158
|
+
var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
5159
|
+
var SPINNER_INTERVAL = 80;
|
|
5160
|
+
var isTTY = process.stdout.isTTY;
|
|
5161
|
+
function clearLine() {
|
|
5162
|
+
if (isTTY) {
|
|
5163
|
+
process.stdout.write("\r\x1B[K");
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
function createSpinner(message) {
|
|
5167
|
+
let frameIndex = 0;
|
|
5168
|
+
let interval = null;
|
|
5169
|
+
let currentMessage = message;
|
|
5170
|
+
function render() {
|
|
5171
|
+
if (!isTTY)
|
|
5172
|
+
return;
|
|
5173
|
+
const frame = SPINNER_FRAMES[frameIndex];
|
|
5174
|
+
clearLine();
|
|
5175
|
+
process.stdout.write(`${frame} ${currentMessage}`);
|
|
5176
|
+
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
5177
|
+
}
|
|
5178
|
+
return {
|
|
5179
|
+
start() {
|
|
5180
|
+
if (!isTTY) {
|
|
5181
|
+
console.log(currentMessage);
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5184
|
+
render();
|
|
5185
|
+
interval = setInterval(render, SPINNER_INTERVAL);
|
|
5186
|
+
},
|
|
5187
|
+
stop(finalMessage) {
|
|
5188
|
+
if (interval) {
|
|
5189
|
+
clearInterval(interval);
|
|
5190
|
+
interval = null;
|
|
5191
|
+
}
|
|
5192
|
+
clearLine();
|
|
5193
|
+
if (finalMessage) {
|
|
5194
|
+
console.log(finalMessage);
|
|
5195
|
+
}
|
|
5196
|
+
},
|
|
5197
|
+
update(message2) {
|
|
5198
|
+
currentMessage = message2;
|
|
5199
|
+
if (!isTTY) {
|
|
5200
|
+
console.log(message2);
|
|
5201
|
+
}
|
|
5202
|
+
},
|
|
5203
|
+
succeed(message2) {
|
|
5204
|
+
if (interval) {
|
|
5205
|
+
clearInterval(interval);
|
|
5206
|
+
interval = null;
|
|
5207
|
+
}
|
|
5208
|
+
clearLine();
|
|
5209
|
+
console.log(`✓ ${message2}`);
|
|
5210
|
+
},
|
|
5211
|
+
fail(message2) {
|
|
5212
|
+
if (interval) {
|
|
5213
|
+
clearInterval(interval);
|
|
5214
|
+
interval = null;
|
|
5215
|
+
}
|
|
5216
|
+
clearLine();
|
|
5217
|
+
console.log(`✗ ${message2}`);
|
|
5218
|
+
},
|
|
5219
|
+
warn(message2) {
|
|
5220
|
+
if (interval) {
|
|
5221
|
+
clearInterval(interval);
|
|
5222
|
+
interval = null;
|
|
5223
|
+
}
|
|
5224
|
+
clearLine();
|
|
5225
|
+
console.log(`⚠ ${message2}`);
|
|
5226
|
+
}
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
function createProgressBar(options) {
|
|
5230
|
+
const { total, label, width = 20 } = options;
|
|
5231
|
+
let started = false;
|
|
5232
|
+
function render(current, suffix) {
|
|
5233
|
+
const percent = Math.min(current / total, 1);
|
|
5234
|
+
const filled = Math.round(percent * width);
|
|
5235
|
+
const empty = width - filled;
|
|
5236
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
5237
|
+
const countStr = `${current}/${total}`;
|
|
5238
|
+
const suffixStr = suffix ? ` ${suffix}` : "";
|
|
5239
|
+
if (isTTY) {
|
|
5240
|
+
clearLine();
|
|
5241
|
+
process.stdout.write(`${label} [${bar}] ${countStr}${suffixStr}`);
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
return {
|
|
5245
|
+
update(current, suffix) {
|
|
5246
|
+
if (!isTTY) {
|
|
5247
|
+
if (!started) {
|
|
5248
|
+
console.log(`${label}... ${total} files`);
|
|
5249
|
+
started = true;
|
|
5250
|
+
}
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
render(current, suffix);
|
|
5254
|
+
},
|
|
5255
|
+
done(message) {
|
|
5256
|
+
clearLine();
|
|
5257
|
+
if (message) {
|
|
5258
|
+
console.log(message);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
};
|
|
5262
|
+
}
|
|
5263
|
+
|
|
4476
5264
|
// src/commands/deploy/list.ts
|
|
4477
5265
|
var deployListCommand = new Command2("list").description("List deployments for a site").argument("<siteSlug>", "Site slug").requiredOption("--org <orgSlug>", "Organization slug").option("--limit <number>", "Number of deployments to show", "10").action(async (siteSlug, options) => {
|
|
4478
5266
|
const token = loadToken();
|
|
@@ -4490,7 +5278,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
|
|
|
4490
5278
|
console.error(`❌ ${error.error}`);
|
|
4491
5279
|
return;
|
|
4492
5280
|
}
|
|
4493
|
-
const deployments = await res.json();
|
|
5281
|
+
const { data: deployments } = await res.json();
|
|
4494
5282
|
const limit = parseInt(options.limit, 10);
|
|
4495
5283
|
const shown = deployments.slice(0, limit);
|
|
4496
5284
|
if (shown.length === 0) {
|
|
@@ -4501,7 +5289,7 @@ var deployListCommand = new Command2("list").description("List deployments for a
|
|
|
4501
5289
|
Deployments for ${options.org}/${siteSlug}:
|
|
4502
5290
|
`);
|
|
4503
5291
|
for (const d of shown) {
|
|
4504
|
-
const current = d.
|
|
5292
|
+
const current = d.is_current ? " ← current" : "";
|
|
4505
5293
|
const status = formatStatus2(d.status);
|
|
4506
5294
|
const date = new Date(d.created_at).toLocaleString();
|
|
4507
5295
|
console.log(` ${d.id.slice(0, 8)} ${status} ${date}${current}`);
|
|
@@ -4583,21 +5371,82 @@ var deployPromoteCommand = new Command2("promote").description("Promote a previe
|
|
|
4583
5371
|
function slugify(input) {
|
|
4584
5372
|
return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
4585
5373
|
}
|
|
5374
|
+
async function verifyDeployment(url, maxRetries = 3, delayMs = 2000) {
|
|
5375
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
5376
|
+
try {
|
|
5377
|
+
const controller = new AbortController;
|
|
5378
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
5379
|
+
const res = await fetch(url, {
|
|
5380
|
+
method: "GET",
|
|
5381
|
+
signal: controller.signal
|
|
5382
|
+
});
|
|
5383
|
+
clearTimeout(timeout);
|
|
5384
|
+
if (res.ok) {
|
|
5385
|
+
return { success: true, status: res.status };
|
|
5386
|
+
}
|
|
5387
|
+
if (attempt < maxRetries) {
|
|
5388
|
+
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
5389
|
+
continue;
|
|
5390
|
+
}
|
|
5391
|
+
return { success: false, status: res.status };
|
|
5392
|
+
} catch (err) {
|
|
5393
|
+
if (attempt < maxRetries) {
|
|
5394
|
+
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
5395
|
+
continue;
|
|
5396
|
+
}
|
|
5397
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
5398
|
+
return { success: false, error };
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
return { success: false, error: "Max retries exceeded" };
|
|
5402
|
+
}
|
|
5403
|
+
async function autoRollback(token, orgSlug, siteSlug, currentDeploymentId) {
|
|
5404
|
+
try {
|
|
5405
|
+
const client = getClient(token);
|
|
5406
|
+
const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].deployments.$get({
|
|
5407
|
+
param: { orgSlug, siteSlug }
|
|
5408
|
+
});
|
|
5409
|
+
if (!listRes.ok) {
|
|
5410
|
+
return { success: false, error: "Failed to fetch deployments" };
|
|
5411
|
+
}
|
|
5412
|
+
const { data: deployments } = await listRes.json();
|
|
5413
|
+
const previousDeployment = deployments.find((d) => d.id !== currentDeploymentId && d.status === "ready");
|
|
5414
|
+
if (!previousDeployment) {
|
|
5415
|
+
return { success: false, error: "No previous deployment to rollback to" };
|
|
5416
|
+
}
|
|
5417
|
+
const rollbackRes = await client.deployments[":id"].rollback.$post({
|
|
5418
|
+
param: { id: previousDeployment.id }
|
|
5419
|
+
});
|
|
5420
|
+
if (!rollbackRes.ok) {
|
|
5421
|
+
return { success: false, error: "Rollback request failed" };
|
|
5422
|
+
}
|
|
5423
|
+
return { success: true, deploymentId: previousDeployment.id };
|
|
5424
|
+
} catch (err) {
|
|
5425
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
5426
|
+
return { success: false, error };
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
4586
5429
|
async function runCommand(command, cwd) {
|
|
4587
5430
|
return new Promise((promiseResolve) => {
|
|
4588
|
-
const
|
|
5431
|
+
const parts = command.split(" ");
|
|
5432
|
+
const cmd = parts[0];
|
|
5433
|
+
if (!cmd) {
|
|
5434
|
+
promiseResolve({ success: false, output: "Empty command" });
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
5437
|
+
const args = parts.slice(1);
|
|
4589
5438
|
const proc = spawn(cmd, args, {
|
|
4590
5439
|
cwd,
|
|
4591
5440
|
shell: true,
|
|
4592
5441
|
stdio: ["inherit", "pipe", "pipe"]
|
|
4593
5442
|
});
|
|
4594
5443
|
let output = "";
|
|
4595
|
-
proc.stdout
|
|
5444
|
+
proc.stdout.on("data", (data) => {
|
|
4596
5445
|
const text = data.toString();
|
|
4597
5446
|
output += text;
|
|
4598
5447
|
process.stdout.write(text);
|
|
4599
5448
|
});
|
|
4600
|
-
proc.stderr
|
|
5449
|
+
proc.stderr.on("data", (data) => {
|
|
4601
5450
|
const text = data.toString();
|
|
4602
5451
|
output += text;
|
|
4603
5452
|
process.stderr.write(text);
|
|
@@ -4613,12 +5462,12 @@ async function runCommand(command, cwd) {
|
|
|
4613
5462
|
async function findBuildDirectory(cwd) {
|
|
4614
5463
|
const candidates = ["dist", "build", "out", "public", "."];
|
|
4615
5464
|
for (const dir of candidates) {
|
|
4616
|
-
const fullPath =
|
|
5465
|
+
const fullPath = resolve4(cwd, dir);
|
|
4617
5466
|
try {
|
|
4618
5467
|
const stats = await stat2(fullPath);
|
|
4619
5468
|
if (stats.isDirectory()) {
|
|
4620
5469
|
try {
|
|
4621
|
-
await stat2(
|
|
5470
|
+
await stat2(resolve4(fullPath, "index.html"));
|
|
4622
5471
|
return fullPath;
|
|
4623
5472
|
} catch {
|
|
4624
5473
|
if (dir !== ".")
|
|
@@ -4630,22 +5479,25 @@ async function findBuildDirectory(cwd) {
|
|
|
4630
5479
|
}
|
|
4631
5480
|
return null;
|
|
4632
5481
|
}
|
|
4633
|
-
async function uploadArchive(token, uploadUrl, archive) {
|
|
4634
|
-
const res = await
|
|
5482
|
+
async function uploadArchive(token, uploadUrl, archive, onRetry) {
|
|
5483
|
+
const res = await fetchWithRetry(uploadUrl, {
|
|
4635
5484
|
method: "POST",
|
|
4636
5485
|
headers: {
|
|
4637
5486
|
"Content-Type": "application/gzip",
|
|
4638
5487
|
Authorization: `Bearer ${token}`
|
|
4639
5488
|
},
|
|
4640
5489
|
body: archive
|
|
5490
|
+
}, {
|
|
5491
|
+
maxRetries: 3,
|
|
5492
|
+
onRetry
|
|
4641
5493
|
});
|
|
4642
5494
|
return res.ok;
|
|
4643
5495
|
}
|
|
4644
|
-
var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
|
|
5496
|
+
var deployCommand = new Command2("deploy").description("Deploy a site").argument("[site]", "Site slug").option("--org <org>", "Organization slug").option("--dir <directory>", "Directory to deploy (default: auto-detect)").option("--build", "Run build command before deploying").option("--no-build", "Skip build step").option("--build-command <command>", "Override build command").option("--install", "Run install command before building").option("--preview", "Deploy without setting as current (preview only)").option("--no-verify", "Skip deployment verification").option("--no-auto-rollback", "Disable automatic rollback on verification failure").option("--pr <number>", "PR number (for GitHub Actions)").option("--pr-title <title>", "PR title").option("--commit <sha>", "Commit SHA").option("--commit-message <message>", "Commit message").option("--branch <branch>", "Branch name").option("--github-output", "Output deployment info in GitHub Actions format").enablePositionalOptions().addCommand(deployListCommand).addCommand(deployRollbackCommand).addCommand(deployPromoteCommand).action(async (siteSlugArg, options) => {
|
|
4645
5497
|
const cwd = process.cwd();
|
|
4646
5498
|
const token = loadToken();
|
|
4647
5499
|
if (!token) {
|
|
4648
|
-
|
|
5500
|
+
displayAuthError();
|
|
4649
5501
|
return;
|
|
4650
5502
|
}
|
|
4651
5503
|
const config = loadProjectConfig(cwd);
|
|
@@ -4692,7 +5544,7 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
|
|
|
4692
5544
|
console.log(`Created site: ${site.subdomain}.zerodeploy.app`);
|
|
4693
5545
|
const configPath = getConfigPath(cwd);
|
|
4694
5546
|
const newConfig = { org: orgSlug, site: siteSlug, dir: dirOption || config.dir };
|
|
4695
|
-
await
|
|
5547
|
+
await writeFile2(configPath, JSON.stringify(newConfig, null, 2) + `
|
|
4696
5548
|
`);
|
|
4697
5549
|
console.log(`Saved config to zerodeploy.json`);
|
|
4698
5550
|
console.log("");
|
|
@@ -4703,6 +5555,50 @@ var deployCommand = new Command2("deploy").description("Deploy a site").argument
|
|
|
4703
5555
|
console.log(`Detected: ${framework.name}`);
|
|
4704
5556
|
}
|
|
4705
5557
|
const shouldBuild = options.build === true;
|
|
5558
|
+
if (shouldBuild || options.install) {
|
|
5559
|
+
const spinner = createSpinner("Checking deployment limits...");
|
|
5560
|
+
spinner.start();
|
|
5561
|
+
try {
|
|
5562
|
+
const client = getClient(token);
|
|
5563
|
+
const sitesRes = await client.orgs[":orgSlug"].sites.$get({
|
|
5564
|
+
param: { orgSlug }
|
|
5565
|
+
});
|
|
5566
|
+
if (!sitesRes.ok) {
|
|
5567
|
+
spinner.fail("Pre-flight check failed");
|
|
5568
|
+
const body = await sitesRes.json();
|
|
5569
|
+
displayError(parseApiError(body));
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
const { data: sites } = await sitesRes.json();
|
|
5573
|
+
const siteExists = sites.some((s) => s.slug === siteSlug);
|
|
5574
|
+
if (!siteExists) {
|
|
5575
|
+
spinner.fail("Pre-flight check failed");
|
|
5576
|
+
displayError({ code: "site_not_found", message: `Site "${siteSlug}" not found in organization "${orgSlug}"` });
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
const usageRes = await client.auth.me.usage.$get();
|
|
5580
|
+
if (usageRes.ok) {
|
|
5581
|
+
const usage = await usageRes.json();
|
|
5582
|
+
const org = usage.orgs.find((o) => o.slug === orgSlug);
|
|
5583
|
+
if (org) {
|
|
5584
|
+
const monthlyUsage = org.deployments_this_month;
|
|
5585
|
+
const monthlyLimit = usage.limits.max_deployments_per_month;
|
|
5586
|
+
const percentUsed = Math.round(monthlyUsage / monthlyLimit * 100);
|
|
5587
|
+
if (percentUsed >= 90) {
|
|
5588
|
+
spinner.warn(`Approaching monthly deployment limit (${monthlyUsage}/${monthlyLimit})`);
|
|
5589
|
+
} else {
|
|
5590
|
+
spinner.succeed("Pre-flight check passed");
|
|
5591
|
+
}
|
|
5592
|
+
} else {
|
|
5593
|
+
spinner.succeed("Pre-flight check passed");
|
|
5594
|
+
}
|
|
5595
|
+
} else {
|
|
5596
|
+
spinner.succeed("Pre-flight check passed");
|
|
5597
|
+
}
|
|
5598
|
+
} catch (err) {
|
|
5599
|
+
spinner.warn("Could not verify limits (will check during deploy)");
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
4706
5602
|
if (options.install) {
|
|
4707
5603
|
const installCmd = config.install || framework?.installCommand || "npm install";
|
|
4708
5604
|
console.log(`
|
|
@@ -4733,7 +5629,7 @@ Error: Build failed`);
|
|
|
4733
5629
|
}
|
|
4734
5630
|
let deployDir;
|
|
4735
5631
|
if (dirOption) {
|
|
4736
|
-
deployDir =
|
|
5632
|
+
deployDir = resolve4(cwd, dirOption);
|
|
4737
5633
|
try {
|
|
4738
5634
|
const stats = await stat2(deployDir);
|
|
4739
5635
|
if (!stats.isDirectory()) {
|
|
@@ -4745,7 +5641,7 @@ Error: Build failed`);
|
|
|
4745
5641
|
return;
|
|
4746
5642
|
}
|
|
4747
5643
|
} else {
|
|
4748
|
-
const detected = framework ?
|
|
5644
|
+
const detected = framework ? resolve4(cwd, framework.outputDir) : await findBuildDirectory(cwd);
|
|
4749
5645
|
if (!detected) {
|
|
4750
5646
|
console.log("Error: Could not find build directory. Use --dir to specify.");
|
|
4751
5647
|
return;
|
|
@@ -4770,7 +5666,7 @@ Error: Build failed`);
|
|
|
4770
5666
|
return;
|
|
4771
5667
|
}
|
|
4772
5668
|
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
4773
|
-
console.log(`Found ${files.length} files (${
|
|
5669
|
+
console.log(`Found ${files.length} files (${formatBytes2(totalSize)})`);
|
|
4774
5670
|
try {
|
|
4775
5671
|
const client = getClient(token);
|
|
4776
5672
|
const deployPayload = { siteSlug, orgSlug };
|
|
@@ -4794,68 +5690,104 @@ Error: Build failed`);
|
|
|
4794
5690
|
});
|
|
4795
5691
|
if (!createRes.ok) {
|
|
4796
5692
|
const body = await createRes.json();
|
|
4797
|
-
|
|
4798
|
-
if (typeof body.error === "string") {
|
|
4799
|
-
message = body.error;
|
|
4800
|
-
} else if (typeof body.message === "string") {
|
|
4801
|
-
message = body.message;
|
|
4802
|
-
} else if (body.success === false && body.error && typeof body.error === "object") {
|
|
4803
|
-
const zodError = body.error;
|
|
4804
|
-
if (zodError.issues?.[0]) {
|
|
4805
|
-
const issue = zodError.issues[0];
|
|
4806
|
-
const field = issue.path?.join(".") || "";
|
|
4807
|
-
message = field ? `${field}: ${issue.message}` : issue.message;
|
|
4808
|
-
} else if (zodError.message) {
|
|
4809
|
-
try {
|
|
4810
|
-
const issues = JSON.parse(zodError.message);
|
|
4811
|
-
if (issues[0]) {
|
|
4812
|
-
const field = issues[0].path?.join(".") || "";
|
|
4813
|
-
message = field ? `${field}: ${issues[0].message}` : issues[0].message;
|
|
4814
|
-
}
|
|
4815
|
-
} catch {
|
|
4816
|
-
message = zodError.message;
|
|
4817
|
-
}
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
console.log(`Error: ${message}`);
|
|
5693
|
+
displayError(parseApiError(body));
|
|
4821
5694
|
return;
|
|
4822
5695
|
}
|
|
4823
5696
|
const deployment = await createRes.json();
|
|
4824
5697
|
console.log(`Created deployment: ${deployment.id}`);
|
|
4825
|
-
|
|
4826
|
-
const archive = await createTarGz(files)
|
|
4827
|
-
|
|
4828
|
-
|
|
5698
|
+
const progressBar2 = createProgressBar({ total: files.length, label: "Archiving" });
|
|
5699
|
+
const archive = await createTarGz(files, (current, total) => {
|
|
5700
|
+
progressBar2.update(current);
|
|
5701
|
+
});
|
|
5702
|
+
progressBar2.done();
|
|
5703
|
+
console.log(` ${formatBytes2(totalSize)} -> ${formatBytes2(archive.length)} (${formatCompression(totalSize, archive.length)})`);
|
|
5704
|
+
const uploadSpinner = createSpinner(`Uploading (${formatBytes2(archive.length)})...`);
|
|
5705
|
+
uploadSpinner.start();
|
|
4829
5706
|
const uploadUrl = deployment.uploadUrl || `${API_URL}/deployments/${deployment.id}/upload`;
|
|
4830
|
-
const uploadSuccess = await uploadArchive(token, uploadUrl, archive)
|
|
5707
|
+
const uploadSuccess = await uploadArchive(token, uploadUrl, archive, (attempt, error, delayMs) => {
|
|
5708
|
+
uploadSpinner.update(`Uploading... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
|
|
5709
|
+
});
|
|
4831
5710
|
if (!uploadSuccess) {
|
|
4832
|
-
|
|
5711
|
+
uploadSpinner.stop("Error: Failed to upload archive");
|
|
4833
5712
|
return;
|
|
4834
5713
|
}
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
5714
|
+
uploadSpinner.stop(` Uploaded ${formatBytes2(archive.length)}`);
|
|
5715
|
+
const finalizeSpinner = createSpinner("Finalizing...");
|
|
5716
|
+
finalizeSpinner.start();
|
|
5717
|
+
const finalizeRes = await fetchWithRetry(`${API_URL}/deployments/${deployment.id}/finalize`, {
|
|
4838
5718
|
method: "POST",
|
|
4839
5719
|
headers: {
|
|
4840
5720
|
"Content-Type": "application/json",
|
|
4841
5721
|
Authorization: `Bearer ${token}`
|
|
4842
5722
|
},
|
|
4843
5723
|
body: JSON.stringify({ preview: options.preview || false })
|
|
5724
|
+
}, {
|
|
5725
|
+
maxRetries: 3,
|
|
5726
|
+
onRetry: (attempt, error, delayMs) => {
|
|
5727
|
+
finalizeSpinner.update(`Finalizing... ${formatRetryMessage(attempt, 3, error)}, retrying in ${Math.round(delayMs / 1000)}s`);
|
|
5728
|
+
}
|
|
4844
5729
|
});
|
|
4845
5730
|
if (!finalizeRes.ok) {
|
|
4846
|
-
|
|
5731
|
+
finalizeSpinner.stop("Error: Failed to finalize deployment");
|
|
4847
5732
|
return;
|
|
4848
5733
|
}
|
|
5734
|
+
finalizeSpinner.stop();
|
|
5735
|
+
const verifyUrl = options.preview ? deployment.previewUrl : deployment.url;
|
|
5736
|
+
let verified = false;
|
|
5737
|
+
if (options.verify !== false) {
|
|
5738
|
+
const verifySpinner = createSpinner("Verifying...");
|
|
5739
|
+
verifySpinner.start();
|
|
5740
|
+
const verification = await verifyDeployment(verifyUrl);
|
|
5741
|
+
verified = verification.success;
|
|
5742
|
+
if (!verified) {
|
|
5743
|
+
verifySpinner.stop();
|
|
5744
|
+
console.log("");
|
|
5745
|
+
console.log("Warning: Could not verify deployment");
|
|
5746
|
+
if (verification.status) {
|
|
5747
|
+
console.log(` Received status ${verification.status}`);
|
|
5748
|
+
} else if (verification.error) {
|
|
5749
|
+
console.log(` ${verification.error}`);
|
|
5750
|
+
}
|
|
5751
|
+
if (options.autoRollback !== false && !options.preview) {
|
|
5752
|
+
console.log("");
|
|
5753
|
+
console.log("Auto-rolling back to previous deployment...");
|
|
5754
|
+
const rollback = await autoRollback(token, orgSlug, siteSlug, deployment.id);
|
|
5755
|
+
if (rollback.success) {
|
|
5756
|
+
console.log(`Rolled back to ${rollback.deploymentId?.slice(0, 8)}`);
|
|
5757
|
+
console.log("");
|
|
5758
|
+
console.log("Deployment failed verification and was rolled back.");
|
|
5759
|
+
console.log(`Failed deployment: ${deployment.id.slice(0, 8)}`);
|
|
5760
|
+
console.log(`Check manually: ${verifyUrl}`);
|
|
5761
|
+
return;
|
|
5762
|
+
} else {
|
|
5763
|
+
console.log(` Could not auto-rollback: ${rollback.error}`);
|
|
5764
|
+
console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
|
|
5765
|
+
}
|
|
5766
|
+
} else {
|
|
5767
|
+
console.log(` The site may still be propagating. Check manually: ${verifyUrl}`);
|
|
5768
|
+
}
|
|
5769
|
+
} else {
|
|
5770
|
+
verifySpinner.stop();
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
4849
5773
|
console.log("");
|
|
4850
5774
|
if (options.preview) {
|
|
4851
5775
|
console.log("Preview deployment created!");
|
|
4852
|
-
|
|
5776
|
+
if (verified) {
|
|
5777
|
+
console.log(`Preview: ${deployment.previewUrl} (verified)`);
|
|
5778
|
+
} else {
|
|
5779
|
+
console.log(`Preview: ${deployment.previewUrl}`);
|
|
5780
|
+
}
|
|
4853
5781
|
console.log("");
|
|
4854
5782
|
console.log(`To make this deployment live, run:`);
|
|
4855
5783
|
console.log(` zerodeploy deploy promote ${deployment.id.slice(0, 8)}`);
|
|
4856
5784
|
} else {
|
|
4857
5785
|
console.log("Deployment successful!");
|
|
4858
|
-
|
|
5786
|
+
if (verified) {
|
|
5787
|
+
console.log(`URL: ${deployment.url} (verified)`);
|
|
5788
|
+
} else {
|
|
5789
|
+
console.log(`URL: ${deployment.url}`);
|
|
5790
|
+
}
|
|
4859
5791
|
console.log(`Preview: ${deployment.previewUrl}`);
|
|
4860
5792
|
}
|
|
4861
5793
|
if (options.githubOutput) {
|
|
@@ -4876,8 +5808,14 @@ Error: Build failed`);
|
|
|
4876
5808
|
}
|
|
4877
5809
|
}
|
|
4878
5810
|
} catch (err) {
|
|
4879
|
-
|
|
4880
|
-
|
|
5811
|
+
if (err instanceof Error) {
|
|
5812
|
+
displayNetworkError(err);
|
|
5813
|
+
} else {
|
|
5814
|
+
displayError({
|
|
5815
|
+
code: "unknown_error",
|
|
5816
|
+
message: "Deploy failed: Unknown error"
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
4881
5819
|
}
|
|
4882
5820
|
});
|
|
4883
5821
|
|
|
@@ -4895,7 +5833,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
|
|
|
4895
5833
|
});
|
|
4896
5834
|
if (!res.ok)
|
|
4897
5835
|
throw new Error(`API Error ${res.status}`);
|
|
4898
|
-
const deployments = await res.json();
|
|
5836
|
+
const { data: deployments } = await res.json();
|
|
4899
5837
|
if (deployments.length === 0) {
|
|
4900
5838
|
console.log("No deployments found.");
|
|
4901
5839
|
return;
|
|
@@ -4903,7 +5841,7 @@ var deploymentsListCommand = new Command2("list").description("List deployments
|
|
|
4903
5841
|
console.log("Deployments:");
|
|
4904
5842
|
console.log();
|
|
4905
5843
|
for (const d of deployments) {
|
|
4906
|
-
const current = d.
|
|
5844
|
+
const current = d.is_current ? " (current)" : "";
|
|
4907
5845
|
const date = new Date(d.created_at).toLocaleString();
|
|
4908
5846
|
const shortId = d.id.slice(0, 8);
|
|
4909
5847
|
const commit = d.commit_sha ? ` [${d.commit_sha.slice(0, 7)}]` : "";
|
|
@@ -4911,8 +5849,9 @@ var deploymentsListCommand = new Command2("list").description("List deployments
|
|
|
4911
5849
|
const message = d.commit_message ? ` - ${(d.commit_message.split(`
|
|
4912
5850
|
`)[0] ?? "").slice(0, 50)}` : "";
|
|
4913
5851
|
console.log(` ${shortId} ${d.status.padEnd(10)} ${date}${current}`);
|
|
5852
|
+
console.log(` ${d.preview_url}`);
|
|
4914
5853
|
if (commit || message) {
|
|
4915
|
-
console.log(`
|
|
5854
|
+
console.log(` ${commit}${branch}${message}`);
|
|
4916
5855
|
}
|
|
4917
5856
|
console.log();
|
|
4918
5857
|
}
|
|
@@ -4922,8 +5861,109 @@ var deploymentsListCommand = new Command2("list").description("List deployments
|
|
|
4922
5861
|
}
|
|
4923
5862
|
});
|
|
4924
5863
|
|
|
5864
|
+
// src/commands/deployments/show.ts
|
|
5865
|
+
function formatBytes3(bytes) {
|
|
5866
|
+
if (bytes === 0)
|
|
5867
|
+
return "0 B";
|
|
5868
|
+
const k = 1024;
|
|
5869
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
5870
|
+
const i2 = Math.floor(Math.log(bytes) / Math.log(k));
|
|
5871
|
+
return parseFloat((bytes / Math.pow(k, i2)).toFixed(1)) + " " + sizes[i2];
|
|
5872
|
+
}
|
|
5873
|
+
function formatStatus3(status) {
|
|
5874
|
+
switch (status) {
|
|
5875
|
+
case "pending":
|
|
5876
|
+
return "⏳ Pending";
|
|
5877
|
+
case "uploading":
|
|
5878
|
+
return "\uD83D\uDCE4 Uploading";
|
|
5879
|
+
case "processing":
|
|
5880
|
+
return "⚙️ Processing";
|
|
5881
|
+
case "ready":
|
|
5882
|
+
return "✅ Ready";
|
|
5883
|
+
case "failed":
|
|
5884
|
+
return "❌ Failed";
|
|
5885
|
+
default:
|
|
5886
|
+
return status;
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
var deploymentsShowCommand = new Command2("show").description("View deployment details").argument("<id>", "Deployment ID (full or short)").action(async (id) => {
|
|
5890
|
+
const token = loadToken();
|
|
5891
|
+
if (!token) {
|
|
5892
|
+
console.log("Not logged in. Run: zerodeploy login");
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
try {
|
|
5896
|
+
const client = getClient(token);
|
|
5897
|
+
const res = await client.deployments[":id"].$get({
|
|
5898
|
+
param: { id }
|
|
5899
|
+
});
|
|
5900
|
+
if (!res.ok) {
|
|
5901
|
+
if (res.status === 404) {
|
|
5902
|
+
console.error(`Deployment not found: ${id}`);
|
|
5903
|
+
console.log();
|
|
5904
|
+
console.log("Use the full deployment ID or at least 8 characters.");
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
const error = await res.json();
|
|
5908
|
+
throw new Error(error.error || `API Error ${res.status}`);
|
|
5909
|
+
}
|
|
5910
|
+
const d = await res.json();
|
|
5911
|
+
console.log("Deployment Details");
|
|
5912
|
+
console.log("=".repeat(50));
|
|
5913
|
+
console.log();
|
|
5914
|
+
console.log(`ID: ${d.id}`);
|
|
5915
|
+
console.log(`Status: ${formatStatus3(d.status)}`);
|
|
5916
|
+
console.log(`Created: ${new Date(d.created_at).toLocaleString()}`);
|
|
5917
|
+
console.log();
|
|
5918
|
+
console.log("URLs");
|
|
5919
|
+
console.log("-".repeat(50));
|
|
5920
|
+
console.log(`Production: ${d.url}`);
|
|
5921
|
+
console.log(`Preview: ${d.preview_url}`);
|
|
5922
|
+
console.log();
|
|
5923
|
+
if (d.commit_sha || d.branch || d.pr_number) {
|
|
5924
|
+
console.log("Git Info");
|
|
5925
|
+
console.log("-".repeat(50));
|
|
5926
|
+
if (d.branch) {
|
|
5927
|
+
console.log(`Branch: ${d.branch}`);
|
|
5928
|
+
}
|
|
5929
|
+
if (d.commit_sha) {
|
|
5930
|
+
console.log(`Commit: ${d.commit_sha}`);
|
|
5931
|
+
}
|
|
5932
|
+
if (d.commit_message) {
|
|
5933
|
+
const firstLine = d.commit_message.split(`
|
|
5934
|
+
`)[0] ?? "";
|
|
5935
|
+
console.log(`Message: ${firstLine.slice(0, 60)}${firstLine.length > 60 ? "..." : ""}`);
|
|
5936
|
+
}
|
|
5937
|
+
if (d.pr_number) {
|
|
5938
|
+
console.log(`PR: #${d.pr_number}${d.pr_title ? ` - ${d.pr_title}` : ""}`);
|
|
5939
|
+
}
|
|
5940
|
+
console.log();
|
|
5941
|
+
}
|
|
5942
|
+
console.log("Files");
|
|
5943
|
+
console.log("-".repeat(50));
|
|
5944
|
+
console.log(`Count: ${d.file_count.toLocaleString()} files`);
|
|
5945
|
+
console.log(`Size: ${formatBytes3(d.total_size_bytes)}`);
|
|
5946
|
+
console.log();
|
|
5947
|
+
if (d.status === "failed" && d.error_message) {
|
|
5948
|
+
console.log("Error");
|
|
5949
|
+
console.log("-".repeat(50));
|
|
5950
|
+
console.log(d.error_message);
|
|
5951
|
+
console.log();
|
|
5952
|
+
}
|
|
5953
|
+
if (d.status === "ready") {
|
|
5954
|
+
console.log("Actions");
|
|
5955
|
+
console.log("-".repeat(50));
|
|
5956
|
+
console.log(`Rollback to this deployment:`);
|
|
5957
|
+
console.log(` zerodeploy rollback <site> --org <org> --to ${d.id.slice(0, 8)}`);
|
|
5958
|
+
}
|
|
5959
|
+
} catch (err) {
|
|
5960
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
5961
|
+
console.error("Failed to get deployment:", message);
|
|
5962
|
+
}
|
|
5963
|
+
});
|
|
5964
|
+
|
|
4925
5965
|
// src/commands/deployments/index.ts
|
|
4926
|
-
var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand);
|
|
5966
|
+
var deploymentsCommand = new Command2("deployments").description("Manage deployments").addCommand(deploymentsListCommand).addCommand(deploymentsShowCommand);
|
|
4927
5967
|
|
|
4928
5968
|
// src/commands/rollback.ts
|
|
4929
5969
|
var rollbackCommand = new Command2("rollback").description("Rollback a site to a previous deployment").argument("<site>", "Site slug").requiredOption("--org <org>", "Organization slug").option("--to <deploymentId>", "Deployment ID to rollback to (defaults to previous deployment)").action(async (site, options) => {
|
|
@@ -4941,13 +5981,13 @@ var rollbackCommand = new Command2("rollback").description("Rollback a site to a
|
|
|
4941
5981
|
});
|
|
4942
5982
|
if (!listRes.ok)
|
|
4943
5983
|
throw new Error(`API Error ${listRes.status}`);
|
|
4944
|
-
const deployments = await listRes.json();
|
|
5984
|
+
const { data: deployments } = await listRes.json();
|
|
4945
5985
|
const readyDeployments = deployments.filter((d) => d.status === "ready");
|
|
4946
5986
|
if (readyDeployments.length < 2) {
|
|
4947
5987
|
console.log("No previous deployment to rollback to.");
|
|
4948
5988
|
return;
|
|
4949
5989
|
}
|
|
4950
|
-
const currentIndex = readyDeployments.findIndex((d) => d.
|
|
5990
|
+
const currentIndex = readyDeployments.findIndex((d) => d.is_current);
|
|
4951
5991
|
if (currentIndex === -1 || currentIndex >= readyDeployments.length - 1) {
|
|
4952
5992
|
deploymentId = readyDeployments[1].id;
|
|
4953
5993
|
} else {
|
|
@@ -4980,13 +6020,10 @@ var tokenCreateCommand = new Command2("create").description("Create a deploy tok
|
|
|
4980
6020
|
return;
|
|
4981
6021
|
}
|
|
4982
6022
|
try {
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
Authorization: `Bearer ${token}`
|
|
4988
|
-
},
|
|
4989
|
-
body: JSON.stringify({ name })
|
|
6023
|
+
const client = getClient(token);
|
|
6024
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$post({
|
|
6025
|
+
param: { orgSlug: options.org, siteSlug: options.site },
|
|
6026
|
+
json: { name }
|
|
4990
6027
|
});
|
|
4991
6028
|
if (!res.ok) {
|
|
4992
6029
|
const error = await res.json();
|
|
@@ -5021,17 +6058,16 @@ var tokenListCommand = new Command2("list").description("List deploy tokens for
|
|
|
5021
6058
|
return;
|
|
5022
6059
|
}
|
|
5023
6060
|
try {
|
|
5024
|
-
const
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
}
|
|
6061
|
+
const client = getClient(token);
|
|
6062
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
|
|
6063
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
5028
6064
|
});
|
|
5029
6065
|
if (!res.ok) {
|
|
5030
6066
|
const error = await res.json();
|
|
5031
6067
|
console.log(`Error: ${error.error || "Failed to list tokens"}`);
|
|
5032
6068
|
return;
|
|
5033
6069
|
}
|
|
5034
|
-
const tokens = await res.json();
|
|
6070
|
+
const { data: tokens } = await res.json();
|
|
5035
6071
|
if (tokens.length === 0) {
|
|
5036
6072
|
console.log(`No deploy tokens for ${options.org}/${options.site}`);
|
|
5037
6073
|
return;
|
|
@@ -5058,19 +6094,18 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
|
|
|
5058
6094
|
return;
|
|
5059
6095
|
}
|
|
5060
6096
|
try {
|
|
6097
|
+
const client = getClient(token);
|
|
5061
6098
|
let fullTokenId = tokenId;
|
|
5062
6099
|
if (tokenId.length < 36) {
|
|
5063
|
-
const listRes = await
|
|
5064
|
-
|
|
5065
|
-
Authorization: `Bearer ${token}`
|
|
5066
|
-
}
|
|
6100
|
+
const listRes = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens.$get({
|
|
6101
|
+
param: { orgSlug: options.org, siteSlug: options.site }
|
|
5067
6102
|
});
|
|
5068
6103
|
if (!listRes.ok) {
|
|
5069
6104
|
const error = await listRes.json();
|
|
5070
6105
|
console.log(`Error: ${error.error || "Failed to find token"}`);
|
|
5071
6106
|
return;
|
|
5072
6107
|
}
|
|
5073
|
-
const tokens = await listRes.json();
|
|
6108
|
+
const { data: tokens } = await listRes.json();
|
|
5074
6109
|
const match = tokens.find((t) => t.id.startsWith(tokenId));
|
|
5075
6110
|
if (!match) {
|
|
5076
6111
|
console.log(`Error: No token found starting with "${tokenId}"`);
|
|
@@ -5078,11 +6113,8 @@ var tokenDeleteCommand = new Command2("delete").description("Delete a deploy tok
|
|
|
5078
6113
|
}
|
|
5079
6114
|
fullTokenId = match.id;
|
|
5080
6115
|
}
|
|
5081
|
-
const res = await
|
|
5082
|
-
|
|
5083
|
-
headers: {
|
|
5084
|
-
Authorization: `Bearer ${token}`
|
|
5085
|
-
}
|
|
6116
|
+
const res = await client.orgs[":orgSlug"].sites[":siteSlug"].tokens[":tokenId"].$delete({
|
|
6117
|
+
param: { orgSlug: options.org, siteSlug: options.site, tokenId: fullTokenId }
|
|
5086
6118
|
});
|
|
5087
6119
|
if (!res.ok) {
|
|
5088
6120
|
const error = await res.json();
|
|
@@ -5150,18 +6182,121 @@ var initCommand = new Command2("init").description("Create a zerodeploy.json con
|
|
|
5150
6182
|
console.log(" zerodeploy deploy --build # build + deploy");
|
|
5151
6183
|
});
|
|
5152
6184
|
|
|
6185
|
+
// src/commands/account-delete.ts
|
|
6186
|
+
import * as readline5 from "readline";
|
|
6187
|
+
function prompt4(question) {
|
|
6188
|
+
const rl = readline5.createInterface({
|
|
6189
|
+
input: process.stdin,
|
|
6190
|
+
output: process.stdout
|
|
6191
|
+
});
|
|
6192
|
+
return new Promise((resolve5) => {
|
|
6193
|
+
rl.question(question, (answer) => {
|
|
6194
|
+
rl.close();
|
|
6195
|
+
resolve5(answer);
|
|
6196
|
+
});
|
|
6197
|
+
});
|
|
6198
|
+
}
|
|
6199
|
+
var accountCommand = new Command2("account").description("Manage your ZeroDeploy account").addCommand(new Command2("email").description("Set your email address for notifications").argument("<email>", "Email address").action(async (email) => {
|
|
6200
|
+
const token = loadToken();
|
|
6201
|
+
if (!token) {
|
|
6202
|
+
displayAuthError();
|
|
6203
|
+
return;
|
|
6204
|
+
}
|
|
6205
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6206
|
+
if (!emailRegex.test(email)) {
|
|
6207
|
+
displayError({ code: "validation_error", message: "Invalid email address format" });
|
|
6208
|
+
return;
|
|
6209
|
+
}
|
|
6210
|
+
try {
|
|
6211
|
+
const client = getClient(token);
|
|
6212
|
+
const res = await client.auth.me.$patch({
|
|
6213
|
+
json: { email }
|
|
6214
|
+
});
|
|
6215
|
+
if (!res.ok) {
|
|
6216
|
+
const body = await res.json();
|
|
6217
|
+
displayError(parseApiError(body));
|
|
6218
|
+
return;
|
|
6219
|
+
}
|
|
6220
|
+
console.log(`
|
|
6221
|
+
✅ Email updated to: ${email}`);
|
|
6222
|
+
console.log(` You will now receive deployment notifications at this address.
|
|
6223
|
+
`);
|
|
6224
|
+
} catch (err) {
|
|
6225
|
+
if (err instanceof Error) {
|
|
6226
|
+
displayNetworkError(err);
|
|
6227
|
+
} else {
|
|
6228
|
+
displayError({ code: "unknown_error", message: "Failed to update email" });
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
})).addCommand(new Command2("delete").description("Permanently delete your account and all data").option("--force", "Skip confirmation prompts").action(async (options) => {
|
|
6232
|
+
const token = loadToken();
|
|
6233
|
+
if (!token) {
|
|
6234
|
+
displayAuthError();
|
|
6235
|
+
return;
|
|
6236
|
+
}
|
|
6237
|
+
if (!options.force) {
|
|
6238
|
+
console.log(`
|
|
6239
|
+
⚠️ WARNING: This will permanently delete:`);
|
|
6240
|
+
console.log(" - Your account");
|
|
6241
|
+
console.log(" - All organizations you own");
|
|
6242
|
+
console.log(" - All sites and deployments");
|
|
6243
|
+
console.log(" - All custom domains");
|
|
6244
|
+
console.log(" - All deploy tokens");
|
|
6245
|
+
console.log(`
|
|
6246
|
+
This action CANNOT be undone.
|
|
6247
|
+
`);
|
|
6248
|
+
const answer1 = await prompt4('Type "delete my account" to confirm: ');
|
|
6249
|
+
if (answer1 !== "delete my account") {
|
|
6250
|
+
console.log("Cancelled.");
|
|
6251
|
+
return;
|
|
6252
|
+
}
|
|
6253
|
+
const answer2 = await prompt4("Are you absolutely sure? (y/N) ");
|
|
6254
|
+
if (answer2.toLowerCase() !== "y") {
|
|
6255
|
+
console.log("Cancelled.");
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
try {
|
|
6260
|
+
const client = getClient(token);
|
|
6261
|
+
const res = await client.auth.me.$delete();
|
|
6262
|
+
if (!res.ok) {
|
|
6263
|
+
const body = await res.json();
|
|
6264
|
+
displayError(parseApiError(body));
|
|
6265
|
+
return;
|
|
6266
|
+
}
|
|
6267
|
+
const result = await res.json();
|
|
6268
|
+
deleteToken();
|
|
6269
|
+
console.log(`
|
|
6270
|
+
✅ Account deleted successfully.`);
|
|
6271
|
+
console.log(` Deleted: ${result.deleted.orgs} org(s), ${result.deleted.sites} site(s), ${result.deleted.deployments} deployment(s)`);
|
|
6272
|
+
console.log(`
|
|
6273
|
+
Your local authentication token has been removed.`);
|
|
6274
|
+
console.log(` Thank you for using ZeroDeploy.
|
|
6275
|
+
`);
|
|
6276
|
+
} catch (err) {
|
|
6277
|
+
if (err instanceof Error) {
|
|
6278
|
+
displayNetworkError(err);
|
|
6279
|
+
} else {
|
|
6280
|
+
displayError({ code: "unknown_error", message: "Failed to delete account" });
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
}));
|
|
6284
|
+
|
|
5153
6285
|
// src/index.ts
|
|
5154
6286
|
var program3 = new Command;
|
|
5155
6287
|
program3.name("zerodeploy").description("ZeroDeploy CLI").version("0.1.0").enablePositionalOptions();
|
|
5156
6288
|
program3.addCommand(loginCommand);
|
|
5157
6289
|
program3.addCommand(logoutCommand);
|
|
5158
6290
|
program3.addCommand(whoamiCommand);
|
|
6291
|
+
program3.addCommand(usageCommand);
|
|
5159
6292
|
program3.addCommand(orgCommand);
|
|
5160
6293
|
program3.addCommand(siteCommand);
|
|
5161
6294
|
program3.addCommand(domainCommand);
|
|
6295
|
+
program3.addCommand(formCommand);
|
|
5162
6296
|
program3.addCommand(deployCommand);
|
|
5163
6297
|
program3.addCommand(deploymentsCommand);
|
|
5164
6298
|
program3.addCommand(rollbackCommand);
|
|
5165
6299
|
program3.addCommand(tokenCommand);
|
|
5166
6300
|
program3.addCommand(initCommand);
|
|
6301
|
+
program3.addCommand(accountCommand);
|
|
5167
6302
|
program3.parse(process.argv);
|