Skip to content

Commit dc8d090

Browse files
committed
chore: wip
1 parent dd7f333 commit dc8d090

File tree

8 files changed

+309
-99
lines changed

8 files changed

+309
-99
lines changed

resources/plugins/preloader.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ const fastCommands = ['dev', 'build', 'test', 'lint', '--version', '-v', 'versio
1313
const skipPreloader = args.length === 0 || fastCommands.includes(args[0])
1414

1515
if (!skipPreloader) {
16+
// Detect production/deployment commands and set environment accordingly BEFORE loading env files
17+
// This ensures the correct .env.{env} file is loaded with proper encryption/decryption
18+
const productionCommands = ['cloud:remove', 'cloud:rm', 'cloud:destroy', 'cloud:cleanup', 'cloud:clean-up', 'undeploy']
19+
const isProductionCommand = productionCommands.includes(args[0])
20+
21+
// Handle deploy command which can have an optional env argument: `deploy [env]`
22+
const isDeployCommand = args[0] === 'deploy'
23+
if (isDeployCommand) {
24+
// Check if second arg is an environment (not a flag starting with -)
25+
const deployEnv = args[1] && !args[1].startsWith('-') ? args[1] : 'production'
26+
process.env.APP_ENV = deployEnv
27+
process.env.NODE_ENV = deployEnv
28+
}
29+
else if (isProductionCommand) {
30+
process.env.APP_ENV = 'production'
31+
process.env.NODE_ENV = 'production'
32+
}
33+
1634
// Load .env files with encryption support using our native Bun plugin
1735
const { autoLoadEnv } = await import('@stacksjs/env')
1836

storage/framework/core/actions/src/domains/add.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import process from 'node:process'
22
import { italic, log, parseOptions, runCommand } from '@stacksjs/cli'
3-
import { app } from '@stacksjs/config'
4-
import { createHostedZone, getNameservers, updateNameservers } from '@stacksjs/dns'
3+
import { createHostedZone, getHostedZoneNameservers, updateNameservers } from '@stacksjs/dns'
4+
import { loadEnv } from '@stacksjs/env'
55
import { handleError } from '@stacksjs/error-handling'
66
import { projectConfigPath } from '@stacksjs/path'
77
import { whois } from '@stacksjs/whois'
88

9+
// Load environment variables including production settings BEFORE importing config
10+
loadEnv({
11+
path: ['.env', '.env.production'],
12+
overload: true,
13+
})
14+
915
const options = parseOptions()
10-
const domain = (options.domain as string | undefined) ?? app.url
16+
17+
// Helper to normalize domain (strip protocol and trailing slash)
18+
function normalizeDomain(url: string | undefined): string | undefined {
19+
if (!url) return undefined
20+
return url
21+
.replace(/^https?:\/\//, '') // Remove http:// or https://
22+
.replace(/\/$/, '') // Remove trailing slash
23+
}
24+
25+
// Use process.env.APP_URL directly to ensure we get the production value
26+
const domain = normalizeDomain((options.domain as string | undefined) ?? process.env.APP_URL)
1127

1228
if (!domain) {
1329
log.error(
@@ -18,15 +34,15 @@ if (!domain) {
1834

1935
const result = await createHostedZone(domain)
2036

21-
if (result.isErr()) {
37+
if (result.isErr) {
2238
handleError(result.error)
2339
process.exit(1)
2440
}
2541

26-
// Update the nameservers
27-
const nameServers = await getNameservers(domain)
42+
// Get nameservers from the hosted zone's delegation set
43+
const nameServers = await getHostedZoneNameservers(domain)
2844

29-
if (!nameServers) {
45+
if (!nameServers || nameServers.length === 0) {
3046
handleError(`No nameservers found for domain: ${domain}. Please ensure the Hosted Zone exists in Route53.`)
3147
process.exit(1)
3248
}
@@ -35,8 +51,8 @@ await updateNameservers(nameServers)
3551

3652
const registrar: string = (await whois(domain, true)).parsedData.Registrar
3753

38-
// usually for Route53 registered domains, we don't need to update create a hosted zone as its already
39-
// done for us. But in case its not, we still need to ensure its created before we can deploy
54+
// usually for Route53 registered domains, we don't need to update create a hosted zone as it's already
55+
// done for us. But in case it's not, we still need to ensure it's created before we can deploy
4056
if (registrar.includes('Amazon')) {
4157
if (options.deploy) {
4258
await runCommand('buddy deploy')

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

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ExitCode } from '@stacksjs/types'
2424
*/
2525
async function createTemporaryCdkRole(roleName: string): Promise<void> {
2626
// Import AWSClient for direct IAM API calls
27-
const { AWSClient } = await import('ts-cloud')
27+
const { AWSClient } = await import('ts-cloud/aws')
2828
const client = new AWSClient()
2929

3030
// Trust policy that allows CloudFormation to assume this role
@@ -128,7 +128,7 @@ async function createTemporaryCdkRole(roleName: string): Promise<void> {
128128
* Uses raw AWS API calls since AWS SDK has dependency issues with Bun
129129
*/
130130
async function deleteTemporaryCdkRole(roleName: string): Promise<void> {
131-
const { AWSClient } = await import('ts-cloud')
131+
const { AWSClient } = await import('ts-cloud/aws')
132132
const client = new AWSClient()
133133

134134
try {
@@ -235,7 +235,7 @@ export function cloud(buddy: CLI): void {
235235
log.info('Invalidating the CloudFront cache...')
236236

237237
// Use ts-cloud CloudFront client instead of AWS SDK
238-
const { CloudFrontClient } = await import('ts-cloud')
238+
const { CloudFrontClient } = await import('ts-cloud/aws')
239239
const cloudfront = new CloudFrontClient()
240240
const distributionId = await getCloudFrontDistributionId()
241241

@@ -347,15 +347,6 @@ export function cloud(buddy: CLI): void {
347347
.action(async (options: CloudCliOptions) => {
348348
log.debug('Running `buddy cloud:remove` ...', options)
349349

350-
// Load production environment variables
351-
const { loadEnv } = await import('@stacksjs/env')
352-
process.env.APP_ENV = 'production'
353-
process.env.NODE_ENV = 'production'
354-
loadEnv({
355-
path: ['.env', '.env.production'],
356-
overload: true,
357-
})
358-
359350
const startTime = await intro('buddy cloud:remove')
360351

361352
if (options.jumpBox) {
@@ -401,7 +392,7 @@ export function cloud(buddy: CLI): void {
401392

402393
try {
403394
// Use ts-cloud's CloudFormation client instead of CDK
404-
const { CloudFormationClient } = await import('ts-cloud')
395+
const { CloudFormationClient } = await import('ts-cloud/aws')
405396

406397
// Unset AWS_PROFILE to force AWS SDK to use static credentials from .env.production
407398
delete process.env.AWS_PROFILE
@@ -730,15 +721,6 @@ export function cloud(buddy: CLI): void {
730721

731722
const startTime = await intro('buddy cloud:cleanup')
732723

733-
// Load production environment variables for AWS credentials
734-
const { loadEnv } = await import('@stacksjs/env')
735-
process.env.APP_ENV = 'production'
736-
process.env.NODE_ENV = 'production'
737-
loadEnv({
738-
path: ['.env', '.env.production'],
739-
overload: true,
740-
})
741-
742724
// Unset AWS_PROFILE to force AWS SDK to use static credentials from .env.production
743725
delete process.env.AWS_PROFILE
744726

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

Lines changed: 141 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,102 @@
11
import type { CLI, DeployOptions } from '@stacksjs/types'
22
import { $ } from 'bun'
3+
import { existsSync, readFileSync } from 'node:fs'
4+
import { homedir } from 'node:os'
5+
import { join } from 'node:path'
36
import process from 'node:process'
47
import { runAction } from '@stacksjs/actions'
58
import { intro, italic, log, outro, prompts, runCommand } from '@stacksjs/cli'
69
import { app } from '@stacksjs/config'
710
import { addDomain, hasUserDomainBeenAddedToCloud } from '@stacksjs/dns'
8-
import { encryptEnv, loadEnv } from '@stacksjs/env'
11+
import { encryptEnv } from '@stacksjs/env'
912
import { Action } from '@stacksjs/enums'
1013
import { path as p } from '@stacksjs/path'
1114
import { 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(/region\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+
13100
export 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(/^APP_URL=(.+)$/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

Comments
 (0)