1+ import { toast } from 'react-toastify' ;
2+ import { useStore } from '@nanostores/react' ;
3+ import { workbenchStore } from '~/lib/stores/workbench' ;
4+ import { webcontainer } from '~/lib/webcontainer' ;
5+ import { path } from '~/utils/path' ;
6+ import { useState } from 'react' ;
7+ import type { ActionCallbackData } from '~/lib/runtime/message-parser' ;
8+ import { chatId } from '~/lib/persistence/useChatHistory' ;
9+ import { getLocalStorage } from '~/lib/persistence/localStorage' ;
10+
11+ export function useGitHubDeploy ( ) {
12+ const [ isDeploying , setIsDeploying ] = useState ( false ) ;
13+ const currentChatId = useStore ( chatId ) ;
14+
15+ const handleGitHubDeploy = async ( ) => {
16+ const connection = getLocalStorage ( 'github_connection' ) ;
17+
18+ if ( ! connection ?. token || ! connection ?. user ) {
19+ toast . error ( 'Please connect your GitHub account in Settings > Connections first' ) ;
20+ return false ;
21+ }
22+
23+ if ( ! currentChatId ) {
24+ toast . error ( 'No active chat found' ) ;
25+ return false ;
26+ }
27+
28+ try {
29+ setIsDeploying ( true ) ;
30+
31+ const artifact = workbenchStore . firstArtifact ;
32+
33+ if ( ! artifact ) {
34+ throw new Error ( 'No active project found' ) ;
35+ }
36+
37+ // Create a deployment artifact for visual feedback
38+ const deploymentId = `deploy-github-project` ;
39+ workbenchStore . addArtifact ( {
40+ id : deploymentId ,
41+ messageId : deploymentId ,
42+ title : 'GitHub Deployment' ,
43+ type : 'standalone' ,
44+ } ) ;
45+
46+ const deployArtifact = workbenchStore . artifacts . get ( ) [ deploymentId ] ;
47+
48+ // Notify that build is starting
49+ deployArtifact . runner . handleDeployAction ( 'building' , 'running' , { source : 'github' } ) ;
50+
51+ const actionId = 'build-' + Date . now ( ) ;
52+ const actionData : ActionCallbackData = {
53+ messageId : 'github build' ,
54+ artifactId : artifact . id ,
55+ actionId,
56+ action : {
57+ type : 'build' as const ,
58+ content : 'npm run build' ,
59+ } ,
60+ } ;
61+
62+ // Add the action first
63+ artifact . runner . addAction ( actionData ) ;
64+
65+ // Then run it
66+ await artifact . runner . runAction ( actionData ) ;
67+
68+ if ( ! artifact . runner . buildOutput ) {
69+ // Notify that build failed
70+ deployArtifact . runner . handleDeployAction ( 'building' , 'failed' , {
71+ error : 'Build failed. Check the terminal for details.' ,
72+ source : 'github' ,
73+ } ) ;
74+ throw new Error ( 'Build failed' ) ;
75+ }
76+
77+ // Notify that build succeeded and deployment preparation is starting
78+ deployArtifact . runner . handleDeployAction ( 'deploying' , 'running' , {
79+ message : 'Preparing files for GitHub deployment...' ,
80+ source : 'github'
81+ } ) ;
82+
83+ // Get all project files instead of just the build directory since we're deploying to a repository
84+ const container = await webcontainer ;
85+
86+ // Get all files recursively - we'll deploy the entire project, not just the build directory
87+ async function getAllFiles ( dirPath : string , basePath : string = '' ) : Promise < Record < string , string > > {
88+ const files : Record < string , string > = { } ;
89+ const entries = await container . fs . readdir ( dirPath , { withFileTypes : true } ) ;
90+
91+ for ( const entry of entries ) {
92+ const fullPath = path . join ( dirPath , entry . name ) ;
93+ // Create a relative path without the leading slash for GitHub
94+ const relativePath = basePath ? `${ basePath } /${ entry . name } ` : entry . name ;
95+
96+ // Skip node_modules, .git directories and other common excludes
97+ if ( entry . isDirectory ( ) && (
98+ entry . name === 'node_modules' ||
99+ entry . name === '.git' ||
100+ entry . name === 'dist' ||
101+ entry . name === 'build' ||
102+ entry . name === '.cache' ||
103+ entry . name === '.next'
104+ ) ) {
105+ continue ;
106+ }
107+
108+ if ( entry . isFile ( ) ) {
109+ // Skip binary files, large files and other common excludes
110+ if ( entry . name . endsWith ( '.DS_Store' ) ||
111+ entry . name . endsWith ( '.log' ) ||
112+ entry . name . startsWith ( '.env' ) ) {
113+ continue ;
114+ }
115+
116+ try {
117+ const content = await container . fs . readFile ( fullPath , 'utf-8' ) ;
118+ // Store the file with its relative path, not the full system path
119+ files [ relativePath ] = content ;
120+ } catch ( error ) {
121+ console . warn ( `Could not read file ${ fullPath } :` , error ) ;
122+ continue ;
123+ }
124+ } else if ( entry . isDirectory ( ) ) {
125+ const subFiles = await getAllFiles ( fullPath , relativePath ) ;
126+ Object . assign ( files , subFiles ) ;
127+ }
128+ }
129+
130+ return files ;
131+ }
132+
133+ const fileContents = await getAllFiles ( '/' ) ;
134+
135+ // Show GitHub deployment dialog here - it will handle the actual deployment
136+ // and will receive these files to deploy
137+
138+ // For now, we'll just complete the deployment with a success message
139+ // Notify that deployment preparation is complete
140+ deployArtifact . runner . handleDeployAction ( 'deploying' , 'complete' , {
141+ message : 'Files prepared for GitHub deployment' ,
142+ source : 'github'
143+ } ) ;
144+
145+ return {
146+ success : true ,
147+ files : fileContents ,
148+ projectName : artifact . title || 'bolt-project'
149+ } ;
150+ } catch ( err ) {
151+ console . error ( 'GitHub deploy error:' , err ) ;
152+ toast . error ( err instanceof Error ? err . message : 'GitHub deployment preparation failed' ) ;
153+ return false ;
154+ } finally {
155+ setIsDeploying ( false ) ;
156+ }
157+ } ;
158+
159+ return {
160+ isDeploying,
161+ handleGitHubDeploy,
162+ isConnected : ! ! getLocalStorage ( 'github_connection' ) ?. user ,
163+ } ;
164+ }
0 commit comments