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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperplay/cli",
"version": "2.14.6",
"version": "2.14.7",
"description": "Hyperplay CLI",
"author": "HyperPlay Labs, Inc.",
"bin": {
Expand Down
138 changes: 129 additions & 9 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosInstance } from 'axios';
import { AxiosInstance, AxiosError } from 'axios';
import { ethers } from 'ethers';
import { SiweMessage } from 'siwe';
import { Cookie, CookieJar } from 'tough-cookie';
Expand Down Expand Up @@ -61,15 +61,135 @@ export async function login(client: AxiosInstance, cookieJar: CookieJar, signer:
CliUx.ux.action.stop();
}

interface ApiErrorResponse {
message?: string;
}

function extractApiError(axiosError: AxiosError): { status: number; statusText: string; apiError: string } {
const status = axiosError.response?.status || 0;
const statusText = axiosError.response?.statusText || '';
const responseData = axiosError.response?.data as unknown as ApiErrorResponse;
const apiError = responseData?.message || statusText || 'Unknown error';

return { status, statusText, apiError };
}

function createApiError(userMessage: string, apiError: string): Error {
return new Error(`${userMessage}\nAPI Error: ${apiError}`);
}

function handleNetworkError(context: string): Error {
return new Error(`Network error ${context}.\nPlease check your internet connection and try again.`);
}

function handleAxiosError(error: unknown, context: string, statusCodeHandlers: Record<number, (apiError: string) => Error>): never {
const axiosError = error as AxiosError;

if (axiosError.response) {
const { status, apiError } = extractApiError(axiosError);
const handler = statusCodeHandlers[status];

if (handler) {
throw handler(apiError);
} else {
const { statusText } = extractApiError(axiosError);
throw createApiError(
`${context} failed (HTTP ${status} ${statusText}).\nPlease contact support if this issue persists.`,
apiError
);
}
} else if (axiosError.request) {
throw handleNetworkError(context);
} else {
throw new Error(`${context}: ${axiosError.message}`);
}
}

export async function publish(client: AxiosInstance, projectID: string, path: string, targetChannel: string) {
CliUx.ux.log('Fetching listing release branches');
const channels = (await client.get<{ channel_id: number, channel_name: string }[]>(`/api/v1/channels?project_id=${projectID}`)).data;
CliUx.ux.log(`Publishing to project ID: ${projectID}`);
CliUx.ux.log(`Target channel: ${targetChannel}`);
CliUx.ux.log(`Release path: ${path}`);

// Fetch available channels for the project
CliUx.ux.log('Fetching available release channels...');
let channels: { channel_id: number, channel_name: string }[];

try {
const response = await client.get<{ channel_id: number, channel_name: string }[]>(`/api/v1/channels?project_id=${projectID}`);
channels = response.data;

if (!Array.isArray(channels)) {
throw new Error('Invalid response format: expected array of channels');
}

CliUx.ux.log(`Found ${channels.length} available channels: ${channels.map(c => c.channel_name).join(', ')}`);

} catch (error) {
handleAxiosError(error, 'Error fetching channels', {
404: (apiError) => createApiError(
`Project not found. Please verify your project ID: ${projectID}\nMake sure the project exists and you have access to it.`,
apiError
),
403: (apiError) => createApiError(
`Access denied to project ${projectID}.\nPlease check that your account has permission to access this project.`,
apiError
),
401: (apiError) => createApiError(
'Authentication failed. Please verify your private key and try again.',
apiError
)
});
}

// Find the target channel
const releaseChannel = channels.find((channel) => targetChannel === channel.channel_name);
if (!releaseChannel) CliUx.ux.warn(`Provided release channel_name ${targetChannel} not found on project!`);

CliUx.ux.log('Submitting release for review');
await client.post("/api/v1/reviews/release", {
path,
channel_id: releaseChannel?.channel_id,
});
if (!releaseChannel) {
const availableChannels = channels.map(c => c.channel_name).join(', ');
throw new Error(`Release channel "${targetChannel}" not found on this project.\n` +
`Available channels: ${availableChannels}\n` +
`Please use --channel flag with one of the available channel names.`);
}

CliUx.ux.log(`Using channel: ${releaseChannel.channel_name} (ID: ${releaseChannel.channel_id})`);

// Submit release for review
CliUx.ux.log('Submitting release for review...');

try {
await client.post("/api/v1/reviews/release", {
path,
channel_id: releaseChannel.channel_id,
});

CliUx.ux.log(`✅ Successfully submitted release for review on channel "${targetChannel}"`);

} catch (error) {
handleAxiosError(error, 'Release submission', {
400: (apiError) => createApiError(
'Invalid request data.\nPlease check your release configuration.',
apiError
),
404: (apiError) => createApiError(
'Release submission endpoint not found.\nThis may indicate an API version mismatch or the endpoint has changed.',
apiError
),
403: (apiError) => createApiError(
'Access denied for release submission.\nPlease verify you have permission to publish releases on this project.',
apiError
),
401: (apiError) => createApiError(
'Authentication expired. Please re-authenticate and try again.',
apiError
),
409: (apiError) => createApiError(
'Release conflict: A release with this configuration may already exist.',
apiError
),
422: (apiError) => createApiError(
'Release validation failed.\nPlease check your release files and metadata.',
apiError
)
});
}
}