11import type { CLI , DeployOptions } from '@stacksjs/types'
22import { $ } from 'bun'
3+ import { existsSync , readFileSync } from 'node:fs'
4+ import { homedir } from 'node:os'
5+ import { join } from 'node:path'
36import process from 'node:process'
47import { runAction } from '@stacksjs/actions'
58import { intro , italic , log , outro , prompts , runCommand } from '@stacksjs/cli'
69import { app } from '@stacksjs/config'
710import { addDomain , hasUserDomainBeenAddedToCloud } from '@stacksjs/dns'
8- import { encryptEnv , loadEnv } from '@stacksjs/env'
11+ import { encryptEnv } from '@stacksjs/env'
912import { Action } from '@stacksjs/enums'
1013import { path as p } from '@stacksjs/path'
1114import { ExitCode } from '@stacksjs/types'
1215
16+ /**
17+ * Load AWS credentials from ~/.aws/credentials file
18+ * Returns credentials for the specified profile (or 'default'/'stacks')
19+ */
20+ function loadAwsCredentialsFromFile ( ) : { accessKeyId ?: string , secretAccessKey ?: string , region ?: string } {
21+ const credentialsPath = join ( homedir ( ) , '.aws' , 'credentials' )
22+ const configPath = join ( homedir ( ) , '.aws' , 'config' )
23+
24+ if ( ! existsSync ( credentialsPath ) ) {
25+ return { }
26+ }
27+
28+ try {
29+ const content = readFileSync ( credentialsPath , 'utf-8' )
30+ const lines = content . split ( '\n' )
31+
32+ // Try to find credentials in order: stacks profile, default profile, any profile
33+ const profiles = [ 'stacks' , 'default' ]
34+ let currentProfile = ''
35+ let credentials : { accessKeyId ?: string , secretAccessKey ?: string } = { }
36+ const profileCredentials : Record < string , { accessKeyId ?: string , secretAccessKey ?: string } > = { }
37+
38+ for ( const line of lines ) {
39+ const trimmed = line . trim ( )
40+
41+ // Check for profile header
42+ const profileMatch = trimmed . match ( / ^ \[ ( .+ ) \] $ / )
43+ if ( profileMatch ) {
44+ currentProfile = profileMatch [ 1 ]
45+ profileCredentials [ currentProfile ] = { }
46+ continue
47+ }
48+
49+ // Parse key=value
50+ const keyValue = trimmed . match ( / ^ ( \w + ) \s * = \s * ( .+ ) $ / )
51+ if ( keyValue && currentProfile ) {
52+ const [ , key , value ] = keyValue
53+ if ( key === 'aws_access_key_id' ) {
54+ profileCredentials [ currentProfile ] . accessKeyId = value
55+ }
56+ else if ( key === 'aws_secret_access_key' ) {
57+ profileCredentials [ currentProfile ] . secretAccessKey = value
58+ }
59+ }
60+ }
61+
62+ // Try to find credentials in preferred order
63+ for ( const profile of profiles ) {
64+ if ( profileCredentials [ profile ] ?. accessKeyId && profileCredentials [ profile ] ?. secretAccessKey ) {
65+ credentials = profileCredentials [ profile ]
66+ log . debug ( `Using AWS credentials from ~/.aws/credentials [${ profile } ] profile` )
67+ break
68+ }
69+ }
70+
71+ // Fallback to any available profile
72+ if ( ! credentials . accessKeyId ) {
73+ for ( const [ profile , creds ] of Object . entries ( profileCredentials ) ) {
74+ if ( creds . accessKeyId && creds . secretAccessKey ) {
75+ credentials = creds
76+ log . debug ( `Using AWS credentials from ~/.aws/credentials [${ profile } ] profile` )
77+ break
78+ }
79+ }
80+ }
81+
82+ // Try to load region from config file
83+ let region : string | undefined
84+ if ( existsSync ( configPath ) ) {
85+ const configContent = readFileSync ( configPath , 'utf-8' )
86+ const regionMatch = configContent . match ( / r e g i o n \s * = \s * ( .+ ) / )
87+ if ( regionMatch ) {
88+ region = regionMatch [ 1 ] . trim ( )
89+ }
90+ }
91+
92+ return { ...credentials , region }
93+ }
94+ catch ( error ) {
95+ log . debug ( 'Failed to read AWS credentials file:' , error )
96+ return { }
97+ }
98+ }
99+
13100export function deploy ( buddy : CLI ) : void {
14101 const descriptions = {
15102 deploy : 'Deploy your project' ,
@@ -34,22 +121,34 @@ export function deploy(buddy: CLI): void {
34121 . action ( async ( env : string | undefined , options : DeployOptions ) => {
35122 log . debug ( 'Running `buddy deploy` ...' , options )
36123
37- // Force production environment for deployment
38124 const deployEnv = env || 'production'
39- process . env . APP_ENV = deployEnv
40- process . env . NODE_ENV = deployEnv
41-
42- // Reload env with production settings BEFORE loading config
43- // Load .env first, then .env.production to ensure production values take precedence
44- loadEnv ( {
45- path : [ '.env' , '.env.production' ] ,
46- overload : true , // Override any existing env vars
47- } )
125+
126+ // Clear AWS_PROFILE to prevent credential conflicts when static credentials are provided
127+ // AWS SDK's defaultProvider prefers profile over static credentials, causing InvalidClientTokenId errors
128+ if ( process . env . AWS_ACCESS_KEY_ID && process . env . AWS_SECRET_ACCESS_KEY ) {
129+ delete process . env . AWS_PROFILE
130+ }
48131
49132 const startTime = await intro ( 'buddy deploy' )
50133
51- // Get domain directly from environment variable after reload
52- const domain = options . domain || process . env . APP_URL || app . url
134+ // For production deploy, explicitly load .env.production to get the correct domain
135+ // This ensures we use production settings even if .env.local has different values
136+ let productionUrl : string | undefined
137+ if ( deployEnv === 'production' || deployEnv === 'prod' ) {
138+ const prodEnvPath = p . projectPath ( '.env.production' )
139+ if ( existsSync ( prodEnvPath ) ) {
140+ const prodEnvContent = readFileSync ( prodEnvPath , 'utf-8' )
141+ const urlMatch = prodEnvContent . match ( / ^ A P P _ U R L = ( .+ ) $ / m)
142+ if ( urlMatch ) {
143+ productionUrl = urlMatch [ 1 ] . trim ( )
144+ log . debug ( 'Using APP_URL from .env.production:' , productionUrl )
145+ }
146+ }
147+ }
148+
149+ // Get domain from options, production env, Bun.env, or config
150+ const envUrl = typeof Bun !== 'undefined' ? Bun . env . APP_URL : process . env . APP_URL
151+ const domain = options . domain || productionUrl || envUrl || app . url
53152
54153 if ( ( options . prod || deployEnv === 'production' || deployEnv === 'prod' ) && ! options . yes )
55154 await confirmProductionDeployment ( )
@@ -70,7 +169,7 @@ export function deploy(buddy: CLI): void {
70169
71170 const result = await runAction ( Action . Deploy , options )
72171
73- if ( result . isErr ( ) ) {
172+ if ( result . isErr ) {
74173 await outro (
75174 'While running the `buddy deploy`, there was an issue' ,
76175 { startTime, useSeconds : true } ,
@@ -150,7 +249,7 @@ async function configureDomain(domain: string, options: DeployOptions, startTime
150249 startTime,
151250 } )
152251
153- if ( result . isErr ( ) ) {
252+ if ( result . isErr ) {
154253 await outro ( 'While running the `buddy deploy`, there was an issue' , { startTime, useSeconds : true } , result . error )
155254 process . exit ( ExitCode . FatalError )
156255 }
@@ -214,17 +313,36 @@ async function checkIfAwsIsBootstrapped(options?: DeployOptions) {
214313 try {
215314 log . info ( 'Ensuring AWS cloud stack exists...' )
216315
217- // Check if AWS credentials are configured
218- const hasCredentials = process . env . AWS_ACCESS_KEY_ID && process . env . AWS_SECRET_ACCESS_KEY
316+ // Check if AWS credentials are configured in env vars (non-empty values)
317+ let hasCredentials = process . env . AWS_ACCESS_KEY_ID && process . env . AWS_SECRET_ACCESS_KEY
318+
319+ // If .env doesn't have credentials, try to load from ~/.aws/credentials
320+ if ( ! hasCredentials ) {
321+ const fileCredentials = loadAwsCredentialsFromFile ( )
322+
323+ if ( fileCredentials . accessKeyId && fileCredentials . secretAccessKey ) {
324+ // Set credentials in process.env for downstream use
325+ process . env . AWS_ACCESS_KEY_ID = fileCredentials . accessKeyId
326+ process . env . AWS_SECRET_ACCESS_KEY = fileCredentials . secretAccessKey
327+ if ( fileCredentials . region && ! process . env . AWS_REGION ) {
328+ process . env . AWS_REGION = fileCredentials . region
329+ }
330+ hasCredentials = true
331+ log . success ( 'Using AWS credentials from ~/.aws/credentials' )
332+ }
333+ }
219334
220335 if ( ! hasCredentials ) {
221- log . info ( 'AWS credentials not found. Let\'s set them up for deployment.' )
336+ log . info ( 'AWS credentials not found in .env or ~/.aws/credentials.' )
337+ log . info ( 'You can either:' )
338+ log . info ( ' 1. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env.production' )
339+ log . info ( ' 2. Add credentials to ~/.aws/credentials' )
340+ log . info ( ' 3. Configure them interactively below' )
341+ console . log ( '' )
222342
223343 // If --yes flag is used, skip prompting and just inform the user
224344 if ( options ?. yes ) {
225345 log . info ( 'Skipping credential setup (--yes flag provided)' )
226- log . info ( 'Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env.production file' )
227- log . info ( 'Or run without --yes to configure credentials interactively' )
228346 process . exit ( ExitCode . FatalError )
229347 }
230348
@@ -259,11 +377,11 @@ async function checkIfAwsIsBootstrapped(options?: DeployOptions) {
259377 const stackName = `${ appName } -cloud`
260378
261379 // Use ts-cloud's CloudFormation client instead of CDK
262- const { CloudFormationClient } = await import ( '/Users/chrisbreuer/Code/ ts-cloud/packages/ts-cloud/src/ aws/cloudformation.ts ' )
380+ const { CloudFormationClient } = await import ( 'ts-cloud/aws' )
263381
382+ // Don't pass AWS_PROFILE when we have static credentials to avoid conflicts
264383 const cfnClient = new CloudFormationClient (
265- process . env . AWS_REGION || 'us-east-1' ,
266- process . env . AWS_PROFILE
384+ process . env . AWS_REGION || 'us-east-1'
267385 )
268386
269387 // Check if stack exists
0 commit comments