Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 64 additions & 39 deletions packages/void/src/cli/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
import { getApiUrl } from "./auth";
import type { DeployEvent } from "./types";

type PlatformErrorPayload = {
error?: string;
message?: string;
};

function parsePlatformErrorBody(body: string): PlatformErrorPayload | null {
try {
const parsed = JSON.parse(body);
if (!parsed || typeof parsed !== "object") return null;
return parsed as PlatformErrorPayload;
} catch {
return null;
}
}

export class PlatformApiError extends Error {
status: number;
body: string;
code?: string;

constructor(action: string, status: number, body: string) {
super(`${action}: ${status} ${body}`);
this.name = "PlatformApiError";
this.status = status;
this.body = body;
const parsed = parsePlatformErrorBody(body);
this.code = parsed?.error ?? parsed?.message;
}
}

async function throwPlatformApiError(action: string, res: Response): Promise<never> {
throw new PlatformApiError(action, res.status, await res.text());
}

export function isExpiredTokenError(error: unknown): boolean {
if (error instanceof PlatformApiError) {
return error.status === 401 && error.code === "invalid or expired token";
}
return false;
}

export interface ProjectResponse {
id: string;
user_id: string;
Expand Down Expand Up @@ -90,8 +131,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to fetch user info: ${res.status} ${body}`);
await throwPlatformApiError("Failed to fetch user info", res);
}

return res.json();
Expand All @@ -105,8 +145,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to list projects: ${res.status} ${body}`);
await throwPlatformApiError("Failed to list projects", res);
}

return res.json();
Expand All @@ -120,8 +159,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to list deployments: ${res.status} ${body}`);
await throwPlatformApiError("Failed to list deployments", res);
}

return res.json();
Expand All @@ -141,8 +179,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Rollback failed: ${res.status} ${body}`);
await throwPlatformApiError("Rollback failed", res);
}

return res.json();
Expand All @@ -159,8 +196,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to create project: ${res.status} ${body}`);
await throwPlatformApiError("Failed to create project", res);
}

return res.json();
Expand All @@ -182,12 +218,15 @@ export class PlatformClient {
});

if (!res.ok) {
const error = new PlatformApiError("Preflight failed", res.status, await res.text());
if (isExpiredTokenError(error)) throw error;
// Preflight not supported or error — fall back to uploading everything
return { needed: Object.keys(assetManifest), total, skipped: 0 };
}

return res.json();
} catch {
} catch (error) {
if (isExpiredTokenError(error)) throw error;
// Network error — fall back to uploading everything
return { needed: Object.keys(assetManifest), total, skipped: 0 };
}
Expand All @@ -203,8 +242,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Deploy failed: ${res.status} ${body}`);
await throwPlatformApiError("Deploy failed", res);
}

const reader = res.body!.getReader();
Expand Down Expand Up @@ -235,8 +273,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to list secrets: ${res.status} ${body}`);
await throwPlatformApiError("Failed to list secrets", res);
}

return res.json();
Expand All @@ -256,8 +293,7 @@ export class PlatformClient {
);

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to set secret: ${res.status} ${body}`);
await throwPlatformApiError("Failed to set secret", res);
}

return res.json();
Expand All @@ -272,8 +308,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to delete project: ${res.status} ${body}`);
await throwPlatformApiError("Failed to delete project", res);
}

return res.json();
Expand All @@ -289,8 +324,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to get migration status: ${res.status} ${body}`);
await throwPlatformApiError("Failed to get migration status", res);
}

return res.json();
Expand All @@ -307,8 +341,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to add domain: ${res.status} ${body}`);
await throwPlatformApiError("Failed to add domain", res);
}

return res.json();
Expand All @@ -322,8 +355,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to list domains: ${res.status} ${body}`);
await throwPlatformApiError("Failed to list domains", res);
}

return res.json();
Expand All @@ -337,8 +369,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to get domain status: ${res.status} ${body}`);
await throwPlatformApiError("Failed to get domain status", res);
}

return res.json();
Expand All @@ -353,8 +384,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to remove domain: ${res.status} ${body}`);
await throwPlatformApiError("Failed to remove domain", res);
}

return res.json();
Expand All @@ -372,8 +402,7 @@ export class PlatformClient {
);

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to delete secret: ${res.status} ${body}`);
await throwPlatformApiError("Failed to delete secret", res);
}

return res.json();
Expand All @@ -387,8 +416,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to get resources: ${res.status} ${body}`);
await throwPlatformApiError("Failed to get resources", res);
}

return res.json();
Expand All @@ -405,8 +433,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to provision database: ${res.status} ${body}`);
await throwPlatformApiError("Failed to provision database", res);
}

return res.json();
Expand All @@ -423,8 +450,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to update database URL: ${res.status} ${body}`);
await throwPlatformApiError("Failed to update database URL", res);
}

return res.json();
Expand All @@ -449,8 +475,7 @@ export class PlatformClient {
});

if (!res.ok) {
const body = await res.text();
throw new Error(`Failed to fetch logs: ${res.status} ${body}`);
await throwPlatformApiError("Failed to fetch logs", res);
}

const json = (await res.json()) as { data: LogEntry[] };
Expand Down
39 changes: 6 additions & 33 deletions packages/void/src/cli/deploy-setup.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,20 @@
import { cancel, confirm, isCancel, log, select, spinner } from "@clack/prompts";
import { cancel, confirm, isCancel, log } from "@clack/prompts";
import { PlatformClient } from "./client";
import { getToken, isStagingMode, runLogin, saveToken } from "./auth";
import { getToken, isStagingMode } from "./auth";
import { promptForLoginToken } from "./login";
import { promptProjectSelection, promptProjectSlug } from "./prompt";
import { inferDefaultSlug } from "./resolve-project";
import { readProjectConfig, writeProjectConfig, type ProjectConfig } from "./project";

async function promptForLoginToken(): Promise<string> {
const provider = await select({
message: "Choose a login provider:",
options: [
{ value: "github", label: "GitHub" },
{ value: "google", label: "Google" },
],
});

if (isCancel(provider)) {
cancel("Setup cancelled.");
process.exit(0);
}

const providerName = provider === "github" ? "GitHub" : "Google";
const s = spinner();
s.start(`Opening browser for ${providerName} login...`);

try {
const token = await runLogin(provider);
saveToken(token);
s.stop("Logged in!");
log.success(isStagingMode() ? "Staging token saved." : "Token saved.");
return token;
} catch (error) {
s.stop("Login failed");
throw error;
}
}

async function ensureToken(): Promise<string> {
const existing = getToken();
if (existing) {
return existing;
}

return promptForLoginToken();
const token = await promptForLoginToken();
log.success(isStagingMode() ? "Staging token saved." : "Token saved.");
return token;
}

async function createProject(
Expand Down
Loading
Loading