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,126 @@
|
|
|
1
|
+
// Single source of truth for `grada add` capabilities.
|
|
2
|
+
// Imported by both src/commands/add.js and src/utils/visualizer.js.
|
|
3
|
+
// Keep this module dependency-free so neither importer creates a cycle.
|
|
4
|
+
export const ADDON_REGISTRY = {
|
|
5
|
+
'storage:s3': {
|
|
6
|
+
file: 's3.tf',
|
|
7
|
+
template: 's3.tf',
|
|
8
|
+
label: 'S3 + CloudFront OAC',
|
|
9
|
+
cost: {
|
|
10
|
+
model: 'usage-based',
|
|
11
|
+
monthlyFixed: 0,
|
|
12
|
+
summary: '$0/mo fixed baseline; billed per GB stored ($0.023/GB-mo), S3 PUT/GET requests, and CloudFront egress',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
'db:dynamodb': {
|
|
16
|
+
file: 'dynamodb.tf',
|
|
17
|
+
template: 'dynamodb.tf',
|
|
18
|
+
label: 'DynamoDB (On-Demand + PITR)',
|
|
19
|
+
cost: {
|
|
20
|
+
model: 'usage-based',
|
|
21
|
+
monthlyFixed: 0,
|
|
22
|
+
summary: '$0/mo fixed instance baseline (VPC Gateway Endpoint is free); billed per read/write request, table storage ($0.25/GB-mo), and PITR continuous backups ($0.20/GB-mo once data is written)',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
'db:redis': {
|
|
26
|
+
file: 'redis.tf',
|
|
27
|
+
template: 'redis.tf',
|
|
28
|
+
label: 'ElastiCache Valkey 8.0',
|
|
29
|
+
cost: {
|
|
30
|
+
model: 'fixed-baseline',
|
|
31
|
+
monthlyFixed: 9.49,
|
|
32
|
+
summary: '~$9.49/mo fixed baseline ($0.013/hr Valkey 8.0 cache.t4g.micro in us-east-2); $0 intra-AZ VPC transfer',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
'queue:sqs': {
|
|
36
|
+
file: 'sqs.tf',
|
|
37
|
+
template: 'sqs.tf',
|
|
38
|
+
label: 'SQS + DLQ',
|
|
39
|
+
cost: {
|
|
40
|
+
model: 'usage-based',
|
|
41
|
+
monthlyFixed: 0,
|
|
42
|
+
summary: '$0/mo fixed baseline; first 1M requests/mo free, then $0.40 per million requests',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
'ai:bedrock': {
|
|
46
|
+
file: 'bedrock.tf',
|
|
47
|
+
template: 'bedrock.tf',
|
|
48
|
+
label: 'Bedrock Runtime IAM',
|
|
49
|
+
cost: {
|
|
50
|
+
model: 'usage-based',
|
|
51
|
+
monthlyFixed: 0,
|
|
52
|
+
summary: '$0/mo fixed baseline; billed per 1K input/output tokens on InvokeModel calls',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
'email:ses': {
|
|
56
|
+
file: 'ses.tf',
|
|
57
|
+
template: 'ses.tf',
|
|
58
|
+
label: 'Amazon SES (Transactional Email & DKIM)',
|
|
59
|
+
cost: {
|
|
60
|
+
model: 'usage-based',
|
|
61
|
+
monthlyFixed: 0,
|
|
62
|
+
summary: '$0/mo fixed baseline; $0.10 per 1,000 emails sent',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
'cron': {
|
|
66
|
+
file: 'cron.tf',
|
|
67
|
+
template: 'cron.tf',
|
|
68
|
+
label: 'EventBridge Scheduler (Cron)',
|
|
69
|
+
cost: {
|
|
70
|
+
model: 'usage-based',
|
|
71
|
+
monthlyFixed: 0,
|
|
72
|
+
summary: '$0/mo fixed baseline (first 14M EventBridge Scheduler invocations/mo free); billed only for Fargate seconds while the cron task runs',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Container environment variables injected into the ECS task definitions
|
|
78
|
+
// per capability. Values are static HCL expressions or `(ctx) => string`
|
|
79
|
+
// resolvers for dynamic values (model IDs, SES sender/region). Lone
|
|
80
|
+
// "${...}" values render as bare HCL references via renderEnvValue in
|
|
81
|
+
// src/commands/add.js; literals and multi-part interpolations stay quoted.
|
|
82
|
+
export const ADDON_ENV_VARS = {
|
|
83
|
+
'storage:s3': [
|
|
84
|
+
{ name: 'S3_BUCKET_NAME', value: '${aws_s3_bucket.storage.id}' },
|
|
85
|
+
{ name: 'S3_CDN_URL', value: 'https://${aws_cloudfront_distribution.storage_cdn.domain_name}' },
|
|
86
|
+
],
|
|
87
|
+
'db:dynamodb': [
|
|
88
|
+
{ name: 'DYNAMODB_TABLE_NAME', value: '${aws_dynamodb_table.main.name}' },
|
|
89
|
+
],
|
|
90
|
+
'db:redis': [
|
|
91
|
+
{ name: 'REDIS_URL', value: 'redis://${aws_elasticache_replication_group.redis.primary_endpoint_address}:${aws_elasticache_replication_group.redis.port}' },
|
|
92
|
+
],
|
|
93
|
+
'queue:sqs': [
|
|
94
|
+
{ name: 'SQS_QUEUE_URL', value: '${aws_sqs_queue.main.id}' },
|
|
95
|
+
{ name: 'SQS_DLQ_URL', value: '${aws_sqs_queue.dlq.id}' },
|
|
96
|
+
],
|
|
97
|
+
'ai:bedrock': [
|
|
98
|
+
{ name: 'BEDROCK_MODEL_ID', value: (ctx) => ctx.model },
|
|
99
|
+
],
|
|
100
|
+
'email:ses': [
|
|
101
|
+
{ name: 'SES_FROM_EMAIL', value: (ctx) => ctx.sesFromEmail },
|
|
102
|
+
{ name: 'SES_REGION', value: (ctx) => ctx.region },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Keys whose values are replaced in place on reruns (Day-2 switching)
|
|
107
|
+
// instead of the default skip-if-present behavior.
|
|
108
|
+
export const ADDON_UPSERT_KEYS = {
|
|
109
|
+
'ai:bedrock': ['BEDROCK_MODEL_ID'],
|
|
110
|
+
'db:redis': ['REDIS_URL'],
|
|
111
|
+
'email:ses': ['SES_FROM_EMAIL', 'SES_REGION'],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Resolves `{ name, value }` entries for `capability`, evaluating
|
|
115
|
+
// function values against `ctx` (`{ region, model, sesFromEmail }`).
|
|
116
|
+
// Entries resolving to empty values are dropped.
|
|
117
|
+
export function resolveAddonEnvVars(capability, ctx = {}) {
|
|
118
|
+
const entries = ADDON_ENV_VARS[capability] || [];
|
|
119
|
+
const resolved = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const value = typeof entry.value === 'function' ? entry.value(ctx) : entry.value;
|
|
122
|
+
if (typeof value !== 'string' || value === '') continue;
|
|
123
|
+
resolved.push({ name: entry.name, value });
|
|
124
|
+
}
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
|
|
3
|
+
export const getBaseRules = (context = {}) => `
|
|
4
|
+
# Grada Infrastructure Context
|
|
5
|
+
|
|
6
|
+
This project's AWS infrastructure is managed strictly by the \`grada\` CLI.
|
|
7
|
+
**DO NOT** generate custom Terraform, AWS CloudFormation, or raw AWS CLI commands to deploy this application or modify the architecture.
|
|
8
|
+
|
|
9
|
+
### Standard Operating Procedures
|
|
10
|
+
|
|
11
|
+
1. **Applying Changes:** If the user asks to deploy, provision, or update the infrastructure, instruct them to run:
|
|
12
|
+
\`npx grada-run apply\`
|
|
13
|
+
|
|
14
|
+
2. **Environment Variables:** If the user adds a new environment variable and needs it available in production, instruct them to run:
|
|
15
|
+
\`npx grada-run secrets push <path-to-env-file>\`
|
|
16
|
+
|
|
17
|
+
3. **Teardown:** If the user wants to destroy the AWS environment, instruct them to run:
|
|
18
|
+
\`npx grada-run destroy\`
|
|
19
|
+
|
|
20
|
+
4. **Architecture Details:**
|
|
21
|
+
- **AWS Region:** \`${context.region || 'Unknown (Check terraform/main.tf)'}\`
|
|
22
|
+
- **Container Port:** \`${context.port || 'Unknown'}\`
|
|
23
|
+
- The infrastructure is an AWS ECS Fargate cluster.
|
|
24
|
+
- It uses an Application Load Balancer (ALB).
|
|
25
|
+
- CI/CD is handled securely via GitHub Actions OIDC (no long-lived IAM keys).
|
|
26
|
+
- Preview environments (Ephemeral PRs) are managed via Terraform Workspaces.
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export const getCursorRules = (context = {}) => `---
|
|
30
|
+
description: "Rules for deploying the application and managing AWS infrastructure"
|
|
31
|
+
globs: ["terraform/*.tf", ".github/workflows/*.yml", "Dockerfile"]
|
|
32
|
+
---${getBaseRules(context)}`;
|
|
33
|
+
|
|
34
|
+
export function injectManagedBlock(filePath, content, isMarkdown = true) {
|
|
35
|
+
const beginMarker = isMarkdown ? '<!-- BEGIN GRADA CONTEXT -->' : '# BEGIN GRADA CONTEXT';
|
|
36
|
+
const endMarker = isMarkdown ? '<!-- END GRADA CONTEXT -->' : '# END GRADA CONTEXT';
|
|
37
|
+
const legacyBeginMarker = isMarkdown ? '<!-- BEGIN DEPLOY-STACK CONTEXT -->' : '# BEGIN DEPLOY-STACK CONTEXT';
|
|
38
|
+
const legacyEndMarker = isMarkdown ? '<!-- END DEPLOY-STACK CONTEXT -->' : '# END DEPLOY-STACK CONTEXT';
|
|
39
|
+
const block = `\n${beginMarker}\n${content.trim()}\n${endMarker}\n`;
|
|
40
|
+
|
|
41
|
+
if (fsSync.existsSync(filePath)) {
|
|
42
|
+
let fileContent = fsSync.readFileSync(filePath, 'utf8');
|
|
43
|
+
// Replace an existing block in either brand variant so re-runs
|
|
44
|
+
// refresh legacy files instead of appending a duplicate block.
|
|
45
|
+
// (Markers carry no regex metacharacters, so direct interpolation
|
|
46
|
+
// is safe.)
|
|
47
|
+
const regex = new RegExp(`\\n?(?:${beginMarker}|${legacyBeginMarker})[\\s\\S]*?(?:${endMarker}|${legacyEndMarker})\\n?`);
|
|
48
|
+
|
|
49
|
+
if (regex.test(fileContent)) {
|
|
50
|
+
fileContent = fileContent.replace(regex, block); // Replace our old rules
|
|
51
|
+
} else {
|
|
52
|
+
fileContent = fileContent.trim() + '\n' + block; // Append to bottom
|
|
53
|
+
}
|
|
54
|
+
fsSync.writeFileSync(filePath, fileContent);
|
|
55
|
+
} else {
|
|
56
|
+
// File doesn't exist, create it cleanly
|
|
57
|
+
fsSync.writeFileSync(filePath, block.trim() + '\n');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function toKey(name) {
|
|
2
|
+
return name
|
|
3
|
+
.split('-')
|
|
4
|
+
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
5
|
+
.join('');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Accepts flag entries as bare names (`'project-name'` → `projectName`) or
|
|
9
|
+
// explicit `{ name, key }` pairs for keys that diverge (e.g. `--headless`).
|
|
10
|
+
function normalizeEntries(entries) {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
for (const entry of entries || []) {
|
|
13
|
+
if (typeof entry === 'string') {
|
|
14
|
+
map.set(entry, toKey(entry));
|
|
15
|
+
} else if (entry && typeof entry.name === 'string') {
|
|
16
|
+
map.set(entry.name, entry.key || toKey(entry.name));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Guards module entry points against non-object options (direct programmatic
|
|
23
|
+
// misuse or automated fuzzing): anything that is not an object behaves like
|
|
24
|
+
// `{}`, so callers always reach structured validation instead of TypeErrors.
|
|
25
|
+
export function normalizeOptions(options) {
|
|
26
|
+
return options !== null && typeof options === 'object' ? options : {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Guards argument parsers against non-array inputs: anything that is not an
|
|
30
|
+
// array behaves like `[]`.
|
|
31
|
+
export function normalizeArgv(argv) {
|
|
32
|
+
return Array.isArray(argv) ? [...argv] : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Declarative CLI flag parsing shared by every command module.
|
|
36
|
+
//
|
|
37
|
+
// - `string`: `--name value` or `--name=value` (last one wins; a trailing
|
|
38
|
+
// `--name` with no value is dropped, matching the hand-written parsers).
|
|
39
|
+
// - `number`: like `string` but coerced with `Number()`.
|
|
40
|
+
// - `boolean`: bare `--name` means `true`; `--name=<v>` is `v === 'true'`.
|
|
41
|
+
// - `bareBoolean`: bare `--name` only; any `=value` form is ignored.
|
|
42
|
+
// - `alias`: single-dash shorthands (`{ f: 'follow' }` makes `-f` act as
|
|
43
|
+
// the bare long flag).
|
|
44
|
+
//
|
|
45
|
+
// Unknown `--flags` are dropped and everything else is returned in `rest`
|
|
46
|
+
// so each command can apply its own positional rules.
|
|
47
|
+
export function parseFlags(args, { string = [], number = [], boolean = [], bareBoolean = [], alias = {} } = {}) {
|
|
48
|
+
const options = {};
|
|
49
|
+
const rest = [];
|
|
50
|
+
const strings = normalizeEntries(string);
|
|
51
|
+
const numbers = normalizeEntries(number);
|
|
52
|
+
const booleans = normalizeEntries(boolean);
|
|
53
|
+
const bares = normalizeEntries(bareBoolean);
|
|
54
|
+
const list = Array.isArray(args) ? args : [];
|
|
55
|
+
for (let i = 0; i < list.length; i++) {
|
|
56
|
+
let arg = list[i];
|
|
57
|
+
if (typeof arg !== 'string') {
|
|
58
|
+
rest.push(arg);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg.length === 2 && arg[0] === '-' && Object.prototype.hasOwnProperty.call(alias, arg[1])) {
|
|
62
|
+
arg = `--${alias[arg[1]]}`;
|
|
63
|
+
}
|
|
64
|
+
if (!arg.startsWith('--')) {
|
|
65
|
+
rest.push(arg);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const eq = arg.indexOf('=');
|
|
69
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
70
|
+
const value = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
71
|
+
if (strings.has(name)) {
|
|
72
|
+
if (value !== undefined) options[strings.get(name)] = value;
|
|
73
|
+
else if (i + 1 < list.length) options[strings.get(name)] = list[++i];
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (numbers.has(name)) {
|
|
77
|
+
if (value !== undefined) options[numbers.get(name)] = Number(value);
|
|
78
|
+
else if (i + 1 < list.length) options[numbers.get(name)] = Number(list[++i]);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (booleans.has(name)) {
|
|
82
|
+
options[booleans.get(name)] = value === undefined ? true : value === 'true';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (bares.has(name)) {
|
|
86
|
+
if (value === undefined) options[bares.get(name)] = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { options, rest };
|
|
91
|
+
}
|
package/src/utils/aws.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
2
|
+
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand, PutBucketTaggingCommand } from '@aws-sdk/client-s3';
|
|
3
|
+
import { DeleteBucketCommand, ListObjectVersionsCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
|
|
7
|
+
export const AWS_CLI_INSTALL_URL = 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html';
|
|
8
|
+
export const AWS_CLI_CACHE_TTL_MS = 5000;
|
|
9
|
+
|
|
10
|
+
const TROUBLESHOOTING_URL = 'https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/guides/aws-credentials.md';
|
|
11
|
+
|
|
12
|
+
let cachedAwsCliResult = null;
|
|
13
|
+
|
|
14
|
+
export function resetAwsCliCache() {
|
|
15
|
+
cachedAwsCliResult = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function checkAwsCli(runSync) {
|
|
19
|
+
try {
|
|
20
|
+
const result = runSync('aws', ['--version'], { stdio: 'ignore' });
|
|
21
|
+
if (result && typeof result.status === 'number') return result.status === 0;
|
|
22
|
+
if (result && result.error) return false;
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasAwsCli(options = {}) {
|
|
30
|
+
// Custom implementations (injected by tests) always execute directly so
|
|
31
|
+
// they never read or pollute the shared cache.
|
|
32
|
+
if (options.spawnSyncImpl !== undefined) {
|
|
33
|
+
return checkAwsCli(options.spawnSyncImpl);
|
|
34
|
+
}
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (cachedAwsCliResult && cachedAwsCliResult.expiresAt > now) {
|
|
37
|
+
return cachedAwsCliResult.value;
|
|
38
|
+
}
|
|
39
|
+
const value = checkAwsCli(spawnSync);
|
|
40
|
+
cachedAwsCliResult = { value, expiresAt: now + AWS_CLI_CACHE_TTL_MS };
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// AWS SDK errors that mean "credentials expired or invalid" rather than a
|
|
45
|
+
// command-specific failure. Single definition shared by every command's
|
|
46
|
+
// catch block.
|
|
47
|
+
export function isAuthError(error) {
|
|
48
|
+
return !!error && (error.name === 'UnrecognizedClientException' || error.name === 'ExpiredTokenException');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The shared auth-failure branch for command catch blocks: prints the
|
|
52
|
+
// credential-recovery hint (and exits). Returns true when the error was an
|
|
53
|
+
// auth failure that has been fully handled — the caller should `return` its
|
|
54
|
+
// own auth result immediately. Call only after tracking failure telemetry,
|
|
55
|
+
// matching the order every catch block uses today.
|
|
56
|
+
export function handleAuthErrorBranch(error, clackSpinner = null, options = {}) {
|
|
57
|
+
if (!isAuthError(error)) return false;
|
|
58
|
+
handleAwsAuthError(error, clackSpinner, options);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Returns an injected mock client when it quacks like an SDK client,
|
|
63
|
+
// otherwise constructs the real one. Single definition for the
|
|
64
|
+
// options-injection pattern every AWS command uses for testability.
|
|
65
|
+
export function resolveClient(injected, ClientClass, clientOptions = {}) {
|
|
66
|
+
if (injected && typeof injected.send === 'function') return injected;
|
|
67
|
+
return new ClientClass(clientOptions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function handleAwsAuthError(error, clackSpinner = null, options = {}) {
|
|
71
|
+
if (clackSpinner) {
|
|
72
|
+
clackSpinner.stop(color.red('❌ AWS session expired or invalid credentials.'));
|
|
73
|
+
}
|
|
74
|
+
if (!hasAwsCli(options)) {
|
|
75
|
+
console.log(color.red('\n✖ AWS CLI not found.'));
|
|
76
|
+
console.log(` Install the AWS CLI: ${color.blue(color.underline(AWS_CLI_INSTALL_URL))}`);
|
|
77
|
+
console.log(` Then run ${color.cyan('aws sso login')} (or ${color.cyan('aws configure')}) and try again.`);
|
|
78
|
+
} else {
|
|
79
|
+
console.log(color.yellow('\n⚠️ AWS Session Expired / Invalid Credentials'));
|
|
80
|
+
console.log(`Run ${color.cyan('aws sso login')} or ${color.cyan('aws configure')} to refresh your credentials.`);
|
|
81
|
+
}
|
|
82
|
+
console.log(color.blue(`\n📘 Troubleshooting Guide: ${color.underline(TROUBLESHOOTING_URL)}\n`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function checkAwsCredentials(region) {
|
|
88
|
+
const resolvedRegion = region || process.env.AWS_REGION || 'us-east-2';
|
|
89
|
+
if (process.env.CI_MOCK_AWS === 'true') {
|
|
90
|
+
return { accountId: '123456789012', awsAccountId: '123456789012', region: resolvedRegion };
|
|
91
|
+
}
|
|
92
|
+
const stsClient = new STSClient({ region: resolvedRegion });
|
|
93
|
+
const { Account } = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
94
|
+
return { accountId: Account, awsAccountId: Account, region: resolvedRegion };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function provisionStateBucket(region, projectName) {
|
|
98
|
+
if (process.env.CI_MOCK_AWS === 'true') {
|
|
99
|
+
return { awsAccountId: '123456789012', stateBucketName: 'mock-tf-state-bucket' };
|
|
100
|
+
}
|
|
101
|
+
const stsClient = new STSClient({ region });
|
|
102
|
+
let awsAccountId;
|
|
103
|
+
|
|
104
|
+
// 1. Get AWS Account ID
|
|
105
|
+
const { Account } = await stsClient.send(new GetCallerIdentityCommand({}));
|
|
106
|
+
awsAccountId = Account;
|
|
107
|
+
|
|
108
|
+
// 2. Format Bucket Name
|
|
109
|
+
let stateBucketName = `${projectName}-tfstate-${awsAccountId}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
110
|
+
if (stateBucketName.length > 63) {
|
|
111
|
+
stateBucketName = stateBucketName.substring(0, 63).replace(/-$/, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Create S3 Bucket & Enable Versioning
|
|
115
|
+
const s3Client = new S3Client({ region });
|
|
116
|
+
try {
|
|
117
|
+
await s3Client.send(new CreateBucketCommand({
|
|
118
|
+
Bucket: stateBucketName,
|
|
119
|
+
CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
await s3Client.send(new PutBucketTaggingCommand({
|
|
123
|
+
Bucket: stateBucketName,
|
|
124
|
+
Tagging: {
|
|
125
|
+
TagSet: [
|
|
126
|
+
{ Key: "ManagedBy", Value: "grada" }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
await s3Client.send(new PutBucketVersioningCommand({
|
|
132
|
+
Bucket: stateBucketName,
|
|
133
|
+
VersioningConfiguration: { Status: 'Enabled' }
|
|
134
|
+
}));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Ignore if the bucket already exists and is owned by the user
|
|
137
|
+
if (error.name !== 'BucketAlreadyOwnedByYou') {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { awsAccountId, stateBucketName };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function teardownStateBucket(region, bucketName) {
|
|
146
|
+
if (process.env.CI_MOCK_AWS === 'true') {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const client = new S3Client({ region });
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// 1. Fetch all object versions and delete markers
|
|
153
|
+
const listCommand = new ListObjectVersionsCommand({ Bucket: bucketName });
|
|
154
|
+
const { Versions, DeleteMarkers } = await client.send(listCommand);
|
|
155
|
+
|
|
156
|
+
const objectsToDelete = [];
|
|
157
|
+
if (Versions) objectsToDelete.push(...Versions.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
158
|
+
if (DeleteMarkers) objectsToDelete.push(...DeleteMarkers.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
159
|
+
|
|
160
|
+
// 2. Delete all contents if any exist
|
|
161
|
+
if (objectsToDelete.length > 0) {
|
|
162
|
+
const deleteCommand = new DeleteObjectsCommand({
|
|
163
|
+
Bucket: bucketName,
|
|
164
|
+
Delete: { Objects: objectsToDelete }
|
|
165
|
+
});
|
|
166
|
+
await client.send(deleteCommand);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 3. Delete the now-empty bucket
|
|
170
|
+
const deleteBucketCommand = new DeleteBucketCommand({ Bucket: bucketName });
|
|
171
|
+
await client.send(deleteBucketCommand);
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.name === 'NoSuchBucket') return true; // Already deleted
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { select, isCancel, cancel } from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
|
|
6
|
+
export async function handleExistingFiles(targetDir, isHeadless = false) {
|
|
7
|
+
const tfPath = path.join(targetDir, 'terraform');
|
|
8
|
+
const dockerfilePath = path.join(targetDir, 'Dockerfile');
|
|
9
|
+
const wfDir = path.join(targetDir, '.github', 'workflows');
|
|
10
|
+
const wfPath = path.join(wfDir, 'deploy.yml');
|
|
11
|
+
|
|
12
|
+
const tfExists = fs.existsSync(tfPath);
|
|
13
|
+
const dockerfileExists = fs.existsSync(dockerfilePath);
|
|
14
|
+
const wfExists = fs.existsSync(wfPath);
|
|
15
|
+
|
|
16
|
+
if (!tfExists && !dockerfileExists && !wfExists) {
|
|
17
|
+
return; // Clean slate
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const foundFiles = [];
|
|
21
|
+
if (tfExists) foundFiles.push('terraform/');
|
|
22
|
+
if (dockerfileExists) foundFiles.push('Dockerfile');
|
|
23
|
+
if (wfExists) foundFiles.push('.github/workflows/deploy.yml');
|
|
24
|
+
|
|
25
|
+
let overwriteDecision = 'backup';
|
|
26
|
+
|
|
27
|
+
if (!isHeadless) {
|
|
28
|
+
overwriteDecision = await select({
|
|
29
|
+
message: color.yellow(`⚠️ Conflicting files found (${foundFiles.join(', ')}). To guarantee a secure, 0-CVE deployment, we must use our optimized configurations.`),
|
|
30
|
+
options: [
|
|
31
|
+
{ value: 'backup', label: 'Backup & Regenerate', hint: 'Move old configs to .bak and generate secure templates' },
|
|
32
|
+
{ value: 'cancel', label: 'Cancel', hint: 'Exit without making changes' }
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (isCancel(overwriteDecision) || overwriteDecision === 'cancel') {
|
|
37
|
+
cancel('Operation cancelled to protect existing files.');
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Execute the safe backup
|
|
43
|
+
const timestamp = Date.now();
|
|
44
|
+
|
|
45
|
+
if (tfExists) fs.renameSync(tfPath, `${tfPath}.bak.${timestamp}`);
|
|
46
|
+
if (dockerfileExists) fs.renameSync(dockerfilePath, `${dockerfilePath}.bak.${timestamp}`);
|
|
47
|
+
if (wfExists) fs.renameSync(wfPath, `${wfPath}.bak.${timestamp}`);
|
|
48
|
+
|
|
49
|
+
ensureGitIgnore(targetDir);
|
|
50
|
+
|
|
51
|
+
console.log(color.cyan(`ℹ️ Existing configuration files were safely backed up locally.`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Appends backup patterns to .gitignore so local clutter never reaches GitHub.
|
|
56
|
+
*/
|
|
57
|
+
function ensureGitIgnore(targetDir) {
|
|
58
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
59
|
+
const ignoreRules = '\n# grada backups\n*.bak.*\n';
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(gitignorePath)) {
|
|
62
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
63
|
+
if (!content.includes('*.bak.*')) {
|
|
64
|
+
fs.appendFileSync(gitignorePath, ignoreRules);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
fs.writeFileSync(gitignorePath, ignoreRules);
|
|
68
|
+
}
|
|
69
|
+
}
|