@@ -33,23 +33,37 @@ export function deploy(buddy: CLI): void {
3333 . action ( async ( env : string | undefined , options : DeployOptions ) => {
3434 log . debug ( 'Running `buddy deploy` ...' , options )
3535
36+ // Force production environment for deployment
37+ const deployEnv = env || 'production'
38+ process . env . APP_ENV = deployEnv
39+ process . env . NODE_ENV = deployEnv
40+
41+ // Reload env with production settings BEFORE loading config
42+ const { loadEnv } = await import ( '@stacksjs/env' )
43+ loadEnv ( {
44+ path : [ '.env.production' , '.env' ] ,
45+ overload : true , // Override any existing env vars
46+ } )
47+
3648 const startTime = await intro ( 'buddy deploy' )
37- const domain = options . domain || app . url
3849
39- if ( ( options . prod || env === 'production' || env === 'prod' ) && ! options . yes )
50+ // Get domain directly from environment variable after reload
51+ const domain = options . domain || process . env . APP_URL || app . url
52+
53+ if ( ( options . prod || deployEnv === 'production' || deployEnv === 'prod' ) && ! options . yes )
4054 await confirmProductionDeployment ( )
4155
4256 if ( ! domain ) {
43- log . info ( 'No domain found in your .env or ./config/app.ts' )
57+ log . info ( 'No domain found in your .env.production or ./config/app.ts' )
4458 log . info ( 'Please ensure your domain is properly configured.' )
4559 log . info ( 'For more info, check out the docs or join our Discord.' )
4660 process . exit ( ExitCode . FatalError )
4761 }
4862
49- log . info ( `Deploying to ${ italic ( domain ) } ` )
63+ log . info ( `Deploying to ${ italic ( domain ) } ( ${ deployEnv } ) ` )
5064
5165 await checkIfAwsIsConfigured ( )
52- await checkIfAwsIsBootstrapped ( )
66+ await checkIfAwsIsBootstrapped ( options )
5367
5468 options . domain = await configureDomain ( domain , options , startTime )
5569
@@ -169,36 +183,205 @@ async function checkIfAwsIsConfigured() {
169183 }
170184}
171185
172- async function checkIfAwsIsBootstrapped ( ) {
186+ async function checkIfAwsIsBootstrapped ( options ?: DeployOptions ) {
173187 try {
174- log . info ( 'Ensuring AWS is bootstrapped...' )
175- const result = await runCommand ( 'aws cloudformation describe-stacks --stack-name stacks-cloud' , { silent : true } )
188+ log . info ( 'Ensuring AWS cloud stack exists...' )
189+
190+ // Check if AWS credentials are configured
191+ if ( ! process . env . AWS_ACCESS_KEY_ID || ! process . env . AWS_SECRET_ACCESS_KEY ) {
192+ log . warn ( 'AWS credentials not found in environment' )
193+ log . info ( 'Let\'s set up your AWS credentials for deployment' )
194+ console . log ( '' )
195+
196+ // If --yes flag is used, skip prompting and just inform the user
197+ if ( options ?. yes ) {
198+ log . info ( 'Skipping credential setup (--yes flag provided)' )
199+ log . info ( 'Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env.production file' )
200+ log . info ( 'Or run without --yes to configure credentials interactively' )
201+ process . exit ( ExitCode . FatalError )
202+ }
176203
177- // Check if command was successful
178- if ( result && typeof result . isErr === 'function' && result . isErr ( ) ) {
179- throw new Error ( 'Stack not found' )
204+ const setupCredentials = await prompts . confirm ( {
205+ message : 'Would you like to configure AWS credentials now?' ,
206+ initial : true ,
207+ } )
208+
209+ log . debug ( 'setupCredentials response:' , setupCredentials , typeof setupCredentials )
210+
211+ // Handle user cancellation (Ctrl+C or ESC) or explicit "no"
212+ if ( setupCredentials === undefined || setupCredentials === false ) {
213+ if ( setupCredentials === undefined ) {
214+ console . log ( '' )
215+ log . info ( 'Deployment cancelled' )
216+ process . exit ( ExitCode . Success )
217+ }
218+ console . log ( '' )
219+ log . info ( 'Skipping cloud infrastructure check' )
220+ log . info ( 'You can configure AWS credentials later by running: buddy configure:aws' )
221+ return true
222+ }
223+
224+ // Prompt for AWS credentials
225+ const accessKeyId = await prompts . text ( {
226+ message : 'AWS Access Key ID:' ,
227+ validate : ( value : string ) => value . length > 0 ? true : 'Access Key ID is required' ,
228+ } )
229+
230+ if ( ! accessKeyId ) {
231+ log . info ( 'Deployment cancelled' )
232+ process . exit ( ExitCode . Success )
233+ }
234+
235+ const secretAccessKey = await prompts . password ( {
236+ message : 'AWS Secret Access Key:' ,
237+ validate : ( value : string ) => value . length > 0 ? true : 'Secret Access Key is required' ,
238+ } )
239+
240+ if ( ! secretAccessKey ) {
241+ log . info ( 'Deployment cancelled' )
242+ process . exit ( ExitCode . Success )
243+ }
244+
245+ const region = await prompts . text ( {
246+ message : 'AWS Region:' ,
247+ initial : 'us-east-1' ,
248+ } )
249+
250+ if ( ! region ) {
251+ log . info ( 'Deployment cancelled' )
252+ process . exit ( ExitCode . Success )
253+ }
254+
255+ // Save credentials to .env.production
256+ const fs = await import ( 'node:fs' )
257+ const envPath = p . projectPath ( '.env.production' )
258+ let envContent = fs . readFileSync ( envPath , 'utf-8' )
259+
260+ // Update or add AWS credentials
261+ if ( envContent . includes ( 'AWS_ACCESS_KEY_ID=' ) ) {
262+ envContent = envContent . replace ( / A W S _ A C C E S S _ K E Y _ I D = .* $ / m, `AWS_ACCESS_KEY_ID=${ accessKeyId } ` )
263+ }
264+ else {
265+ envContent += `\nAWS_ACCESS_KEY_ID=${ accessKeyId } `
266+ }
267+
268+ if ( envContent . includes ( 'AWS_SECRET_ACCESS_KEY=' ) ) {
269+ envContent = envContent . replace ( / A W S _ S E C R E T _ A C C E S S _ K E Y = .* $ / m, `AWS_SECRET_ACCESS_KEY=${ secretAccessKey } ` )
270+ }
271+ else {
272+ envContent += `\nAWS_SECRET_ACCESS_KEY=${ secretAccessKey } `
273+ }
274+
275+ if ( envContent . includes ( 'AWS_REGION=' ) ) {
276+ envContent = envContent . replace ( / A W S _ R E G I O N = .* $ / m, `AWS_REGION=${ region } ` )
277+ }
278+ else {
279+ envContent += `\nAWS_REGION=${ region } `
280+ }
281+
282+ fs . writeFileSync ( envPath , envContent )
283+
284+ // Update process.env
285+ process . env . AWS_ACCESS_KEY_ID = accessKeyId
286+ process . env . AWS_SECRET_ACCESS_KEY = secretAccessKey
287+ process . env . AWS_REGION = region
288+
289+ log . success ( 'AWS credentials saved to .env.production' )
290+ console . log ( '' )
180291 }
181292
182- log . success ( 'AWS is bootstrapped' )
183- return true
184- }
185- catch ( err : any ) {
186- log . debug ( `Not yet bootstrapped. Error: ${ err . message } ` )
293+ // Use ts-cloud's CloudFormation client instead of CDK
294+ const { CloudFormationClient } = await import ( '/Users/chrisbreuer/Code/ts-cloud/packages/ts-cloud/src/aws/cloudformation.ts' )
295+
296+ const cfnClient = new CloudFormationClient (
297+ process . env . AWS_REGION || 'us-east-1' ,
298+ process . env . AWS_PROFILE
299+ )
300+
301+ // Check if stack exists
302+ try {
303+ const result = await cfnClient . describeStacks ( { stackName : 'stacks-cloud' } )
187304
188- log . info ( 'AWS is not bootstrapped yet' )
189- log . info ( 'Bootstrapping. This may take a few moments...' )
305+ if ( result . Stacks && result . Stacks . length > 0 ) {
306+ log . success ( 'Cloud stack exists' )
307+ return true
308+ }
309+ }
310+ catch ( error : any ) {
311+ log . debug ( `Stack not found: ${ error . message } ` )
312+ // Stack doesn't exist, we'll create it below
313+ }
314+
315+ log . info ( 'Cloud stack not found' )
316+ log . info ( 'Creating cloud infrastructure. This may take a few moments...' )
317+
318+ // Create basic CloudFormation template for Stacks cloud infrastructure
319+ const template = {
320+ AWSTemplateFormatVersion : '2010-09-09' ,
321+ Description : 'Stacks Cloud Infrastructure' ,
322+ Resources : {
323+ StacksBucket : {
324+ Type : 'AWS::S3::Bucket' ,
325+ Properties : {
326+ BucketName : `stacks-${ process . env . APP_ENV || 'production' } -assets` ,
327+ PublicAccessBlockConfiguration : {
328+ BlockPublicAcls : false ,
329+ BlockPublicPolicy : false ,
330+ IgnorePublicAcls : false ,
331+ RestrictPublicBuckets : false ,
332+ } ,
333+ WebsiteConfiguration : {
334+ IndexDocument : 'index.html' ,
335+ ErrorDocument : 'error.html' ,
336+ } ,
337+ } ,
338+ } ,
339+ } ,
340+ Outputs : {
341+ BucketName : {
342+ Description : 'Name of the S3 bucket' ,
343+ Value : { Ref : 'StacksBucket' } ,
344+ Export : {
345+ Name : 'StacksBucketName' ,
346+ } ,
347+ } ,
348+ BucketWebsiteURL : {
349+ Description : 'URL of the S3 bucket website' ,
350+ Value : { 'Fn::GetAtt' : [ 'StacksBucket' , 'WebsiteURL' ] } ,
351+ } ,
352+ } ,
353+ }
190354
191355 try {
192- $ . cwd ( p . frameworkCloudPath ( ) )
193- const result = await $ `bun run bootstrap`
194- console . log ( result )
195- log . success ( 'AWS bootstrapped successfully' )
356+ // Create the stack using ts-cloud
357+ const result = await cfnClient . createStack ( {
358+ stackName : 'stacks-cloud' ,
359+ templateBody : JSON . stringify ( template ) ,
360+ capabilities : [ 'CAPABILITY_IAM' ] ,
361+ tags : [
362+ { Key : 'Environment' , Value : process . env . APP_ENV || 'production' } ,
363+ { Key : 'ManagedBy' , Value : 'Stacks' } ,
364+ ] ,
365+ } )
366+
367+ log . info ( `Stack creation initiated: ${ result . StackId } ` )
368+ log . info ( 'Waiting for stack creation to complete...' )
369+
370+ // Wait for stack creation to complete
371+ await cfnClient . waitForStack ( 'stacks-cloud' , 'stack-create-complete' )
372+
373+ log . success ( 'Cloud infrastructure created successfully' )
196374 return true
197375 }
198- catch ( error ) {
199- log . error ( 'Failed to bootstrap AWS ' )
376+ catch ( error : any ) {
377+ log . error ( 'Failed to create cloud infrastructure ' )
200378 console . error ( error )
201379 process . exit ( ExitCode . FatalError )
202380 }
203381 }
382+ catch ( err : any ) {
383+ log . error ( 'Error checking cloud infrastructure' )
384+ console . error ( err )
385+ process . exit ( ExitCode . FatalError )
386+ }
204387}
0 commit comments