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