Skip to content

Commit c134177

Browse files
committed
chore: wip
1 parent 0115522 commit c134177

File tree

3 files changed

+309
-96
lines changed

3 files changed

+309
-96
lines changed

storage/framework/cloud/bunfig.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
preload = [
2-
"../../../node_modules/bun-plugin-env/src/index.ts"
3-
]
1+
# Disabled preload for cloud operations - env is loaded by buddy deploy command
2+
# preload = [
3+
# "../core/env/src/plugin.ts"
4+
# ]
45

56
[install]
67
# set default registry as a string

storage/framework/core/buddy/src/commands/deploy.ts

Lines changed: 207 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/AWS_ACCESS_KEY_ID=.*$/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(/AWS_SECRET_ACCESS_KEY=.*$/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(/AWS_REGION=.*$/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

Comments
 (0)