grada-run 0.0.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-docs.yml +37 -0
- package/.github/workflows/iac-validation.yml +303 -0
- package/.github/workflows/publish.yml +68 -0
- package/.github/workflows/sync-bedrock-models.yml +57 -0
- package/.github/workflows/test.yml +43 -0
- package/.muserules +31 -0
- package/LICENSE +21 -0
- package/README.md +193 -3
- package/apps/docs/.astro/collections/docs.schema.json +644 -0
- package/apps/docs/.astro/content-assets.mjs +4 -0
- package/apps/docs/.astro/content-modules.mjs +4 -0
- package/apps/docs/.astro/content.d.ts +179 -0
- package/apps/docs/.astro/data-store.json +1 -0
- package/apps/docs/.astro/dev.json +14 -0
- package/apps/docs/.astro/settings.json +5 -0
- package/apps/docs/.astro/types.d.ts +2 -0
- package/apps/docs/astro.config.mjs +97 -0
- package/apps/docs/package.json +17 -0
- package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
- package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
- package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
- package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
- package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
- package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
- package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
- package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
- package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
- package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
- package/apps/docs/src/content/docs/cli/add.md +84 -0
- package/apps/docs/src/content/docs/cli/apply.md +32 -0
- package/apps/docs/src/content/docs/cli/db.md +200 -0
- package/apps/docs/src/content/docs/cli/destroy.md +31 -0
- package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
- package/apps/docs/src/content/docs/cli/doctor.md +28 -0
- package/apps/docs/src/content/docs/cli/domain.md +57 -0
- package/apps/docs/src/content/docs/cli/drift.md +40 -0
- package/apps/docs/src/content/docs/cli/eject.md +29 -0
- package/apps/docs/src/content/docs/cli/exec.md +49 -0
- package/apps/docs/src/content/docs/cli/gc.md +37 -0
- package/apps/docs/src/content/docs/cli/init.md +72 -0
- package/apps/docs/src/content/docs/cli/logs.md +39 -0
- package/apps/docs/src/content/docs/cli/rollback.md +51 -0
- package/apps/docs/src/content/docs/cli/secrets.md +73 -0
- package/apps/docs/src/content/docs/cli/sleep.md +53 -0
- package/apps/docs/src/content/docs/cli/status.md +34 -0
- package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
- package/apps/docs/src/content/docs/guides/architecture.md +87 -0
- package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
- package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
- package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
- package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
- package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
- package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
- package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
- package/apps/docs/src/content/docs/guides/examples.md +50 -0
- package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
- package/apps/docs/src/content/docs/guides/headless.md +75 -0
- package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
- package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
- package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
- package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
- package/apps/docs/src/content/docs/index.mdx +103 -0
- package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
- package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
- package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
- package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
- package/apps/docs/src/content/docs/roadmap.md +97 -0
- package/apps/docs/src/content/docs/testing-strategy.md +32 -0
- package/apps/docs/src/content.config.ts +7 -0
- package/apps/docs/src/custom.css +14 -0
- package/apps/docs/tsconfig.json +6 -0
- package/bin/cli.js +140 -0
- package/package.json +105 -7
- package/scripts/sync-bedrock-models.js +22 -0
- package/scripts/test-iac.js +261 -0
- package/specs/add-redis-sqs-bedrock.md +128 -0
- package/specs/add-storage-dynamodb.md +106 -0
- package/specs/bedrock-model-catalog.md +131 -0
- package/specs/ci-pipeline.md +17 -0
- package/specs/cost-transparency.md +115 -0
- package/specs/custom-domains-and-ses.md +153 -0
- package/specs/database-suite-expansion.md +151 -0
- package/specs/db-connect.md +69 -0
- package/specs/db-lifecycle-migrations.md +159 -0
- package/specs/dependency-aware-init.md +176 -0
- package/specs/deploy-stack-to-grada-run-rebrand.md +45 -0
- package/specs/deployment-safety.md +170 -0
- package/specs/diagnose.md +16 -0
- package/specs/docs-hub.md +16 -0
- package/specs/dx-polish.md +46 -0
- package/specs/exec.md +25 -0
- package/specs/finops-cron-drift.md +161 -0
- package/specs/gc.md +26 -0
- package/specs/integration-suite.md +16 -0
- package/specs/logs.md +32 -0
- package/specs/rollback-live-polling.md +40 -0
- package/specs/secrets-pull-audit.md +51 -0
- package/specs/serverless-lambda-target.md +133 -0
- package/specs/status.md +31 -0
- package/specs/telemetry-and-spawn-hardening.md +69 -0
- package/specs/telemetry-hardening.md +35 -0
- package/src/commands/add.js +1111 -0
- package/src/commands/apply.js +214 -0
- package/src/commands/db/backup.js +229 -0
- package/src/commands/db/connect.js +304 -0
- package/src/commands/db/enable-vector.js +344 -0
- package/src/commands/db/import.js +604 -0
- package/src/commands/db/migrate.js +477 -0
- package/src/commands/db/restore.js +361 -0
- package/src/commands/db.js +87 -0
- package/src/commands/destroy.js +217 -0
- package/src/commands/diagnose.js +460 -0
- package/src/commands/doctor.js +109 -0
- package/src/commands/domain.js +685 -0
- package/src/commands/drift.js +243 -0
- package/src/commands/eject.js +127 -0
- package/src/commands/exec.js +222 -0
- package/src/commands/gc.js +250 -0
- package/src/commands/init.js +649 -0
- package/src/commands/logs.js +256 -0
- package/src/commands/rollback.js +323 -0
- package/src/commands/secrets.js +485 -0
- package/src/commands/sleep.js +347 -0
- package/src/commands/status.js +309 -0
- package/src/commands/sync-ai.js +115 -0
- package/src/commands/wake.js +337 -0
- package/src/core/parser.js +126 -0
- package/src/core/telemetry.js +244 -0
- package/src/data/bedrock-models.json +896 -0
- package/src/utils/addons.js +126 -0
- package/src/utils/ai-rules.js +59 -0
- package/src/utils/args.js +91 -0
- package/src/utils/aws.js +178 -0
- package/src/utils/backup.js +69 -0
- package/src/utils/bedrock-catalog.js +511 -0
- package/src/utils/capabilities.js +500 -0
- package/src/utils/command.js +65 -0
- package/src/utils/db-tunnel.js +164 -0
- package/src/utils/detector.js +298 -0
- package/src/utils/dockerCompose.js +65 -0
- package/src/utils/domains.js +73 -0
- package/src/utils/ecs-runner.js +289 -0
- package/src/utils/ecs.js +92 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +527 -0
- package/src/utils/hcl.js +426 -0
- package/src/utils/lambda-ecr.js +185 -0
- package/src/utils/prompts.js +278 -0
- package/src/utils/rds.js +131 -0
- package/src/utils/resolvers.js +174 -0
- package/src/utils/sleep-state.js +140 -0
- package/src/utils/sleep-targets.js +139 -0
- package/src/utils/system.js +42 -0
- package/src/utils/terraform.js +70 -0
- package/src/utils/visualizer.js +381 -0
- package/src/utils/warnings.js +49 -0
- package/templates/README.md +150 -0
- package/templates/docker/django.Dockerfile +40 -0
- package/templates/docker/go.Dockerfile +23 -0
- package/templates/docker/nestjs.Dockerfile +33 -0
- package/templates/docker/nextjs.Dockerfile +55 -0
- package/templates/docker/node.Dockerfile +24 -0
- package/templates/docker/nuxt.Dockerfile +47 -0
- package/templates/docker/python.Dockerfile +38 -0
- package/templates/docker/rails.Dockerfile +59 -0
- package/templates/docker/static.Dockerfile +32 -0
- package/templates/docker/svelte.Dockerfile +52 -0
- package/templates/github/deploy-lambda.yml +120 -0
- package/templates/github/deploy.yml +138 -0
- package/templates/github/drift.yml +112 -0
- package/templates/github/preview-lambda.yml +86 -0
- package/templates/github/preview.yml +69 -0
- package/templates/github/teardown.yml +43 -0
- package/templates/terraform/addons/bedrock.tf +34 -0
- package/templates/terraform/addons/cron-lambda.tf +78 -0
- package/templates/terraform/addons/cron.tf +101 -0
- package/templates/terraform/addons/dynamodb.tf +73 -0
- package/templates/terraform/addons/redis.tf +64 -0
- package/templates/terraform/addons/s3.tf +143 -0
- package/templates/terraform/addons/ses.tf +73 -0
- package/templates/terraform/addons/sqs.tf +67 -0
- package/templates/terraform/backend.tf +22 -0
- package/templates/terraform/cloudfront-lambda.tf +80 -0
- package/templates/terraform/cloudfront.tf +80 -0
- package/templates/terraform/database-aurora-postgresql.tf +92 -0
- package/templates/terraform/database-mysql.tf +72 -0
- package/templates/terraform/database.tf +71 -0
- package/templates/terraform/main-lambda.tf +229 -0
- package/templates/terraform/main.tf +296 -0
- package/templates/terraform/network.tf +95 -0
- package/templates/terraform/oidc.tf +64 -0
- package/templates/terraform/secrets.tf +31 -0
- package/templates/terraform/worker.tf +69 -0
- package/tests/__snapshots__/generator.test.js.snap +9633 -0
- package/tests/add.test.js +2037 -0
- package/tests/ai.test.js +94 -0
- package/tests/apply.test.js +488 -0
- package/tests/args.test.js +86 -0
- package/tests/aws.test.js +244 -0
- package/tests/capabilities.test.js +307 -0
- package/tests/cli.test.js +29 -0
- package/tests/command.test.js +100 -0
- package/tests/commands-import.test.js +74 -0
- package/tests/db.test.js +2704 -0
- package/tests/destroy.test.js +391 -0
- package/tests/detector.test.js +79 -0
- package/tests/diagnose.test.js +779 -0
- package/tests/doctor.test.js +202 -0
- package/tests/domain.test.js +899 -0
- package/tests/drift.test.js +243 -0
- package/tests/ecs.test.js +130 -0
- package/tests/eject.test.js +65 -0
- package/tests/exec.test.js +380 -0
- package/tests/gc.test.js +496 -0
- package/tests/generator.test.js +794 -0
- package/tests/headless.test.js +562 -0
- package/tests/lambda-ecr.test.js +185 -0
- package/tests/logs.test.js +447 -0
- package/tests/parser.test.js +160 -0
- package/tests/rds.test.js +244 -0
- package/tests/resolvers.test.js +282 -0
- package/tests/rollback.test.js +692 -0
- package/tests/secrets.test.js +752 -0
- package/tests/sleep-wake.test.js +1016 -0
- package/tests/status.test.js +370 -0
- package/tests/system.test.js +70 -0
- package/tests/telemetry.test.js +520 -0
- package/tests/terraform.test.js +84 -0
- package/tests/visualizer.test.js +496 -0
- package/vitest.config.js +9 -0
- package/index.js +0 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { text, select, multiselect, confirm, group, cancel } from '@clack/prompts';
|
|
3
|
+
import color from 'picocolors';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
// Sanitizes a raw project name for AWS resource compatibility (ALB names
|
|
7
|
+
// allow `[a-zA-Z0-9-]` only; ECR repositories require lowercase): trim,
|
|
8
|
+
// lowercase, collapse every run of characters outside `[a-z0-9-]` —
|
|
9
|
+
// including dots and underscores — into a single hyphen, strip
|
|
10
|
+
// leading/trailing hyphens, and fall back to `'app'` when empty.
|
|
11
|
+
export function sanitizeProjectName(rawName) {
|
|
12
|
+
const sanitized = String(rawName ?? '')
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
16
|
+
.replace(/^-+|-+$/g, '');
|
|
17
|
+
return sanitized || 'app';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveActualProjectName(rawName) {
|
|
21
|
+
const actualProjectName = sanitizeProjectName(rawName);
|
|
22
|
+
if (actualProjectName !== String(rawName ?? '').trim()) {
|
|
23
|
+
console.log(color.dim(`Project name sanitized to "${actualProjectName}" for AWS resource compatibility.`));
|
|
24
|
+
}
|
|
25
|
+
return actualProjectName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getTargetDirectory(isHeadless, headlessOptions) {
|
|
29
|
+
if (isHeadless) {
|
|
30
|
+
const projectName = headlessOptions.dir || '.';
|
|
31
|
+
const rawName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
32
|
+
return {
|
|
33
|
+
projectName,
|
|
34
|
+
actualProjectName: resolveActualProjectName(rawName),
|
|
35
|
+
targetDir: projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectName = await text({
|
|
40
|
+
message: 'Where should we generate the infrastructure? (Type "." for current directory)',
|
|
41
|
+
placeholder: '.',
|
|
42
|
+
initialValue: '.',
|
|
43
|
+
validate: (value) => {
|
|
44
|
+
if (!value) return 'Please enter a name or directory.';
|
|
45
|
+
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (typeof projectName === 'symbol') {
|
|
50
|
+
cancel('Operation cancelled.');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rawName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
55
|
+
return {
|
|
56
|
+
projectName,
|
|
57
|
+
actualProjectName: resolveActualProjectName(rawName),
|
|
58
|
+
targetDir: projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getProjectConfig(isHeadless, headlessOptions, targetDir, detectedFramework, hints = {}) {
|
|
63
|
+
const capabilities = hints && typeof hints === 'object' ? hints.capabilities : null;
|
|
64
|
+
const explicitDbEngine = hints && typeof hints === 'object' && typeof hints.dbEngine === 'string' && hints.dbEngine
|
|
65
|
+
? hints.dbEngine
|
|
66
|
+
: null;
|
|
67
|
+
// Lambda functions are fixed at 512 MB with no ALB health checks or
|
|
68
|
+
// replica counts, so the ECS sizing prompts are skipped for that target.
|
|
69
|
+
const isLambda = hints && typeof hints === 'object' && hints.target === 'lambda';
|
|
70
|
+
if (isHeadless) {
|
|
71
|
+
return {
|
|
72
|
+
framework: headlessOptions.framework || (detectedFramework ? detectedFramework.id : 'static'),
|
|
73
|
+
region: headlessOptions.region || 'us-east-2',
|
|
74
|
+
port: headlessOptions.port || (headlessOptions.framework === 'static' ? '8080' : '3000'),
|
|
75
|
+
size: headlessOptions.size || 'micro',
|
|
76
|
+
healthCheckPath: headlessOptions.healthCheckPath || '/',
|
|
77
|
+
desiredCount: headlessOptions.desiredCount || '1',
|
|
78
|
+
branch: headlessOptions.branch || 'main',
|
|
79
|
+
needsDatabase: headlessOptions.needsDatabase === 'true' || headlessOptions.needsDatabase === true,
|
|
80
|
+
dbEngine: 'postgres',
|
|
81
|
+
enablePrPreviews: headlessOptions.enablePrPreviews === 'true' || headlessOptions.enablePrPreviews === true,
|
|
82
|
+
setupType: 'headless'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
87
|
+
|
|
88
|
+
if (!finalFramework) {
|
|
89
|
+
finalFramework = await select({
|
|
90
|
+
message: 'Which framework preset should we configure?',
|
|
91
|
+
options: [
|
|
92
|
+
{ value: 'node', label: 'Node.js / Express' },
|
|
93
|
+
{ value: 'nestjs', label: 'NestJS' },
|
|
94
|
+
{ value: 'nextjs', label: 'Next.js (Standalone)' },
|
|
95
|
+
{ value: 'nuxt', label: 'Nuxt 3 (SSR)' },
|
|
96
|
+
{ value: 'svelte', label: 'SvelteKit (SSR)' },
|
|
97
|
+
{ value: 'python', label: 'Python FastAPI' },
|
|
98
|
+
{ value: 'django', label: 'Django (Python)' },
|
|
99
|
+
{ value: 'rails', label: 'Ruby on Rails' },
|
|
100
|
+
{ value: 'go', label: 'Go (Golang)' },
|
|
101
|
+
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
if (typeof finalFramework === 'symbol') process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const setupType = await select({
|
|
108
|
+
message: 'Choose your setup mode:',
|
|
109
|
+
options: [
|
|
110
|
+
{ value: 'quick', label: '⚡ Quickstart (Recommended)', hint: 'Production defaults, minimal prompts' },
|
|
111
|
+
{ value: 'advanced', label: '🛠️ Advanced Configuration', hint: 'Customize health checks, task count, branch, etc.' },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
if (typeof setupType === 'symbol') process.exit(0);
|
|
115
|
+
|
|
116
|
+
let defaultPort = '3000';
|
|
117
|
+
if (finalFramework === 'static' || finalFramework === 'go') defaultPort = '8080';
|
|
118
|
+
if (finalFramework === 'python' || finalFramework === 'django') defaultPort = '8000';
|
|
119
|
+
|
|
120
|
+
let currentGitBranch = 'main';
|
|
121
|
+
try {
|
|
122
|
+
currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
|
|
123
|
+
} catch (e) { }
|
|
124
|
+
|
|
125
|
+
let needsDatabase = false;
|
|
126
|
+
let dbEngine = 'postgres';
|
|
127
|
+
const isBackendFramework = ['node', 'nestjs', 'nextjs', 'nuxt', 'svelte', 'python', 'django', 'rails', 'go'].includes(finalFramework);
|
|
128
|
+
|
|
129
|
+
if (isBackendFramework) {
|
|
130
|
+
const dbEvidence = capabilities?.relationalDb?.detected === true
|
|
131
|
+
? capabilities.relationalDb.evidence || []
|
|
132
|
+
: [];
|
|
133
|
+
const mysqlHint = capabilities?.upcomingHints?.mysql === true;
|
|
134
|
+
const dbChoice = await confirm({
|
|
135
|
+
message: dbEvidence.length > 0
|
|
136
|
+
? `Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier) (detected: ${dbEvidence.join(', ')})`
|
|
137
|
+
: 'Do you need a managed AWS RDS PostgreSQL database? (Adds ~$14/month or uses AWS Free Tier)',
|
|
138
|
+
initialValue: dbEvidence.length > 0 || mysqlHint,
|
|
139
|
+
});
|
|
140
|
+
if (typeof dbChoice === 'symbol') process.exit(0);
|
|
141
|
+
needsDatabase = dbChoice;
|
|
142
|
+
if (needsDatabase) {
|
|
143
|
+
if (explicitDbEngine) {
|
|
144
|
+
dbEngine = explicitDbEngine;
|
|
145
|
+
} else {
|
|
146
|
+
const engineChoice = await select({
|
|
147
|
+
message: 'Which database engine should we provision?',
|
|
148
|
+
options: [
|
|
149
|
+
{ value: 'postgres', label: 'PostgreSQL 16 (RDS db.t4g.micro)', hint: '~$13.98/mo fixed' },
|
|
150
|
+
{ value: 'aurora-postgresql', label: 'Aurora PostgreSQL Serverless v2 (0–2 ACU)', hint: '$0/mo idle compute + storage' },
|
|
151
|
+
{ value: 'mysql', label: 'MySQL 8.0 (RDS db.t4g.micro)', hint: '~$13.98/mo fixed' },
|
|
152
|
+
],
|
|
153
|
+
initialValue: mysqlHint ? 'mysql' : 'postgres',
|
|
154
|
+
});
|
|
155
|
+
if (typeof engineChoice === 'symbol') process.exit(0);
|
|
156
|
+
dbEngine = engineChoice;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let enablePrPreviews = false;
|
|
162
|
+
let aiAssistants = [];
|
|
163
|
+
if (setupType === 'advanced') {
|
|
164
|
+
const prChoice = await confirm({
|
|
165
|
+
message: `Enable Ephemeral PR Previews? (Spins up isolated, temporary AWS environments for PRs)\n ${color.gray('📖 Learn more: https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md')}`,
|
|
166
|
+
initialValue: false,
|
|
167
|
+
});
|
|
168
|
+
if (typeof prChoice === 'symbol') process.exit(0);
|
|
169
|
+
enablePrPreviews = prChoice;
|
|
170
|
+
|
|
171
|
+
aiAssistants = await getAiAssistants();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const project = await group({
|
|
175
|
+
region: () => select({
|
|
176
|
+
message: 'Which AWS region do you want to deploy to?',
|
|
177
|
+
options: [
|
|
178
|
+
{ value: 'us-east-1', label: 'us-east-1 (N. Virginia)' },
|
|
179
|
+
{ value: 'us-east-2', label: 'us-east-2 (Ohio)' },
|
|
180
|
+
{ value: 'eu-west-1', label: 'eu-west-1 (Ireland)' },
|
|
181
|
+
{ value: 'eu-central-1', label: 'EU (Frankfurt)' },
|
|
182
|
+
{ value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
|
|
183
|
+
],
|
|
184
|
+
}),
|
|
185
|
+
port: () => text({
|
|
186
|
+
message: 'What port does your container expose?',
|
|
187
|
+
placeholder: defaultPort,
|
|
188
|
+
defaultValue: defaultPort,
|
|
189
|
+
}),
|
|
190
|
+
size: () => isLambda ? undefined : select({
|
|
191
|
+
message: 'Select your Fargate compute size:',
|
|
192
|
+
options: [
|
|
193
|
+
{ value: 'micro', label: 'Micro (0.25 vCPU, 512MB RAM) - Best for POCs' },
|
|
194
|
+
{ value: 'small', label: 'Small (0.5 vCPU, 1GB RAM) - Best for small Projects' },
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
197
|
+
healthCheckPath: () => (setupType === 'quick' || isLambda) ? undefined : text({
|
|
198
|
+
message: 'ALB Health Check Path:',
|
|
199
|
+
placeholder: '/',
|
|
200
|
+
defaultValue: '/',
|
|
201
|
+
}),
|
|
202
|
+
desiredCount: () => (setupType === 'quick' || isLambda) ? undefined : select({
|
|
203
|
+
message: 'How many container replicas (tasks) should run?',
|
|
204
|
+
options: [
|
|
205
|
+
{ value: '1', label: '1 Task (Single instance - lowest cost)' },
|
|
206
|
+
{ value: '2', label: '2 Tasks (High Availability across AZs)' },
|
|
207
|
+
],
|
|
208
|
+
defaultValue: '1',
|
|
209
|
+
}),
|
|
210
|
+
branch: () => setupType === 'quick' ? undefined : text({
|
|
211
|
+
message: 'Primary Git deployment branch for CI/CD:',
|
|
212
|
+
placeholder: currentGitBranch,
|
|
213
|
+
defaultValue: currentGitBranch,
|
|
214
|
+
}),
|
|
215
|
+
}, { onCancel: () => process.exit(0) });
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
framework: finalFramework,
|
|
219
|
+
region: project.region,
|
|
220
|
+
port: project.port,
|
|
221
|
+
size: project.size || 'micro',
|
|
222
|
+
healthCheckPath: project.healthCheckPath || '/',
|
|
223
|
+
desiredCount: project.desiredCount || '1',
|
|
224
|
+
branch: project.branch || currentGitBranch,
|
|
225
|
+
needsDatabase,
|
|
226
|
+
dbEngine,
|
|
227
|
+
enablePrPreviews,
|
|
228
|
+
aiAssistants,
|
|
229
|
+
setupType
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Prompts for the background worker command. Pre-fills from the Procfile
|
|
234
|
+
// worker process when present, otherwise from capability detection
|
|
235
|
+
// (`npm run <script>`). Returns the (possibly edited) command string.
|
|
236
|
+
export async function promptWorkerCommand(procfile, capabilities = null) {
|
|
237
|
+
const procfileCommand = Array.isArray(procfile?.worker) ? procfile.worker.join(' ') : '';
|
|
238
|
+
const detectedCommand = typeof capabilities?.worker?.suggestedCommand === 'string'
|
|
239
|
+
? capabilities.worker.suggestedCommand
|
|
240
|
+
: '';
|
|
241
|
+
const answer = await text({
|
|
242
|
+
message: procfileCommand
|
|
243
|
+
? 'What command runs your background worker? (Empty keeps the Procfile default)'
|
|
244
|
+
: 'What command runs your background worker? (Empty skips the worker service)',
|
|
245
|
+
placeholder: 'celery -A config worker',
|
|
246
|
+
initialValue: procfileCommand || detectedCommand,
|
|
247
|
+
});
|
|
248
|
+
if (typeof answer === 'symbol') {
|
|
249
|
+
cancel('Operation cancelled.');
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
return typeof answer === 'string' ? answer : '';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getAiAssistants() {
|
|
256
|
+
const selected = await multiselect({
|
|
257
|
+
message: 'Which AI coding assistants does your team use?',
|
|
258
|
+
options: [
|
|
259
|
+
{ value: 'cursor', label: 'Cursor', hint: 'Generates .cursor/rules/grada.mdc' },
|
|
260
|
+
{ value: 'roo', label: 'Roo Code', hint: 'Generates .roo/rules/grada.md' },
|
|
261
|
+
{ value: 'trae', label: 'Trae IDE', hint: 'Generates .trae/rules/project_rules.md' },
|
|
262
|
+
{ value: 'windsurf', label: 'Windsurf', hint: 'Generates .windsurfrules' },
|
|
263
|
+
{ value: 'copilot', label: 'GitHub Copilot', hint: 'Generates .github/copilot-instructions.md' },
|
|
264
|
+
{ value: 'claude', label: 'Claude Code', hint: 'Updates CLAUDE.md' },
|
|
265
|
+
{ value: 'goose', label: 'Goose', hint: 'Updates .goosehints' },
|
|
266
|
+
{ value: 'aider', label: 'Aider', hint: 'Updates .aider.conf.yml' },
|
|
267
|
+
{ value: 'continue', label: 'Continue.dev', hint: 'Generates .prompts/grada.prompt' }
|
|
268
|
+
],
|
|
269
|
+
required: false,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (typeof selected === 'symbol') {
|
|
273
|
+
cancel('Operation cancelled.');
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return selected;
|
|
278
|
+
}
|
package/src/utils/rds.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { DescribeDBInstancesCommand, DescribeDBClustersCommand } from '@aws-sdk/client-rds';
|
|
2
|
+
import { resolveProjectName, resolveWorkspaceSuffix } from './resolvers.js';
|
|
3
|
+
import { normalizeOptions } from './args.js';
|
|
4
|
+
|
|
5
|
+
// Error names the RDS API returns for a missing instance across SDK shapes.
|
|
6
|
+
const NOT_FOUND_NAMES = new Set(['DBInstanceNotFound', 'DBInstanceNotFoundFault']);
|
|
7
|
+
|
|
8
|
+
// Same convention for Aurora clusters.
|
|
9
|
+
const CLUSTER_NOT_FOUND_NAMES = new Set(['DBClusterNotFound', 'DBClusterNotFoundFault']);
|
|
10
|
+
|
|
11
|
+
// Shared RDS instance lookup for every `db` subcommand: exact identifier
|
|
12
|
+
// match, `null` when the project has no database, unexpected errors rethrown.
|
|
13
|
+
export async function findDbInstance(rdsClient, dbIdentifier) {
|
|
14
|
+
try {
|
|
15
|
+
const resp = await rdsClient.send(
|
|
16
|
+
new DescribeDBInstancesCommand({ DBInstanceIdentifier: dbIdentifier })
|
|
17
|
+
);
|
|
18
|
+
return (resp.DBInstances || [])[0] || null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error && NOT_FOUND_NAMES.has(error.name)) return null;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Shared Aurora cluster lookup: exact identifier match, `null` when no
|
|
26
|
+
// cluster exists under it, unexpected errors rethrown.
|
|
27
|
+
export async function findDbCluster(rdsClient, dbClusterIdentifier) {
|
|
28
|
+
try {
|
|
29
|
+
const resp = await rdsClient.send(
|
|
30
|
+
new DescribeDBClustersCommand({ DBClusterIdentifier: dbClusterIdentifier })
|
|
31
|
+
);
|
|
32
|
+
return (resp.DBClusters || [])[0] || null;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error && CLUSTER_NOT_FOUND_NAMES.has(error.name)) return null;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isMysqlEngine(engine) {
|
|
40
|
+
return engine === 'mysql' || engine === 'aurora-mysql';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unified RDS discovery across single instances and Aurora clusters.
|
|
44
|
+
// Tries the instance identifier first (zero extra API calls for the common
|
|
45
|
+
// postgres/mysql case), then the cluster identifiers. Returns a normalized
|
|
46
|
+
// descriptor `{ kind, id, engine, status, endpoint, port, dbName,
|
|
47
|
+
// masterSecretArn, raw }` or `null` when nothing is provisioned.
|
|
48
|
+
export async function findDbTarget(rdsClient, { dbIdentifier, dbClusterIdentifier } = {}) {
|
|
49
|
+
if (dbIdentifier) {
|
|
50
|
+
const instance = await findDbInstance(rdsClient, dbIdentifier);
|
|
51
|
+
if (instance) {
|
|
52
|
+
const port = instance.Endpoint?.Port ?? (isMysqlEngine(instance.Engine) ? 3306 : 5432);
|
|
53
|
+
return {
|
|
54
|
+
kind: 'instance',
|
|
55
|
+
id: dbIdentifier,
|
|
56
|
+
engine: instance.Engine,
|
|
57
|
+
status: instance.Status,
|
|
58
|
+
endpoint: instance.Endpoint?.Address,
|
|
59
|
+
port: String(port),
|
|
60
|
+
dbName: instance.DBName,
|
|
61
|
+
masterSecretArn: instance.MasterUserSecret?.SecretArn,
|
|
62
|
+
raw: instance,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const clusterIds = [dbClusterIdentifier, dbIdentifier].filter(
|
|
67
|
+
(id, index, list) => typeof id === 'string' && id !== '' && list.indexOf(id) === index
|
|
68
|
+
);
|
|
69
|
+
for (const clusterId of clusterIds) {
|
|
70
|
+
const cluster = await findDbCluster(rdsClient, clusterId);
|
|
71
|
+
if (cluster) {
|
|
72
|
+
const port = cluster.Port ?? (isMysqlEngine(cluster.Engine) ? 3306 : 5432);
|
|
73
|
+
return {
|
|
74
|
+
kind: 'cluster',
|
|
75
|
+
id: clusterId,
|
|
76
|
+
engine: cluster.Engine,
|
|
77
|
+
status: cluster.Status,
|
|
78
|
+
endpoint: cluster.Endpoint,
|
|
79
|
+
port: String(port),
|
|
80
|
+
dbName: cluster.DatabaseName,
|
|
81
|
+
masterSecretArn: cluster.MasterUserSecret?.SecretArn,
|
|
82
|
+
raw: cluster,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pad2(value) {
|
|
90
|
+
return String(value).padStart(2, '0');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Default manual snapshot id: `<db>-manual-YYYYMMDD-HHmmss` in UTC.
|
|
94
|
+
// Lowercased so project names with uppercase stay RDS-safe.
|
|
95
|
+
export function generateSnapshotId(dbIdentifier, now = new Date()) {
|
|
96
|
+
const stamp = `${now.getUTCFullYear()}${pad2(now.getUTCMonth() + 1)}${pad2(now.getUTCDate())}`
|
|
97
|
+
+ `-${pad2(now.getUTCHours())}${pad2(now.getUTCMinutes())}${pad2(now.getUTCSeconds())}`;
|
|
98
|
+
return `${dbIdentifier}-manual-${stamp}`.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// RDS snapshot identifier rules: starts with a letter, letters/digits/hyphens
|
|
102
|
+
// after that, no consecutive hyphens, no trailing hyphen, max 255 chars.
|
|
103
|
+
export function isValidSnapshotId(id) {
|
|
104
|
+
if (typeof id !== 'string') return false;
|
|
105
|
+
if (!/^[a-zA-Z][a-zA-Z0-9-]{0,254}$/.test(id)) return false;
|
|
106
|
+
if (id.includes('--') || id.endsWith('-')) return false;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Database identifier for the current project/workspace. An explicit
|
|
111
|
+
// `--db-identifier` override wins; otherwise `<appName>-db`.
|
|
112
|
+
export function resolveDbIdentifier(options = {}, cwd = process.cwd()) {
|
|
113
|
+
const opts = normalizeOptions(options);
|
|
114
|
+
if (typeof opts.dbIdentifier === 'string' && opts.dbIdentifier.trim()) {
|
|
115
|
+
return opts.dbIdentifier.trim();
|
|
116
|
+
}
|
|
117
|
+
const base = resolveProjectName(opts, cwd);
|
|
118
|
+
return `${base}${resolveWorkspaceSuffix(opts, cwd)}-db`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Aurora cluster identifier for the current project/workspace. An explicit
|
|
122
|
+
// `--db-identifier` override wins (tried as both kinds); otherwise
|
|
123
|
+
// `<appName>-db-cluster`, matching the generated cluster_identifier.
|
|
124
|
+
export function resolveDbClusterIdentifier(options = {}, cwd = process.cwd()) {
|
|
125
|
+
const opts = normalizeOptions(options);
|
|
126
|
+
if (typeof opts.dbIdentifier === 'string' && opts.dbIdentifier.trim()) {
|
|
127
|
+
return opts.dbIdentifier.trim();
|
|
128
|
+
}
|
|
129
|
+
const base = resolveProjectName(opts, cwd);
|
|
130
|
+
return `${base}${resolveWorkspaceSuffix(opts, cwd)}-db-cluster`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { normalizeOptions } from './args.js';
|
|
4
|
+
|
|
5
|
+
export const FALLBACK_REGION = 'us-east-2';
|
|
6
|
+
|
|
7
|
+
export function readFileSafe(filePath) {
|
|
8
|
+
if (typeof filePath !== 'string') return null;
|
|
9
|
+
try {
|
|
10
|
+
if (fsSync.existsSync(filePath)) return fsSync.readFileSync(filePath, 'utf8');
|
|
11
|
+
} catch {
|
|
12
|
+
// Fall through to defaults
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Compute targets supported by `--target`. `ecs` covers both the
|
|
18
|
+
// canonical `ecs` value and its `fargate` synonym.
|
|
19
|
+
export const COMPUTE_TARGETS = ['ecs', 'lambda'];
|
|
20
|
+
|
|
21
|
+
// Pure target check over rendered `main.tf` content: a Lambda project is
|
|
22
|
+
// any project whose main.tf provisions the serverless function.
|
|
23
|
+
export function detectComputeTargetFromMainTf(mainTfContent) {
|
|
24
|
+
if (typeof mainTfContent === 'string' && mainTfContent.includes('resource "aws_lambda_function"')) {
|
|
25
|
+
return 'lambda';
|
|
26
|
+
}
|
|
27
|
+
return 'ecs';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Reads the compute target of the project rooted at `cwd`. Missing or
|
|
31
|
+
// unreadable Terraform falls back to `ecs` so legacy projects and
|
|
32
|
+
// pre-init guards keep their historical behavior.
|
|
33
|
+
export function readTerraformComputeTarget(cwd = process.cwd()) {
|
|
34
|
+
const base = resolveCwd({}, cwd);
|
|
35
|
+
return detectComputeTargetFromMainTf(readFileSafe(path.join(base, 'terraform', 'main.tf')));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Working directory for file resolution: an explicit string `cwd` option,
|
|
39
|
+
// otherwise the positional `fallback` when it is a usable path, otherwise
|
|
40
|
+
// the process cwd. A throwing `process.cwd()` (deleted directory)
|
|
41
|
+
// propagates so callers route through PROJECT_NOT_INITIALIZED.
|
|
42
|
+
export function resolveCwd(options = {}, fallback) {
|
|
43
|
+
const opts = normalizeOptions(options);
|
|
44
|
+
if (typeof opts.cwd === 'string' && opts.cwd) return opts.cwd;
|
|
45
|
+
if (typeof fallback === 'string' && fallback) return fallback;
|
|
46
|
+
return process.cwd();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readTerraformRegion(cwd = process.cwd()) {
|
|
50
|
+
const base = resolveCwd({}, cwd);
|
|
51
|
+
const mainTf = readFileSafe(path.join(base, 'terraform', 'main.tf'));
|
|
52
|
+
if (!mainTf) return null;
|
|
53
|
+
const match = mainTf.match(/region\s*=\s*"([^"]+)"/);
|
|
54
|
+
if (!match || match[1].includes('{{')) return null;
|
|
55
|
+
return match[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveRegion(options = {}, cwd = process.cwd()) {
|
|
59
|
+
const opts = normalizeOptions(options);
|
|
60
|
+
if (typeof opts.region === 'string' && opts.region.trim()) {
|
|
61
|
+
return opts.region.trim();
|
|
62
|
+
}
|
|
63
|
+
if (typeof process.env.AWS_REGION === 'string' && process.env.AWS_REGION.trim()) {
|
|
64
|
+
return process.env.AWS_REGION.trim();
|
|
65
|
+
}
|
|
66
|
+
return readTerraformRegion(resolveCwd(opts, cwd)) || FALLBACK_REGION;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readTerraformProjectName(cwd = process.cwd()) {
|
|
70
|
+
const base = resolveCwd({}, cwd);
|
|
71
|
+
const mainTf = readFileSafe(path.join(base, 'terraform', 'main.tf'));
|
|
72
|
+
if (!mainTf) return null;
|
|
73
|
+
const appNameMatch = mainTf.match(/app_name\s*=\s*"([^"$]+)\$\{local\.env_suffix\}"/);
|
|
74
|
+
if (appNameMatch) return appNameMatch[1];
|
|
75
|
+
// Hand-written files may set a plain app_name (possibly with some other
|
|
76
|
+
// ${...} suffix); it still outranks the ECR heuristic below.
|
|
77
|
+
const genericMatch = mainTf.match(/app_name\s*=\s*"([^"]+)"/);
|
|
78
|
+
if (genericMatch && !genericMatch[1].includes('{{')) {
|
|
79
|
+
const stripped = genericMatch[1].replace(/\$\{.*$/, '').replace(/[-_]$/, '');
|
|
80
|
+
if (stripped) return stripped;
|
|
81
|
+
}
|
|
82
|
+
const ecrMatch = mainTf.match(/resource\s+"aws_ecr_repository"\s+"app"\s*\{[^}]*?name\s*=\s*"([^"]+)-repo"/);
|
|
83
|
+
if (ecrMatch) return ecrMatch[1];
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveProjectName(options = {}, cwd = process.cwd()) {
|
|
88
|
+
const opts = normalizeOptions(options);
|
|
89
|
+
const base = resolveCwd(opts, cwd);
|
|
90
|
+
if (typeof opts.projectName === 'string' && opts.projectName.trim()) {
|
|
91
|
+
return opts.projectName.trim();
|
|
92
|
+
}
|
|
93
|
+
return readTerraformProjectName(base) || path.basename(path.resolve(base));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveCluster(options = {}, cwd = process.cwd()) {
|
|
97
|
+
const opts = normalizeOptions(options);
|
|
98
|
+
if (typeof opts.cluster === 'string' && opts.cluster.trim()) return opts.cluster.trim();
|
|
99
|
+
if (typeof process.env.ECS_CLUSTER === 'string' && process.env.ECS_CLUSTER.trim()) {
|
|
100
|
+
return process.env.ECS_CLUSTER.trim();
|
|
101
|
+
}
|
|
102
|
+
return `${resolveProjectName(opts, cwd)}-cluster`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveService(options = {}, cwd = process.cwd()) {
|
|
106
|
+
const opts = normalizeOptions(options);
|
|
107
|
+
if (typeof opts.service === 'string' && opts.service.trim()) return opts.service.trim();
|
|
108
|
+
if (typeof opts.serviceName === 'string' && opts.serviceName.trim()) return opts.serviceName.trim();
|
|
109
|
+
if (typeof process.env.ECS_SERVICE === 'string' && process.env.ECS_SERVICE.trim()) {
|
|
110
|
+
return process.env.ECS_SERVICE.trim();
|
|
111
|
+
}
|
|
112
|
+
return `${resolveProjectName(opts, cwd)}-service`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveLogGroup(options = {}, cwd = process.cwd()) {
|
|
116
|
+
const opts = normalizeOptions(options);
|
|
117
|
+
if (typeof opts.logGroup === 'string' && opts.logGroup.trim()) {
|
|
118
|
+
return opts.logGroup.trim();
|
|
119
|
+
}
|
|
120
|
+
if (typeof opts.logGroupName === 'string' && opts.logGroupName.trim()) {
|
|
121
|
+
return opts.logGroupName.trim();
|
|
122
|
+
}
|
|
123
|
+
if (typeof process.env.ECS_LOG_GROUP === 'string' && process.env.ECS_LOG_GROUP.trim()) {
|
|
124
|
+
return process.env.ECS_LOG_GROUP.trim();
|
|
125
|
+
}
|
|
126
|
+
const base = resolveCwd(opts, cwd);
|
|
127
|
+
const projectName = resolveProjectName(opts, base);
|
|
128
|
+
// Lambda functions log to /aws/lambda/<fn> (see main-lambda.tf), where
|
|
129
|
+
// the function name is `<app>-fn`.
|
|
130
|
+
if (readTerraformComputeTarget(base) === 'lambda') {
|
|
131
|
+
return `/aws/lambda/${projectName}-fn`;
|
|
132
|
+
}
|
|
133
|
+
return `/ecs/${projectName}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// True when prompts must not block: explicit headless flags, CI/test
|
|
137
|
+
// environments, or non-TTY stdio. An explicit `isHeadless: false` (or
|
|
138
|
+
// `headless: false`) forces interactive mode so unit tests can simulate
|
|
139
|
+
// TTY prompts. `env`/`stdin`/`stdout` are injectable for tests.
|
|
140
|
+
export function resolveHeadless(options = {}, env = process.env, stdin = process.stdin, stdout = process.stdout) {
|
|
141
|
+
const opts = normalizeOptions(options);
|
|
142
|
+
if (opts.isHeadless === false || opts.headless === false) return false;
|
|
143
|
+
return Boolean(
|
|
144
|
+
opts.isHeadless ||
|
|
145
|
+
opts.headless ||
|
|
146
|
+
(env && (env.CI || env.VITEST)) ||
|
|
147
|
+
(env && env.NODE_ENV === 'test') ||
|
|
148
|
+
!stdin?.isTTY ||
|
|
149
|
+
!stdout?.isTTY
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Workspace-namespaced application name (`myapp` or `myapp-pr-123`) shared
|
|
154
|
+
// by every `db` subcommand. `workspace` is the explicit `--workspace` value
|
|
155
|
+
// (or undefined to auto-detect `.terraform/environment` under `cwd`).
|
|
156
|
+
export function resolveAppName(projectName, workspace, cwd = process.cwd()) {
|
|
157
|
+
return `${projectName}${resolveWorkspaceSuffix({ workspace, cwd }, cwd)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reads the local Terraform workspace (e.g. a PR-preview environment).
|
|
161
|
+
// Returns '' for the default workspace so names stay un-suffixed.
|
|
162
|
+
export function resolveWorkspaceSuffix(options = {}, cwd = process.cwd()) {
|
|
163
|
+
const opts = normalizeOptions(options);
|
|
164
|
+
const base = resolveCwd(opts, cwd);
|
|
165
|
+
let workspace = null;
|
|
166
|
+
if (typeof opts.workspace === 'string' && opts.workspace.trim()) {
|
|
167
|
+
workspace = opts.workspace.trim();
|
|
168
|
+
} else {
|
|
169
|
+
const detected = readFileSafe(path.join(base, '.terraform', 'environment'));
|
|
170
|
+
if (typeof detected === 'string' && detected.trim()) workspace = detected.trim();
|
|
171
|
+
}
|
|
172
|
+
if (!workspace || workspace === 'default') return '';
|
|
173
|
+
return `-${workspace}`;
|
|
174
|
+
}
|