mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-09 21:50:36 +00:00
* Fix: error building my application #1414 * fix for vite * Update vite.config.ts * Update root.tsx * fix the root.tsx and the debugtab * lm studio fix and fix for the api key * Update api.enhancer for prompt enhancement * bugfixes * Revert api.enhancer.ts back to original code * Update api.enhancer.ts * Update api.git-proxy.$.ts * Update api.git-proxy.$.ts * Update api.enhancer.ts
238 lines
7.3 KiB
TypeScript
238 lines
7.3 KiB
TypeScript
import { type ActionFunctionArgs, json } from '@remix-run/cloudflare';
|
|
import type { NetlifySiteInfo } from '~/types/netlify';
|
|
|
|
interface DeployRequestBody {
|
|
siteId?: string;
|
|
files: Record<string, string>;
|
|
chatId: string;
|
|
}
|
|
|
|
async function sha1(message: string) {
|
|
const msgBuffer = new TextEncoder().encode(message);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
return hashHex;
|
|
}
|
|
|
|
export async function action({ request }: ActionFunctionArgs) {
|
|
try {
|
|
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
|
|
|
if (!token) {
|
|
return json({ error: 'Not connected to Netlify' }, { status: 401 });
|
|
}
|
|
|
|
let targetSiteId = siteId;
|
|
let siteInfo: NetlifySiteInfo | undefined;
|
|
|
|
// If no siteId provided, create a new site
|
|
if (!targetSiteId) {
|
|
const siteName = `bolt-diy-${chatId}-${Date.now()}`;
|
|
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: siteName,
|
|
custom_domain: null,
|
|
}),
|
|
});
|
|
|
|
if (!createSiteResponse.ok) {
|
|
return json({ error: 'Failed to create site' }, { status: 400 });
|
|
}
|
|
|
|
const newSite = (await createSiteResponse.json()) as any;
|
|
targetSiteId = newSite.id;
|
|
siteInfo = {
|
|
id: newSite.id,
|
|
name: newSite.name,
|
|
url: newSite.url,
|
|
chatId,
|
|
};
|
|
} else {
|
|
// Get existing site info
|
|
if (targetSiteId) {
|
|
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (siteResponse.ok) {
|
|
const existingSite = (await siteResponse.json()) as any;
|
|
siteInfo = {
|
|
id: existingSite.id,
|
|
name: existingSite.name,
|
|
url: existingSite.url,
|
|
chatId,
|
|
};
|
|
} else {
|
|
targetSiteId = undefined;
|
|
}
|
|
}
|
|
|
|
// If no siteId provided or site doesn't exist, create a new site
|
|
if (!targetSiteId) {
|
|
const siteName = `bolt-diy-${chatId}-${Date.now()}`;
|
|
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: siteName,
|
|
custom_domain: null,
|
|
}),
|
|
});
|
|
|
|
if (!createSiteResponse.ok) {
|
|
return json({ error: 'Failed to create site' }, { status: 400 });
|
|
}
|
|
|
|
const newSite = (await createSiteResponse.json()) as any;
|
|
targetSiteId = newSite.id;
|
|
siteInfo = {
|
|
id: newSite.id,
|
|
name: newSite.name,
|
|
url: newSite.url,
|
|
chatId,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create file digests
|
|
const fileDigests: Record<string, string> = {};
|
|
|
|
for (const [filePath, content] of Object.entries(files)) {
|
|
// Ensure file path starts with a forward slash
|
|
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
|
const hash = await sha1(content);
|
|
fileDigests[normalizedPath] = hash;
|
|
}
|
|
|
|
// Create a new deploy with digests
|
|
const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
files: fileDigests,
|
|
async: true,
|
|
skip_processing: false,
|
|
draft: false, // Change this to false for production deployments
|
|
function_schedules: [],
|
|
required: Object.keys(fileDigests), // Add this line
|
|
framework: null,
|
|
}),
|
|
});
|
|
|
|
if (!deployResponse.ok) {
|
|
return json({ error: 'Failed to create deployment' }, { status: 400 });
|
|
}
|
|
|
|
const deploy = (await deployResponse.json()) as any;
|
|
let retryCount = 0;
|
|
const maxRetries = 60;
|
|
|
|
// Poll until deploy is ready for file uploads
|
|
while (retryCount < maxRetries) {
|
|
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
const status = (await statusResponse.json()) as any;
|
|
|
|
if (status.state === 'prepared' || status.state === 'uploaded') {
|
|
// Upload all files regardless of required array
|
|
for (const [filePath, content] of Object.entries(files)) {
|
|
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
|
|
|
let uploadSuccess = false;
|
|
let uploadRetries = 0;
|
|
|
|
while (!uploadSuccess && uploadRetries < 3) {
|
|
try {
|
|
const uploadResponse = await fetch(
|
|
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
body: content,
|
|
},
|
|
);
|
|
|
|
uploadSuccess = uploadResponse.ok;
|
|
|
|
if (!uploadSuccess) {
|
|
console.error('Upload failed:', await uploadResponse.text());
|
|
uploadRetries++;
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
uploadRetries++;
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
}
|
|
|
|
if (!uploadSuccess) {
|
|
return json({ error: `Failed to upload file ${filePath}` }, { status: 500 });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (status.state === 'ready') {
|
|
// Only return after files are uploaded
|
|
if (Object.keys(files).length === 0 || status.summary?.status === 'ready') {
|
|
return json({
|
|
success: true,
|
|
deploy: {
|
|
id: status.id,
|
|
state: status.state,
|
|
url: status.ssl_url || status.url,
|
|
},
|
|
site: siteInfo,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (status.state === 'error') {
|
|
return json({ error: status.error_message || 'Deploy preparation failed' }, { status: 500 });
|
|
}
|
|
|
|
retryCount++;
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
|
|
if (retryCount >= maxRetries) {
|
|
return json({ error: 'Deploy preparation timed out' }, { status: 500 });
|
|
}
|
|
|
|
// Make sure we're returning the deploy ID and site info
|
|
return json({
|
|
success: true,
|
|
deploy: {
|
|
id: deploy.id,
|
|
state: deploy.state,
|
|
},
|
|
site: siteInfo,
|
|
});
|
|
} catch (error) {
|
|
console.error('Deploy error:', error);
|
|
return json({ error: 'Deployment failed' }, { status: 500 });
|
|
}
|
|
}
|