@grada-run/grada 0.0.1 → 0.33.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/e2e.yml +73 -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 +85 -0
- package/apps/docs/src/content/docs/cli/apply.md +35 -0
- package/apps/docs/src/content/docs/cli/db.md +200 -0
- package/apps/docs/src/content/docs/cli/destroy.md +35 -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 +33 -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 +87 -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 +99 -0
- package/apps/docs/src/content/docs/testing-strategy.md +37 -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 +107 -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/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/e2e-testing.md +50 -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/specs/test-suite-deduplication.md +42 -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 +130 -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 +128 -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 +78 -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 +2018 -0
- package/tests/ai.test.js +91 -0
- package/tests/apply.test.js +446 -0
- package/tests/args.test.js +86 -0
- package/tests/aws.test.js +244 -0
- package/tests/capabilities.test.js +304 -0
- package/tests/cli.test.js +29 -0
- package/tests/command.test.js +99 -0
- package/tests/commands-import.test.js +74 -0
- package/tests/db.test.js +2673 -0
- package/tests/destroy.test.js +365 -0
- package/tests/detector.test.js +79 -0
- package/tests/diagnose.test.js +765 -0
- package/tests/doctor.test.js +195 -0
- package/tests/domain.test.js +882 -0
- package/tests/drift.test.js +228 -0
- package/tests/e2e/helpers.js +122 -0
- package/tests/e2e/tier0.e2e.test.js +142 -0
- package/tests/e2e/tier1.live.e2e.test.js +118 -0
- package/tests/ecs.test.js +130 -0
- package/tests/eject.test.js +65 -0
- package/tests/exec.test.js +366 -0
- package/tests/gc.test.js +466 -0
- package/tests/generator.test.js +794 -0
- package/tests/headless.test.js +491 -0
- package/tests/helpers/clack.js +103 -0
- package/tests/helpers/console.js +41 -0
- package/tests/helpers/telemetry.js +37 -0
- package/tests/helpers/tmpdir.js +36 -0
- package/tests/lambda-ecr.test.js +185 -0
- package/tests/logs.test.js +436 -0
- package/tests/parser.test.js +160 -0
- package/tests/rds.test.js +244 -0
- package/tests/resolvers.test.js +279 -0
- package/tests/rollback.test.js +670 -0
- package/tests/secrets.test.js +729 -0
- package/tests/sleep-wake.test.js +998 -0
- package/tests/status.test.js +356 -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 +480 -0
- package/vitest.config.js +10 -0
- package/vitest.e2e.tier0.config.js +8 -0
- package/vitest.e2e.tier1.config.js +8 -0
- package/index.js +0 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Spec: Database Suite Completion (`db enable-vector`, `db import`, and Multi-Engine RDS / Aurora Scale-to-Zero)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Complete the `deploy-stack db` lifecycle suite and relational database engine layer by delivering the final three database roadmap capabilities in Phase 10:
|
|
5
|
+
1. **Multi-Engine RDS & Aurora Serverless v2 Scale-to-Zero:** Support PostgreSQL (`postgres`), MySQL (`mysql`), and Aurora PostgreSQL Serverless v2 (`aurora-postgresql` with `min_capacity = 0` auto-pause) across `init`, `visualizer`, `db connect`, `db migrate`, `db backup`, and `db restore`.
|
|
6
|
+
2. **Vector Databases (`deploy-stack db enable-vector`):** One-command `pgvector` activation on PostgreSQL (`postgres` and `aurora-postgresql`) inside the isolated VPC without requiring `psql` inside the user's application container image.
|
|
7
|
+
3. **Zero-Trust Database Ingestion (`deploy-stack db import`):** Stream a local SQL/dump file (`--file <path>`) or a remote database (`--from <url>` from Heroku, Supabase, Render, Railway, or Neon) into the private VPC database over an ephemeral SSM Port Forwarding tunnel.
|
|
8
|
+
|
|
9
|
+
### Core Architectural Principles
|
|
10
|
+
1. **100% Backwards Compatibility for Default PostgreSQL:** Calling `init --headless --needsDatabase` without `--db-engine` must continue to generate the exact same `postgres` (`aws_db_instance.postgres`) configuration and keep existing snapshot contracts intact.
|
|
11
|
+
2. **Container-Runtime Agnostic VPC Execution:** `db enable-vector` must not assume the user's application container has the `psql` CLI installed. Execute the SQL lifecycle over a tiny zero-dependency Node.js/Python inline driver or raw PostgreSQL wire-protocol/TCP handshake inside the ephemeral ECS task, or fall back to an inline Node script using the runtime environment variables (`DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`).
|
|
12
|
+
3. **Unified RDS & Cluster Discovery:** Abstract single-instance RDS (`aws_db_instance`) and Aurora Cluster (`aws_rds_cluster` + `aws_rds_cluster_instance`) behind `src/utils/rds.js` so `db connect`, `db backup`, `db restore`, `db migrate`, `db enable-vector`, and `db import` work seamlessly across all three engines.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Part 1: Multi-Engine RDS & Aurora Scale-to-Zero
|
|
17
|
+
|
|
18
|
+
### 1. Supported Database Engines (`--db-engine <engine>`)
|
|
19
|
+
Introduce three canonical engine identifiers across `src/core/parser.js`, `src/commands/init.js`, `src/utils/prompts.js`, and `src/utils/generator.js`:
|
|
20
|
+
* `postgres` (default): Standard RDS PostgreSQL 16 (`aws_db_instance.postgres`, `db.t4g.micro`, port `5432`, URI scheme `postgresql://`).
|
|
21
|
+
* `mysql`: Standard RDS MySQL 8.0 (`aws_db_instance.postgres` or canonical resource name `aws_db_instance.main` / `postgres` kept for HCL/reference continuity or cleanly parameterized, `db.t4g.micro`, port `3306`, default username `dbadmin`, URI scheme `mysql://`).
|
|
22
|
+
* `aurora-postgresql`: Amazon Aurora PostgreSQL Serverless v2 (`aws_rds_cluster.postgres` + `aws_rds_cluster_instance.postgres`, engine `aurora-postgresql`, no `engine_version` on the cluster so new databases take the regional AWS default (AWS retires pinned minors such as `16.4`), port `5432`, `serverlessv2_scaling_configuration { min_capacity = 0.0, max_capacity = 2.0 }`, `enable_http_endpoint = false`, `manage_master_user_password = true`, `storage_encrypted = true`).
|
|
23
|
+
|
|
24
|
+
### 2. Interactive & Headless `init` Integration
|
|
25
|
+
* **CLI Flag:** Parse `--db-engine <postgres|mysql|aurora-postgresql>` in `src/core/parser.js`. Fail fast with `INVALID_DB_ENGINE` if an unsupported value is passed.
|
|
26
|
+
* **Capability Detection Alignment:** In `src/utils/capabilities.js`, when `relationalDb.detected` is true, use the detected `relationalDb.engine` (`postgres` vs `mysql`) as the default initial value in the interactive engine selector prompt.
|
|
27
|
+
* **Interactive Prompt (`src/utils/prompts.js`):** When the user answers `Yes` to provisioning a managed database in interactive `init` (and `--db-engine` was not explicitly passed):
|
|
28
|
+
* Present a `select` prompt to choose the database engine:
|
|
29
|
+
1. `PostgreSQL 16 (RDS db.t4g.micro — ~$13.98/mo fixed)`
|
|
30
|
+
2. `Aurora PostgreSQL Serverless v2 (Scale-to-Zero 0–2 ACU — $0/mo idle compute + storage)`
|
|
31
|
+
3. `MySQL 8.0 (RDS db.t4g.micro — ~$13.98/mo fixed)`
|
|
32
|
+
* **Headless Default:** When `--headless` or `--preconfigured` is used with `--needsDatabase` and no `--db-engine` flag is passed, default to `postgres` so all existing tests and snapshots remain unchanged.
|
|
33
|
+
|
|
34
|
+
### 3. Terraform Templates & Container Environment Wiring
|
|
35
|
+
* **Template Strategy:**
|
|
36
|
+
* Keep `templates/terraform/database.tf` as the single target output path (`terraform/database.tf`), rendered according to the chosen `dbEngine` (`postgres`, `mysql`, or `aurora-postgresql`).
|
|
37
|
+
* Ensure security group ingress rules on `aws_security_group.rds` open port `5432` for `postgres` / `aurora-postgresql` and port `3306` for `mysql`, restricted strictly to `aws_security_group.ecs_tasks.id`.
|
|
38
|
+
* For `aurora-postgresql`:
|
|
39
|
+
* Provision `aws_rds_cluster.postgres` with `cluster_identifier = "${local.app_name}-db-cluster"`, `engine = "aurora-postgresql"`, `database_name = replace(local.app_name, "-", "_")`, `master_username = "dbadmin"`, `manage_master_user_password = true`, `db_subnet_group_name = aws_db_subnet_group.main.name`, `vpc_security_group_ids = [aws_security_group.rds.id]`, `skip_final_snapshot = true`, `storage_encrypted = true`, and `serverlessv2_scaling_configuration { min_capacity = 0, max_capacity = 2 }`.
|
|
40
|
+
* Provision `aws_rds_cluster_instance.postgres` with `identifier = "${local.app_name}-db-instance-1"`, `cluster_identifier = aws_rds_cluster.postgres.id`, `instance_class = "db.serverless"`, `engine = aws_rds_cluster.postgres.engine`, `engine_version = aws_rds_cluster.postgres.engine_version`, `publicly_accessible = false`.
|
|
41
|
+
* Wire `DB_HOST` to `aws_rds_cluster.postgres.endpoint`, `DB_NAME` to `aws_rds_cluster.postgres.database_name`, `DB_PORT` to `"5432"`, and `DB_USER` / `DB_PASSWORD` secrets to `aws_rds_cluster.postgres.master_user_secret[0].secret_arn`.
|
|
42
|
+
* Also inject `{ "name": "DB_ENGINE", "value": "<engine>" }` (or detect the port/scheme in `migrate.js`) when `mysql` or `aurora-postgresql` is selected so runtime tools know whether to synthesize `mysql://` or `postgresql://`.
|
|
43
|
+
* Ensure all generated HCL across all three engine modes is `terraform validate`-clean and `tflint`-clean (using bare HCL references rather than deprecated single-expression interpolations).
|
|
44
|
+
|
|
45
|
+
### 4. Visualizer & Cost Estimation (`src/utils/visualizer.js`)
|
|
46
|
+
* Inspect `terraform/database.tf` to detect the provisioned engine (`aws_rds_cluster` -> Aurora Serverless v2; `engine = "mysql"` -> RDS MySQL; default -> RDS PostgreSQL).
|
|
47
|
+
* Render accurate topology labels and cost math:
|
|
48
|
+
* `postgres`: `🐘 Amazon RDS (PostgreSQL managed instance)` — `$13.98/mo` fixed baseline.
|
|
49
|
+
* `mysql`: `🐬 Amazon RDS (MySQL managed instance)` — `$13.98/mo` fixed baseline.
|
|
50
|
+
* `aurora-postgresql`: `✨ Amazon Aurora PostgreSQL (Serverless v2 · 0–2 ACU scale-to-zero)` — `$0/mo` idle compute baseline (`+$0.12/ACU-hr when active`).
|
|
51
|
+
|
|
52
|
+
### 5. Cross-Command Multi-Engine Support (`src/utils/rds.js`, `connect.js`, `migrate.js`, `backup.js`, `restore.js`)
|
|
53
|
+
* **Unified `findDbTarget(rdsClient, identifier, cwd)` in `src/utils/rds.js`:**
|
|
54
|
+
* Detect whether the project uses a standard RDS instance (`aws_db_instance`) or an Aurora cluster (`aws_rds_cluster`) by inspecting `terraform/database.tf` (if present) or querying `DescribeDBInstancesCommand` (`${appName}-db`) with fallback to `DescribeDBClustersCommand` (`${appName}-db-cluster`).
|
|
55
|
+
* Return a normalized descriptor: `{ kind: 'instance' | 'cluster', id, engine, status, endpoint, port, dbName, masterSecretArn, raw }`.
|
|
56
|
+
* **`db connect`:**
|
|
57
|
+
* Use the normalized `endpoint` and `port` (`5432` or `3306`).
|
|
58
|
+
* Default `--local-port` to the remote database port (`5432` for Postgres/Aurora, `3306` for MySQL) unless overridden by the user.
|
|
59
|
+
* Format the printed connection URI using `mysql://` when `engine === 'mysql'` and `postgresql://` otherwise.
|
|
60
|
+
* **`db migrate`:**
|
|
61
|
+
* Update `buildRuntimeMigrationCommand` in `src/commands/db/migrate.js` so that when synthesizing `DATABASE_URL` at runtime, it uses `mysql://` if `DB_PORT === '3306'` or `DB_ENGINE === 'mysql'`, and `postgresql://` otherwise.
|
|
62
|
+
* **`db backup` & `db restore`:**
|
|
63
|
+
* When `kind === 'cluster'` (Aurora), use `CreateDBClusterSnapshotCommand` and `DescribeDBClusterSnapshotsCommand` in `backup.js` / `restore.js`, and pin `snapshot_identifier` inside the `resource "aws_rds_cluster" "postgres"` block in `terraform/database.tf`.
|
|
64
|
+
* Maintain existing `CreateDBSnapshotCommand` / `DescribeDBSnapshotsCommand` behavior for `kind === 'instance'`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Part 2: Vector Databases (`deploy-stack db enable-vector`)
|
|
69
|
+
|
|
70
|
+
### 1. Command Interface & Routing
|
|
71
|
+
* Route `deploy-stack db enable-vector` through `src/commands/db.js` to a new module `src/commands/db/enable-vector.js` (`runDbEnableVector`).
|
|
72
|
+
* Supported flags: `--cluster <name>`, `--service <name>`, `--container <name>`, `--task-def <arn-or-family>`, `--project-name <name>`, `--workspace <name>`, `--region <region>`, `--headless` / `--yes`.
|
|
73
|
+
* Update `VALID_DB_SUBCOMMANDS` and `HELP_TEXT` in `src/commands/db.js` and `bin/cli.js`.
|
|
74
|
+
|
|
75
|
+
### 2. Engine & Prerequisite Guards
|
|
76
|
+
* Inspect `terraform/database.tf` (if present in `cwd`):
|
|
77
|
+
* If `terraform/database.tf` exists and its engine is `mysql`, fail fast with error code `UNSUPPORTED_VECTOR_ENGINE` explaining that `pgvector` requires PostgreSQL or Aurora PostgreSQL.
|
|
78
|
+
* If `terraform/database.tf` does not exist and no `--cluster`/`--service` overrides are passed, fail fast with `NO_DATABASE_CONFIGURED` and guide the user to enable PostgreSQL first.
|
|
79
|
+
|
|
80
|
+
### 3. Zero-Dependency Ephemeral Execution Inside the VPC
|
|
81
|
+
* Reuse the ephemeral Fargate task runner machinery from `src/commands/db/migrate.js` (extract shared task-launch + CloudWatch log-streaming + SIGINT cancellation into a reusable helper in `src/utils/ecs-runner.js` or `src/commands/db/migrate.js` so `migrate` and `enable-vector` do not duplicate the 150-line `RunTask` / `pollUntil` / `getLogEvents` lifecycle).
|
|
82
|
+
* **How the SQL is executed inside the container:**
|
|
83
|
+
* Because user containers may be Node, Python, Go, or Ruby (and rarely have the `psql` CLI binary installed), construct a compact, self-contained shell command that tries:
|
|
84
|
+
1. `psql "$DATABASE_URL" -c "CREATE EXTENSION IF NOT EXISTS vector;"` if `psql` is on `PATH`.
|
|
85
|
+
2. Otherwise, if `node` is on `PATH` and `pg` or `@prisma/client` is installed in `node_modules`, execute a one-liner via `pg` (`Client`) or `@prisma/client` (`$executeRawUnsafe('CREATE EXTENSION IF NOT EXISTS vector;')`) and verify via `SELECT extversion FROM pg_extension WHERE extname = 'vector';`.
|
|
86
|
+
3. Otherwise, if `python3` / `python` is on `PATH` with `psycopg` / `psycopg2` / `asyncpg` / `django`, execute the equivalent 5-line Python snippet.
|
|
87
|
+
4. If none of those drivers exist in the container image, exit with a clear diagnostic code so the CLI prints an actionable hint (e.g., install `pg` / `@prisma/client` / `psycopg` or `postgresql-client` in the image).
|
|
88
|
+
* **Local Project Marker & Prisma Hint:**
|
|
89
|
+
* After the remote task succeeds (exit code `0`), if `prisma/schema.prisma` exists in `cwd` and does not yet mention `postgresqlExtensions`, print a helpful note showing how to enable `previewFeatures = ["postgresqlExtensions"]` and `extensions = [vector]` in `schema.prisma`.
|
|
90
|
+
* Emit `trackSuccess('db_enable_vector', { duration_ms, engine })` on completion.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Part 3: Zero-Trust Database Ingestion (`deploy-stack db import`)
|
|
95
|
+
|
|
96
|
+
### 1. Command Interface & Flags
|
|
97
|
+
* Route `deploy-stack db import` through `src/commands/db.js` to a new module `src/commands/db/import.js` (`runDbImport`).
|
|
98
|
+
* Supported flags:
|
|
99
|
+
* `--file <path>`: Path to a local `.sql`, `.dump`, or `.sql.gz` file to import into the remote database.
|
|
100
|
+
* `--from <source-url>`: Source `postgresql://...` or `mysql://...` connection URI (e.g., from Heroku, Supabase, Render, Railway, or Neon) to dump and stream directly into the remote AWS database.
|
|
101
|
+
* `--yes` / `--force`: Skip the interactive confirmation prompt before importing into the remote database.
|
|
102
|
+
* Standard target overrides: `--db-identifier <id>`, `--project-name <name>`, `--workspace <name>`, `--region <region>`, `--headless`.
|
|
103
|
+
|
|
104
|
+
### 2. Input Validation & Pre-Flight Checks
|
|
105
|
+
* Require **exactly one** of `--file <path>` or `--from <source-url>` (or prompt interactively when in a TTY if neither was provided). If both or neither are provided in headless mode, fail fast with `INVALID_IMPORT_SOURCE`.
|
|
106
|
+
* If `--file <path>` is used, verify the file exists and is readable on disk before making any AWS API calls (`IMPORT_FILE_NOT_FOUND`).
|
|
107
|
+
* If `--from <source-url>` is used, validate that the URI starts with `postgres://`, `postgresql://`, or `mysql://` (`INVALID_SOURCE_URI`), and never log or emit the raw password in terminal output or telemetry (`redactUri(url)`).
|
|
108
|
+
* Check local client tooling prerequisites before opening the SSM tunnel:
|
|
109
|
+
* For PostgreSQL targets: verify `psql` (or `pg_restore` for custom-format `.dump` files, plus `pg_dump` when `--from` is used) is available on the host `PATH`.
|
|
110
|
+
* For MySQL targets: verify `mysql` (plus `mysqldump` when `--from` is used) is available on the host `PATH`.
|
|
111
|
+
* If missing, fail fast with `MISSING_DB_CLIENT_BINARY` and print exact `brew install libpq` / `brew install mysql-client` instructions.
|
|
112
|
+
|
|
113
|
+
### 3. Ephemeral Background SSM Tunnel + Password Retrieval
|
|
114
|
+
* Extract the SSM port-forwarding tunnel setup from `src/commands/db/connect.js` into a reusable helper (e.g., `withDatabaseTunnel(options, async ({ localPort, host, port, dbName, username, password, engine }) => { ... })`) so `connect.js` and `import.js` share the exact same bastion/ECS task selection, RDS discovery (`findDbTarget`), and SSM session lifecycle:
|
|
115
|
+
* Automatically pick an ephemeral free local port (via `net.createServer().listen(0)` or a configurable port) so `db import` never collides with a local Postgres/MySQL instance running on `5432` / `3306`.
|
|
116
|
+
* Fetch the RDS master user password at runtime from AWS Secrets Manager (`GetSecretValueCommand` against `masterSecretArn` from `findDbTarget`) in memory only—never write the password to disk or command-line arguments visible in `ps` (pass via `PGPASSWORD` or `MYSQL_PWD` environment variable to the child process).
|
|
117
|
+
* Wait until the local SSM port-forwarding socket accepts TCP connections (polling `127.0.0.1:<localPort>` with `pollUntil`), then execute the import stream:
|
|
118
|
+
* **`--file <path>.sql`:** Pipe the file stream into `psql` / `mysql` connected to `127.0.0.1:<localPort>`.
|
|
119
|
+
* **`--file <path>.sql.gz`:** Pipe through `zlib.createGunzip()` into `psql` / `mysql`.
|
|
120
|
+
* **`--file <path>.dump` (Postgres custom archive):** Run `pg_restore --no-owner --no-acl -h 127.0.0.1 -p <localPort> -U <username> -d <dbName> <path>`.
|
|
121
|
+
* **`--from <source-url>`:** Spawn `pg_dump --no-owner --no-acl <source-url>` (or `mysqldump`) and pipe its `stdout` directly into `psql` (or `mysql`) connected to `127.0.0.1:<localPort>`, streaming stderr progress to the terminal.
|
|
122
|
+
* In a `finally` block, always terminate the background SSM session child process cleanly even if the import fails or the user presses `Ctrl+C`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Part 4: Documentation, Roadmap & Testing Requirements
|
|
127
|
+
|
|
128
|
+
### 1. Documentation & Roadmap Updates
|
|
129
|
+
* Update `apps/docs/src/content/docs/cli/db.md` to document:
|
|
130
|
+
* `deploy-stack db enable-vector`
|
|
131
|
+
* `deploy-stack db import --file <path>` and `deploy-stack db import --from <url>`
|
|
132
|
+
* Multi-engine support (`postgres`, `mysql`, `aurora-postgresql` scale-to-zero) across `db connect`, `db migrate`, `db backup`, and `db restore`.
|
|
133
|
+
* Update `apps/docs/src/content/docs/cli/init.md` and `apps/docs/src/content/docs/architecture/database-connections.md` with the `--db-engine` flag and Aurora Serverless v2 scale-to-zero (`0 ACU`) cost breakdown.
|
|
134
|
+
* Check off the 3 completed items in `apps/docs/src/content/docs/roadmap.md`:
|
|
135
|
+
* `[x] Vector Databases`
|
|
136
|
+
* `[x] Multi-Engine RDS & Aurora Scale-to-Zero`
|
|
137
|
+
* `[x] Zero-Trust Database Ingestion`
|
|
138
|
+
|
|
139
|
+
### 2. Test Suite & IaC Validation Requirements
|
|
140
|
+
* **Existing Snapshots Preserved:** Default `init --headless --needsDatabase` (when `--db-engine` is omitted) must keep existing snapshots untouched.
|
|
141
|
+
* **New Unit & Integration Tests:**
|
|
142
|
+
* `tests/rds-multi-engine.test.js` (or in `tests/db.test.js` & `tests/headless.test.js`):
|
|
143
|
+
* Verify `init --headless --needsDatabase --db-engine mysql` generates valid MySQL 8.0 HCL (port `3306`, `mysql` engine) and injects `DB_PORT = "3306"`.
|
|
144
|
+
* Verify `init --headless --needsDatabase --db-engine aurora-postgresql` generates `aws_rds_cluster.postgres` + `aws_rds_cluster_instance.postgres` with `min_capacity = 0` and `max_capacity = 2`.
|
|
145
|
+
* Verify `visualizer.js` calculates `$0/mo` idle compute baseline for `aurora-postgresql` and `$13.98/mo` for `postgres` / `mysql`.
|
|
146
|
+
* Verify `findDbTarget` resolves both `aws_db_instance` and `aws_rds_cluster`, and that `db backup` / `db restore` use `CreateDBClusterSnapshotCommand` / `DescribeDBClusterSnapshotsCommand` and update `resource "aws_rds_cluster" "postgres"` when running against an Aurora cluster.
|
|
147
|
+
* `tests/db.test.js` additions for `enable-vector` and `import`:
|
|
148
|
+
* Verify `db enable-vector` rejects `mysql` projects with `UNSUPPORTED_VECTOR_ENGINE`, launches the ephemeral task for `postgres` / `aurora-postgresql`, and prints the Prisma vector extension hint when `prisma/schema.prisma` is present.
|
|
149
|
+
* Verify `db import` validates `--file` / `--from` exclusivity, redacts source URIs on error, checks for required local client binaries, passes `PGPASSWORD` / `MYSQL_PWD` via `env` (never CLI args), and always kills the SSM tunnel child process in `finally`.
|
|
150
|
+
* **`scripts/test-iac.js` & `.github/workflows/iac-validation.yml`:**
|
|
151
|
+
* Extend `scripts/test-iac.js` to also validate `aurora-postgresql` and `mysql` scaffolded configurations with `terraform validate` and `tflint` so all three database engines are continuously verified.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Phase 9: Secure Database Tunneling & Doc Alignment
|
|
2
|
+
|
|
3
|
+
## Part 1: Fix Existing Logic & Documentation Gaps
|
|
4
|
+
Address the three misalignments identified between the generated code and the documentation.
|
|
5
|
+
|
|
6
|
+
**1. SvelteKit Database Prompt (Logic Gap):**
|
|
7
|
+
* In `src/utils/prompts.js`, update the `isBackendFramework` check to include `'svelte'`. Currently, SvelteKit users are not prompted for a database even though it is a full-stack framework.
|
|
8
|
+
|
|
9
|
+
**2. Connection String Clarity (Doc Gap):**
|
|
10
|
+
* In `apps/docs/src/content/docs/guides/database-connections.md`, change "Standard environment variables" to "deploy-stack injected variables".
|
|
11
|
+
* Add a brief explanation and a code block demonstrating how the user should construct their framework's connection string from these variables (e.g., `DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"`).
|
|
12
|
+
|
|
13
|
+
**3. Minor Wording Fixes:**
|
|
14
|
+
* In `apps/docs/src/content/docs/guides/database-connections.md`, update "Your auto-generated database name" to "Your deterministic database name".
|
|
15
|
+
* In `templates/README.md`, update "DB_USER: The auto-generated master username" to "DB_USER: The hardcoded master username (dbadmin)".
|
|
16
|
+
|
|
17
|
+
## Part 2: Implement `deploy-stack db connect`
|
|
18
|
+
Create a new command that securely tunnels from the developer's localhost directly to the isolated RDS instance via an active ECS container.
|
|
19
|
+
|
|
20
|
+
**1. Dependencies & CLI Plumbing:**
|
|
21
|
+
* Add `@aws-sdk/client-rds` to `package.json` dependencies. Pin the version to match the existing AWS SDKs (e.g., `3.1119.0`).
|
|
22
|
+
* Extract `resolveRegion`, `resolveProjectName`, `resolveCluster`, and `resolveService` from `src/commands/exec.js` into a new shared module: `src/utils/resolvers.js`.
|
|
23
|
+
* Update `src/commands/exec.js` to import them from here.
|
|
24
|
+
* **Critical Test Fix:** Update `tests/exec.test.js` to import these functions from `../src/utils/resolvers.js` instead of `exec.js`.
|
|
25
|
+
* **Scope Limit:** Leave the identical resolver functions inside `logs.js`, `status.js`, and `gc.js` completely untouched.
|
|
26
|
+
* Update `bin/cli.js` to parse the `db` command and add a HELP_TEXT line for it (e.g., ` db connect Open a secure local tunnel to your database`).
|
|
27
|
+
* If the user types `deploy-stack db` with no subcommand, print the `db connect` usage and exit non-zero (do not fall through to the scaffold/init flow).
|
|
28
|
+
* If the argument is `connect`, route it to `runDbConnect` exported from `src/commands/db.js`.
|
|
29
|
+
* Implement `parseDbArgs` in `src/commands/db.js` to support:
|
|
30
|
+
* `--port`: Validate this is purely numeric. Default to `5432`.
|
|
31
|
+
* `--show-credentials`: Boolean.
|
|
32
|
+
* Overrides: `--region`, `--cluster`, `--service`, and `--workspace`.
|
|
33
|
+
|
|
34
|
+
**2. AWS Resource Discovery (`src/commands/db.js`):**
|
|
35
|
+
* **Preconditions:** Reuse the `hasAwsCli` check (and Session Manager plugin guidance) before attempting the connection.
|
|
36
|
+
* **Resolve Names:** Use the shared resolvers. If a `--workspace` is provided (or if you detect one locally by reading `.terraform/environment`), append `-${workspace}` to the base project name so it correctly targets PR-preview resources (e.g., `${baseProjectName}-${workspace}-db`). If it's the default workspace, do not append anything.
|
|
37
|
+
* **Find the DB:** Query RDS `DescribeDBInstances` for the resolved DB identifier.
|
|
38
|
+
* *Edge Case:* If this throws `DBInstanceNotFound` (or if no DB is found), catch it and print a friendly, graceful exit message explaining that no database is provisioned for this environment.
|
|
39
|
+
* Extract the `Endpoint.Address`, `DBName`, and `MasterUserSecret.SecretArn`.
|
|
40
|
+
* **Fetch the Credentials:** Query Secrets Manager `GetSecretValue` using the `SecretArn`. Parse the returned JSON string to dynamically extract both `username` and `password`.
|
|
41
|
+
* **Find an ECS Task:** Query ECS `ListTasks` for the resolved cluster and service. Call `DescribeTasks` on the first returned task ARN.
|
|
42
|
+
* *Robustness:* Instead of blindly taking `containers[0]`, use the same logic as `exec.js`: find the expected container by name, or fallback to the first `RUNNING` container, then extract its `runtimeId`.
|
|
43
|
+
|
|
44
|
+
**3. Terminal Output & Security:**
|
|
45
|
+
Print a clear success message so the user can easily copy the credentials:
|
|
46
|
+
* Print the Local Host, Local Port, Database Name, and Username.
|
|
47
|
+
* **Security Check:** Unless the user passed `--show-credentials`, mask the password in the terminal output as `********`. If the flag is present, print the decrypted password.
|
|
48
|
+
* Print a fully formed connection string (also masking the password unless the flag is passed):
|
|
49
|
+
`postgresql://<username>:<password_or_mask>@localhost:<localPort>/<dbname>`
|
|
50
|
+
|
|
51
|
+
**4. Execute the SSM Tunnel:**
|
|
52
|
+
* Spawn the `aws` CLI as a child process using `spawn` (inheriting `stdio: 'inherit'`).
|
|
53
|
+
* Construct the arguments as an array to prevent Windows parsing errors:
|
|
54
|
+
```javascript
|
|
55
|
+
const ssmArgs = [
|
|
56
|
+
'ssm', 'start-session',
|
|
57
|
+
'--target', `ecs:${clusterName}_${taskId}_${runtimeId}`,
|
|
58
|
+
'--document-name', 'AWS-StartPortForwardingSessionToRemoteHost',
|
|
59
|
+
'--parameters', `{"host":["${dbHost}"],"portNumber":["5432"],"localPortNumber":["${localPort}"]}`,
|
|
60
|
+
'--region', region
|
|
61
|
+
];
|
|
62
|
+
```
|
|
63
|
+
* Wrap the flow in the standard error handling. Explicitly check for `UnrecognizedClientException` and `ExpiredTokenException` to trigger the centralized `aws sso login` guidance. Handle the specific case where no running ECS tasks are found.
|
|
64
|
+
* Fire a telemetry event named `db_connect_run`. **CRITICAL:** Explicitly ensure that the database password, username, and connection string are NEVER included in the telemetry event properties.
|
|
65
|
+
|
|
66
|
+
**5. Additional Deliverables:**
|
|
67
|
+
* Create `tests/db.test.js` with fully hoisted mocks for the AWS SDKs (`RDSClient`, `ECSClient`, `SecretsManagerClient`) and `spawn`. Verify the masking logic, missing DB logic, workspace resolution, and successful arg construction.
|
|
68
|
+
* Create a documentation page at `apps/docs/src/content/docs/cli/db.md` explaining the `db connect` command, the `--port` flag, the `--show-credentials` flag, and the override flags (`--workspace`, `--region`, `--cluster`, `--service`).
|
|
69
|
+
* **Doc Registration:** Update `apps/docs/astro.config.mjs` to register the new `db.md` page in the `sidebar` array under the CLI Reference section.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Spec: Database Lifecycle & Migration Suite (`db migrate`, `db backup`, `db restore`, & Pre-Deploy Migration Gate)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Deliver end-to-end Day-2 relational database lifecycle management for `deploy-stack` in two clean phases:
|
|
5
|
+
* **Phase A (Pre-Refactor & Shared Utilities):** Split `src/commands/db.js` into modular subcommand files under `src/commands/db/` (with `src/commands/db.js` acting as the dispatcher and re-export barrel), recurse in `tests/commands-import.test.js`, and extract shared helpers across `src/utils/rds.js`, `src/utils/ecs.js`, `src/utils/detector.js`, `src/utils/resolvers.js`, `src/utils/system.js`, and `src/commands/logs.js`.
|
|
6
|
+
* **Phase B (Feature Implementation):**
|
|
7
|
+
1. **On-Demand Remote Migration Runner (`deploy-stack db migrate`):** Launch an ephemeral one-off ECS Fargate task in the service's VPC subnets using the active service task definition (or an explicit `--task-def` revision in CI), stream CloudWatch logs via `FilterLogEventsCommand` with event deduplication, and exit with the container's exit code via `failCommand`.
|
|
8
|
+
2. **Pre-Deploy Database Migration Gate (`deploy-stack db migrate --setup-ci`):** Idempotently inject a migration gate step into `.github/workflows/deploy.yml` after the task definition registration step (using `actions/setup-node@v4` and `--task-def`) so migrations from the **newly built image** execute before the ECS service updates.
|
|
9
|
+
3. **On-Demand Database Snapshots (`deploy-stack db backup`):** Create tagged manual RDS snapshots (`CreateDBSnapshotCommand`) and poll with `pollUntil` until `available` (or return immediately with `--no-wait`).
|
|
10
|
+
4. **Database Snapshot Restore (`deploy-stack db restore`):** List snapshots via `DescribeDBSnapshotsCommand` (with pagination), warn explicitly about data replacement (`skip_final_snapshot = true`), and idempotently upsert `snapshot_identifier` inside `resource "aws_db_instance" "postgres"` in `terraform/database.tf`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Part 1: Pre-Refactor & Shared Utilities
|
|
15
|
+
|
|
16
|
+
1. **Modular `db` Directory + Recursive Import Guard:**
|
|
17
|
+
* Move the existing `connect` implementation into `src/commands/db/connect.js` and add `src/commands/db/migrate.js`, `src/commands/db/backup.js`, and `src/commands/db/restore.js`.
|
|
18
|
+
* Keep `src/commands/db.js` as the `runDb` dispatcher and re-export barrel (`runDb`, `runDbConnect`, `parseDbArgs`, `runDbMigrate`, `parseDbMigrateArgs`, `runDbBackup`, `parseDbBackupArgs`, `runDbRestore`, `parseDbRestoreArgs`, `detectMigrationCommand`) so existing imports in `bin/cli.js` and `tests/db.test.js` remain unbroken.
|
|
19
|
+
* Update `tests/commands-import.test.js` to recursively scan `src/commands/**/*.js` so all subcommand modules in `src/commands/db/` are checked by the telemetry-import guard.
|
|
20
|
+
2. **`src/utils/rds.js` (New):**
|
|
21
|
+
* `findDbInstance(rdsClient, dbIdentifier)`: wraps `DescribeDBInstancesCommand({ DBInstanceIdentifier: dbIdentifier })` and returns the instance object, or returns `null` when `DBInstanceNotFound` / `DBInstanceNotFoundFault` is thrown (re-throwing unexpected errors).
|
|
22
|
+
* `generateSnapshotId(dbIdentifier, now = new Date())`: formats `${dbIdentifier}-manual-YYYYMMDD-HHmmss` in UTC (lowercased, RDS-safe).
|
|
23
|
+
* `isValidSnapshotId(id)`: validates against RDS snapshot identifier rules (`^[a-zA-Z][a-zA-Z0-9-]{0,254}$`, no `--`, no trailing `-`).
|
|
24
|
+
3. **`src/utils/detector.js`:**
|
|
25
|
+
* Implement and export `detectMigrationCommand(cwd)` here (and re-export from `src/commands/db.js`), reusing the existing `package.json`, `Gemfile`, and `manage.py` inspection patterns in priority order:
|
|
26
|
+
1. `package.json` scripts: `scripts["db:migrate"]` -> `'npm run db:migrate'`, `scripts["migrate"]` -> `'npm run migrate'`.
|
|
27
|
+
2. `prisma/schema.prisma` or `prisma` in dependencies/devDependencies -> `'npx prisma migrate deploy'`.
|
|
28
|
+
3. `drizzle.config.ts` / `.js` / `.mjs` -> `'npx drizzle-kit migrate'`.
|
|
29
|
+
4. `alembic.ini` -> `'alembic upgrade head'`.
|
|
30
|
+
5. `manage.py` -> `'python manage.py migrate --noinput'`.
|
|
31
|
+
6. `bin/rails` or `Gemfile` with `rails` -> `'bundle exec rails db:migrate'`.
|
|
32
|
+
7. Otherwise `null`.
|
|
33
|
+
4. **`src/utils/resolvers.js`:**
|
|
34
|
+
* Export `resolveHeadless(options = {}, env = process.env, stdin = process.stdin, stdout = process.stdout)`: returns `true` when `Boolean(options.isHeadless || options.headless || env.CI || env.VITEST || env.NODE_ENV === 'test' || !stdin?.isTTY || !stdout?.isTTY)` (with an explicit `options.isHeadless === false` override supported for unit tests that simulate interactive TTY prompts).
|
|
35
|
+
* Export `resolveAppName(projectName, workspace, env = process.env)`: combines `projectName` with `resolveWorkspaceSuffix({ workspace }, env)` (`${projectName}${suffix}`).
|
|
36
|
+
5. **`src/utils/ecs.js`:**
|
|
37
|
+
* Export `fetchActiveService(ecsClient, clusterName, serviceName)`: calls `DescribeServicesCommand({ cluster: clusterName, services: [serviceName] })` and returns the service if found with `status === 'ACTIVE'`, or `null` otherwise.
|
|
38
|
+
6. **`src/utils/system.js`:**
|
|
39
|
+
* Export `pollUntil({ intervalMs, timeoutMs, sleepFn = sleep, nowFn = Date.now, onTick })`: executes `await onTick({ elapsedMs })` in a loop until it returns `{ done: true, value }` or `elapsedMs >= timeoutMs` (returning `{ timedOut: true }`), sleeping `intervalMs` between ticks.
|
|
40
|
+
7. **`src/commands/logs.js`:**
|
|
41
|
+
* Export `isNotFoundError(err)` and `buildLogStreamName(containerName, taskId)` (returning `ecs/${containerName}/${taskId}`).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Part 2: CLI Dispatcher, Parsers & Telemetry Conventions (`bin/cli.js` & `src/commands/db.js`)
|
|
46
|
+
|
|
47
|
+
1. **Dispatcher (`runDb`):**
|
|
48
|
+
* `bin/cli.js` routes `case 'db':` to `await runDb(args.slice(1), { ...(isHeadless ? { isHeadless: true } : {}) })` and updates `HELP_TEXT` to list `db connect`, `db migrate`, `db backup`, and `db restore`.
|
|
49
|
+
* If `subcommand` is missing or not one of `connect | migrate | backup | restore`, call `failCommand` with event `'db_run'`, `errorCode: 'UNKNOWN_DB_SUBCOMMAND'`, `reason: 'unknown-db-subcommand'`, and actionable usage output.
|
|
50
|
+
2. **Subcommand Argument Parsers (Backward-Compatible):**
|
|
51
|
+
* Keep `parseDbArgs(rawArgs)` unchanged for `db connect`.
|
|
52
|
+
* `parseDbMigrateArgs(rawArgs)`:
|
|
53
|
+
* Boolean flags: `['setup-ci', 'headless']`
|
|
54
|
+
* String flags: `[{ name: 'cmd', key: 'cmd' }, { name: 'task-def', key: 'taskDef' }, { name: 'timeout', key: 'timeout' }, { name: 'project-name', key: 'projectName' }, { name: 'region', key: 'region' }, { name: 'workspace', key: 'workspace' }, { name: 'cluster', key: 'cluster' }, { name: 'service', key: 'service' }, { name: 'container', key: 'container' }]`
|
|
55
|
+
* Also capture `rest`: if `rest.length > 0`, fail fast in `runDbMigrate` with `errorCode: 'UNEXPECTED_POSITIONAL_ARGS'`, `reason: 'unexpected-positional-args'`, hinting to wrap multi-word `--cmd` values in quotes.
|
|
56
|
+
* `parseDbBackupArgs(rawArgs)`:
|
|
57
|
+
* Boolean flags: `['no-wait', 'headless']`
|
|
58
|
+
* String flags: `[{ name: 'id', key: 'snapshotId' }, { name: 'timeout', key: 'timeout' }, { name: 'project-name', key: 'projectName' }, { name: 'region', key: 'region' }, { name: 'workspace', key: 'workspace' }, { name: 'db-identifier', key: 'dbIdentifier' }]`
|
|
59
|
+
* `parseDbRestoreArgs(rawArgs)`:
|
|
60
|
+
* Boolean flags: `['yes', 'headless']`
|
|
61
|
+
* String flags: `[{ name: 'project-name', key: 'projectName' }, { name: 'region', key: 'region' }, { name: 'workspace', key: 'workspace' }, { name: 'db-identifier', key: 'dbIdentifier' }]`
|
|
62
|
+
* Positional: `snapshotId = rest[0] || ''` (matching the `rollback.js` positional pattern).
|
|
63
|
+
3. **Telemetry Conventions:**
|
|
64
|
+
* Event names: `'db_migrate_run'`, `'db_backup_run'`, `'db_restore_run'`.
|
|
65
|
+
* Use `trackSuccess` and `failCommand` (with `errorCode: 'SCREAMING_SNAKE_CASE'` and `reason: 'kebab-case'`, plus `handleAuthErrorBranch` for AWS credential errors).
|
|
66
|
+
* **Privacy:** Never include raw `--cmd` strings or snapshot ARNs in telemetry properties; for `db_migrate_run`, record `cmd_source: 'explicit' | 'detected' | 'prompted'` and `ci_setup: Boolean(setupCi)`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Part 3: On-Demand Remote Migration Runner (`src/commands/db/migrate.js`)
|
|
71
|
+
|
|
72
|
+
1. **Command Resolution & Timeout Validation:**
|
|
73
|
+
* Validate `--timeout` if provided: must parse to a positive integer (seconds, default `600`); otherwise fail before AWS calls with `errorCode: 'INVALID_TIMEOUT'`, `reason: 'invalid-timeout'`.
|
|
74
|
+
* Resolve migration command:
|
|
75
|
+
* If `cmd` flag is provided and non-empty after `.trim()`, use it (`cmd_source = 'explicit'`).
|
|
76
|
+
* Else call `detectMigrationCommand(cwd)`:
|
|
77
|
+
* In interactive mode (`!resolveHeadless(options)`), prompt via Clack `text` (with `initialValue: detectedCmd || ''`, `placeholder: 'e.g. npx prisma migrate deploy'`). Handle `isCancel` cleanly. Set `cmd_source = 'prompted'`.
|
|
78
|
+
* In headless mode, if `detectedCmd` exists, use it (`cmd_source = 'detected'`); otherwise fail with `errorCode: 'MISSING_MIGRATION_CMD'`, `reason: 'missing-migration-cmd'`.
|
|
79
|
+
2. **If `--setup-ci` Is Passed (Pre-Deploy Gate Injection):**
|
|
80
|
+
* Do not make AWS API calls. Locate `.github/workflows/deploy.yml` in `cwd` (if missing, fail with `errorCode: 'WORKFLOW_NOT_FOUND'`, `reason: 'workflow-not-found'`).
|
|
81
|
+
* Inject or replace a delimited block (`# deploy-stack:db-migrate-start` ... `# deploy-stack:db-migrate-end`) inside `.github/workflows/deploy.yml`:
|
|
82
|
+
* Place the block **immediately after** the `Register new Task Definition` step (or immediately before `- name: Force ECS deployment` / service update step).
|
|
83
|
+
* Ensure `actions/setup-node@v4` (with `node-version: '20'`) is included in the injected block (unless `actions/setup-node` already exists earlier in the job) so `npx` is available on `ubuntu-latest`.
|
|
84
|
+
* Run `npx deploy-stack db migrate --cmd <shell-safe-quoted-cmd> --task-def "${{ steps.register-task-def.outputs.task-def-arn || env.NEW_TASK_DEF_ARN }}" --headless` (matching the task definition output/variable from `templates/github/deploy.yml` so migrations run using the **newly registered image revision** before the ECS service updates).
|
|
85
|
+
* Write the updated workflow file, emit `trackSuccess('db_migrate_run', { cmd_source, ci_setup: true })`, and return.
|
|
86
|
+
3. **Live ECS Task Execution (`!setupCi`):**
|
|
87
|
+
* Resolve `projectName`, `region`, `appName = resolveAppName(projectName, workspace)`, `clusterName = resolveCluster({ cluster }, appName)`, `serviceName = resolveService({ service }, appName)`.
|
|
88
|
+
* Call `fetchActiveService(ecsClient, clusterName, serviceName)`:
|
|
89
|
+
* If missing, fail with `errorCode: 'ECS_SERVICE_NOT_FOUND'`, `reason: 'ecs-service-not-found'`.
|
|
90
|
+
* Extract `awsvpcConfiguration` (`subnets`, `securityGroups`, `assignPublicIp = 'ENABLED'`) from `service.networkConfiguration.awsvpcConfiguration`.
|
|
91
|
+
* Determine `targetTaskDef = options.taskDef || service.taskDefinition`.
|
|
92
|
+
* Call `DescribeTaskDefinitionCommand({ taskDefinition: targetTaskDef })` and resolve the target container via `resolveContainer({ container }, appName)` (or `pickRuntimeContainer`):
|
|
93
|
+
* If the resolved container name does not exist in `taskDefinition.containerDefinitions`, fail before `RunTaskCommand` with `errorCode: 'CONTAINER_NOT_FOUND'`, `reason: 'container-not-found'`.
|
|
94
|
+
* Launch the task via `RunTaskCommand`:
|
|
95
|
+
* `cluster`: `clusterName`
|
|
96
|
+
* `taskDefinition`: `targetTaskDef`
|
|
97
|
+
* `launchType`: `'FARGATE'`
|
|
98
|
+
* `networkConfiguration`: `{ awsvpcConfiguration: { subnets, securityGroups, assignPublicIp } }`
|
|
99
|
+
* `startedBy`: `'deploy-stack-db-migrate'`
|
|
100
|
+
* `overrides`: `{ containerOverrides: [{ name: containerName, command: ['sh', '-c', resolvedCmd] }] }`
|
|
101
|
+
* If `failures?.length > 0` or `!tasks?.[0]?.taskArn`, fail with `errorCode: 'RUN_TASK_FAILED'`, `reason: 'run-task-failed'`.
|
|
102
|
+
4. **CloudWatch Log Streaming, SIGINT & Exit Code Propagation:**
|
|
103
|
+
* Register a `SIGINT` listener during polling that calls `StopTaskCommand({ cluster: clusterName, task: taskArn, reason: 'Cancelled by user via SIGINT' })` (best-effort) and cleans up the listener in `finally`.
|
|
104
|
+
* Use `pollUntil` (polling interval `2000ms` default, configurable via `options.pollIntervalMs` for tests; timeout `timeoutSeconds * 1000`):
|
|
105
|
+
* On each tick, call `FilterLogEventsCommand({ logGroupName: '/ecs/' + appName, logStreamNames: [buildLogStreamName(containerName, taskId)], startTime })` while deduplicating via a `seenEventIds = new Set()` (mirroring `src/commands/logs.js` and ignoring `isNotFoundError(err)` while the stream is initializing). Print new messages to stdout.
|
|
106
|
+
* Call `DescribeTasksCommand({ cluster: clusterName, tasks: [taskArn] })`. When `task.lastStatus === 'STOPPED'`, sleep `settleDelayMs` (`500ms` default, `0ms` in tests), perform one final log fetch, and return `{ done: true, value: task }`.
|
|
107
|
+
* If `pollUntil` times out, call `StopTaskCommand` (best-effort) and call `failCommand` with `errorCode: 'MIGRATION_TIMEOUT'`, `reason: 'migration-timeout'`.
|
|
108
|
+
* Inspect the matching container in `task.containers`:
|
|
109
|
+
* Prefer `container.reason` over `task.stoppedReason`.
|
|
110
|
+
* If `container?.exitCode === 0`, call `trackSuccess('db_migrate_run', { cmd_source, ci_setup: false })` and return `{ success: true, exitCode: 0, taskArn }`.
|
|
111
|
+
* Otherwise, compute `exitCode = (typeof container?.exitCode === 'number' && container.exitCode > 0) ? container.exitCode : 1` and call `failCommand` with event `'db_migrate_run'`, `errorCode: 'MIGRATION_TASK_FAILED'`, `reason: 'migration-task-failed'`, `exitCode`, and `extra: { exit_code: container?.exitCode ?? -1 }`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Part 4: On-Demand RDS Snapshots & Restore (`src/commands/db/backup.js` & `restore.js`)
|
|
116
|
+
|
|
117
|
+
1. **`deploy-stack db backup` (`src/commands/db/backup.js`):**
|
|
118
|
+
* Validate `--id` (if provided) via `isValidSnapshotId(id)` before making AWS calls; if invalid, fail with `errorCode: 'INVALID_SNAPSHOT_ID'`, `reason: 'invalid-snapshot-id'`.
|
|
119
|
+
* Validate `--timeout` (if provided, positive integer seconds, default `900`).
|
|
120
|
+
* Resolve `appName = resolveAppName(projectName, workspace)` and `dbIdentifier = resolveDbIdentifier({ dbIdentifier }, appName)`.
|
|
121
|
+
* Call `findDbInstance(rdsClient, dbIdentifier)`; if `null`, fail with `errorCode: 'RDS_INSTANCE_NOT_FOUND'`, `reason: 'rds-instance-not-found'`.
|
|
122
|
+
* Generate `snapshotId = options.snapshotId || generateSnapshotId(dbIdentifier)`.
|
|
123
|
+
* Call `CreateDBSnapshotCommand({ DBInstanceIdentifier: dbIdentifier, DBSnapshotIdentifier: snapshotId, Tags: [{ Key: 'ManagedBy', Value: 'deploy-stack' }, { Key: 'Project', Value: projectName }] })`.
|
|
124
|
+
* If `--no-wait` is true, log the snapshot ID, emit `trackSuccess('db_backup_run', { waited: false })`, and return `{ snapshotId, status: 'creating' }`.
|
|
125
|
+
* Otherwise poll `DescribeDBSnapshotsCommand({ DBSnapshotIdentifier: snapshotId })` via `pollUntil` (interval `5000ms` default, configurable via `options.pollIntervalMs`; timeout `timeoutSeconds * 1000`):
|
|
126
|
+
* Ignore transient `DBSnapshotNotFound` / `DBSnapshotNotFoundFault` on early ticks (eventual consistency right after `CreateDBSnapshot`).
|
|
127
|
+
* When `snapshot.Status === 'available'`, log the snapshot ID and restore command hint (`npx deploy-stack db restore ${snapshotId}`), emit `trackSuccess('db_backup_run', { waited: true })`, and return `{ snapshotId, status: 'available' }`.
|
|
128
|
+
* On timeout, fail with `errorCode: 'SNAPSHOT_TIMEOUT'`, `reason: 'snapshot-timeout'`.
|
|
129
|
+
2. **`deploy-stack db restore` (`src/commands/db/restore.js`):**
|
|
130
|
+
* Check that `terraform/database.tf` exists in `cwd` (if missing, fail with `errorCode: 'DATABASE_TF_NOT_FOUND'`, `reason: 'database-tf-not-found'`).
|
|
131
|
+
* Resolve `appName` and `dbIdentifier = resolveDbIdentifier({ dbIdentifier }, appName)`.
|
|
132
|
+
* Paginate `DescribeDBSnapshotsCommand({ DBInstanceIdentifier: dbIdentifier })` using `Marker` (and if a specific positional `snapshotId` was passed and not found under `DBInstanceIdentifier`, fall back to `DescribeDBSnapshotsCommand({ DBSnapshotIdentifier: snapshotId })` so snapshots from previously replaced/deleted instances can still be restored by ID).
|
|
133
|
+
* Sort discovered snapshots by `SnapshotCreateTime` descending:
|
|
134
|
+
* If no snapshots are found, fail with `errorCode: 'NO_SNAPSHOTS_FOUND'`, `reason: 'no-snapshots-found'`.
|
|
135
|
+
* If positional `snapshotId` is omitted:
|
|
136
|
+
* If `resolveHeadless(options)` is true, fail with `errorCode: 'MISSING_SNAPSHOT_ID'`, `reason: 'missing-snapshot-id'`.
|
|
137
|
+
* Otherwise prompt with Clack `select` displaying each snapshot's identifier, UTC timestamp, storage size, and type.
|
|
138
|
+
* Verify the selected snapshot has `Status === 'available'` (else fail with `errorCode: 'SNAPSHOT_NOT_AVAILABLE'`, `reason: 'snapshot-not-available'`).
|
|
139
|
+
* **Destructive Data Replacement Warning & Confirmation:**
|
|
140
|
+
* If `!options.yes`:
|
|
141
|
+
* In headless mode, fail with `errorCode: 'CONFIRMATION_REQUIRED'`, `reason: 'confirmation-required'`.
|
|
142
|
+
* In interactive mode, warn explicitly that setting `snapshot_identifier` on `aws_db_instance.postgres` will replace the current RDS instance on the next `apply` and (`skip_final_snapshot = true`) permanently discard any data written after the snapshot unless backed up first (recommending `npx deploy-stack db backup` first), then prompt `confirm`.
|
|
143
|
+
* **Idempotent HCL Upsert in `terraform/database.tf`:**
|
|
144
|
+
* Export a pure helper `upsertSnapshotIdentifier(hclContent, snapshotId)` scoped to `resource "aws_db_instance" "postgres"` (following the resource-scoped edit pattern in `src/commands/add.js`):
|
|
145
|
+
* Replace an existing `snapshot_identifier = "..."` attribute inside `resource "aws_db_instance" "postgres"`, or insert `snapshot_identifier = "${snapshotId}"` inside the resource block.
|
|
146
|
+
* Add or preserve a comment noting that `snapshot_identifier` should remain in `database.tf` after `apply` so subsequent applies stay no-op.
|
|
147
|
+
* Write `terraform/database.tf`, log instructions to run `npx deploy-stack apply`, call `trackSuccess('db_restore_run', {})`, and return `{ snapshotId }`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Part 5: Documentation, Roadmap & Unit Tests
|
|
152
|
+
|
|
153
|
+
1. **Documentation & Roadmap:**
|
|
154
|
+
* Update `apps/docs/src/content/docs/cli/db.md` to cover `db connect`, `db migrate` (`--cmd`, `--task-def`, `--setup-ci`, `--timeout`), `db backup` (`--id`, `--no-wait`, `--timeout`), and `db restore` (`<snapshot-id>`, `--yes`, and the `snapshot_identifier` lifecycle note).
|
|
155
|
+
* Mark **Pre-Deploy Database Migration Gate**, **On-Demand Database Snapshots & Restore**, and **On-Demand Remote Migration Runner** as `[x]` in `README.md` and `apps/docs/src/content/docs/roadmap.md`.
|
|
156
|
+
2. **Unit Tests (`tests/db.test.js` & utility test files):**
|
|
157
|
+
* Use `stripVTControlCharacters` from `node:util` on any `picocolors`-styled output assertions per `.muserules`.
|
|
158
|
+
* Test `src/utils/rds.js` (`findDbInstance`, `generateSnapshotId`, `isValidSnapshotId`), `detectMigrationCommand(cwd)` in `src/utils/detector.js`, `resolveHeadless` in `src/utils/resolvers.js`, `fetchActiveService` in `src/utils/ecs.js`, `pollUntil` in `src/utils/system.js`, and `buildLogStreamName` / `isNotFoundError` in `src/commands/logs.js`.
|
|
159
|
+
* Test `runDb` dispatcher (`UNKNOWN_DB_SUBCOMMAND`), `db migrate` (command resolution, unquoted positional guard, container validation against task definition, `--task-def` override, `FilterLogEventsCommand` deduplication + `ResourceNotFoundException` handling, exit code propagation via `failCommand`, timeout + `StopTaskCommand`, and `--setup-ci` idempotent injection into `.github/workflows/deploy.yml`), `db backup` (invalid `--id` fast failure, `--no-wait`, `DBSnapshotNotFound` eventual consistency during polling, quota/modifying error propagation), and `db restore` (pagination, fallback lookup by `DBSnapshotIdentifier`, headless guards, and idempotent `upsertSnapshotIdentifier` on `terraform/database.tf`).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Spec: Dependency-Aware Smart `init` & Multi-Capability Composition
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Upgrade `deploy-stack init` (`mainStack` in `src/commands/init.js`) from a framework-only scaffolder into a dependency-aware stack composer that inspects project manifests, local Docker Compose topologies, and environment variable templates to pre-select infrastructure capabilities and scaffold Day-0 + Day-2 addons in a single pass—while keeping `init --headless` deterministic when `--with` is omitted.
|
|
5
|
+
|
|
6
|
+
### Core Principles
|
|
7
|
+
1. **Zero Duplicate Template Logic:** `src/commands/init.js` must never duplicate HCL manipulation or addon rendering. Extract a quiet `scaffoldAddon()` helper in `src/commands/add.js` (paired with the existing `resolveAddonOptions`) and reuse `injectMigrationGate` from `src/commands/db/migrate.js`.
|
|
8
|
+
2. **Evidence-Based Pre-Selection:** Every pre-checked option in interactive mode displays the exact manifest or file signal that triggered it (`detected: ioredis, docker-compose redis`).
|
|
9
|
+
3. **Deterministic Headless Mode:** Running `deploy-stack init --headless` without `--with` preserves existing behavior and stdout (no surprise addon `.tf` files). Passing `--with <cap1,cap2,...>` explicitly scaffolds the requested addons in headless or interactive mode.
|
|
10
|
+
4. **Fail-Fast Validation Before Side Effects:** All `--with` and addon-flag validations run immediately after target directory resolution—before interactive prompts, AWS state-bucket provisioning, or `handleExistingFiles` backup creation.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Part 1: Project Capability Scanner (`src/utils/capabilities.js` & `src/utils/detector.js`)
|
|
15
|
+
|
|
16
|
+
### 1. New Module Placement & Export
|
|
17
|
+
Create `src/utils/capabilities.js` exporting `detectProjectCapabilities(cwd)` and re-export it from `src/utils/detector.js`.
|
|
18
|
+
|
|
19
|
+
### 2. Return Shape
|
|
20
|
+
* `relationalDb`: `{ detected: boolean, evidence: string[] }` (PostgreSQL-compatible RDS signals only)
|
|
21
|
+
* `worker`: `{ detected: boolean, suggestedCommand: string | null, evidence: string[] }`
|
|
22
|
+
* `migration`: `{ detected: boolean, command: string | null }` (delegates to `detectMigrationCommand(cwd)` from `src/commands/db/migrate.js`)
|
|
23
|
+
* `addons`: Map keyed by `ADDON_REGISTRY` capability ID (`db:redis`, `queue:sqs`, `storage:s3`, `db:dynamodb`, `ai:bedrock`, `email:ses`), each `{ detected: boolean, evidence: string[] }`
|
|
24
|
+
* `upcomingHints`: `{ vector: boolean, cron: boolean, mysql: boolean }`
|
|
25
|
+
|
|
26
|
+
### 3. Manifest & Config Scanning Rules (Read-Only, Fault-Tolerant, Tokenized)
|
|
27
|
+
Wrap all file reads in `try/catch` (reusing `readFileSafe` where helpful) so malformed files never crash `init`.
|
|
28
|
+
* **Tokenized Matching (No Substring Hazards):**
|
|
29
|
+
* `package.json`: match exact keys in merged `dependencies`, `devDependencies`, and `peerDependencies`.
|
|
30
|
+
* `requirements.txt`, `pyproject.toml`, `Pipfile`: extract normalized package names per line/entry via regex (`^[a-zA-Z0-9._-]+`, lowercased, with `_` normalized to `-` for comparison) without adding a TOML dependency.
|
|
31
|
+
* `go.mod`: match exact module paths per line.
|
|
32
|
+
* `Gemfile`: match exact gem names via `/^\s*gem\s+['"]([^'"]+)['"]/m`.
|
|
33
|
+
* **Environment Template Keys Only:** Scan `.env.example`, `.env.sample`, `.env.template`, and `.env` strictly for **variable key names** (`^[A-Z0-9_]+(?==)`). Never read, parse, or log variable values.
|
|
34
|
+
* **Docker Compose (`src/utils/dockerCompose.js`):** Extend `parseDockerCompose` additively to check `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, and `compose.yaml`.
|
|
35
|
+
* **Signal Matrix:**
|
|
36
|
+
1. **`relationalDb` (PostgreSQL RDS Pre-Select):**
|
|
37
|
+
* Node: `pg`, `postgres`, `typeorm`, `sequelize`, `knex`, `mikro-orm`, `drizzle-orm`, `drizzle-kit`, `@prisma/client`, `prisma` (unless `prisma/schema.prisma` explicitly specifies `provider = "mysql"`, `"sqlite"`, or `"mongodb"`).
|
|
38
|
+
* Python: `psycopg2`, `psycopg2-binary`, `psycopg`, `asyncpg`, `sqlalchemy`, `sqlmodel`, `alembic`, `django`.
|
|
39
|
+
* Go: `github.com/lib/pq`, `github.com/jackc/pgx`, `gorm.io/driver/postgres`.
|
|
40
|
+
* Ruby: `pg`, `rails`.
|
|
41
|
+
* Files / Compose / Env Keys: `drizzle.config.ts|js|mjs`, `alembic.ini`, `manage.py`, `bin/rails`, `prisma/schema.prisma` with `provider = "postgresql" | "postgres" | "cockroachdb"`, Docker Compose `postgres` / `postgis/postgis` image, or env keys `DATABASE_URL`, `POSTGRES_URL`, `POSTGRES_PRISMA_URL`, `PGHOST`.
|
|
42
|
+
2. **MySQL Signals (`upcomingHints.mysql` Only — Never Pre-Select PostgreSQL RDS):**
|
|
43
|
+
* Node: `mysql2`, `mysql`; Python: `pymysql`, `mysqlclient`, `aiomysql`; Go: `github.com/go-sql-driver/mysql`, `gorm.io/driver/mysql`; Ruby: `mysql2`; `prisma/schema.prisma` with `provider = "mysql"`; Docker Compose `mysql` / `mariadb` image; env keys `MYSQL_URL`, `MYSQL_HOST`.
|
|
44
|
+
* When only MySQL signals are present (and no PostgreSQL signals), set `relationalDb.detected = false` and `upcomingHints.mysql = true`. If both PostgreSQL and MySQL signals are present, both `relationalDb.detected` and `upcomingHints.mysql` are `true`.
|
|
45
|
+
3. **`db:redis`:**
|
|
46
|
+
* Node: `ioredis`, `redis`, `@upstash/redis`, `bull`, `bullmq`; Python: `redis`, `aioredis`, `rq`, or `celery` + `redis` co-presence; Go: `github.com/redis/go-redis`, `github.com/gomodule/redigo`; Ruby: `redis`, `sidekiq`, `connection_pool`.
|
|
47
|
+
* Compose: `redis`, `valkey/valkey`, `bitnami/redis`, `redis/redis-stack`.
|
|
48
|
+
* Env keys: `REDIS_URL`, `VALKEY_URL`, `REDIS_HOST`, `CELERY_BROKER_URL`.
|
|
49
|
+
4. **`queue:sqs`:**
|
|
50
|
+
* Node: `@aws-sdk/client-sqs`, `sqs-consumer`; Python: `kombu` or env key; Go: `github.com/aws/aws-sdk-go-v2/service/sqs`; Ruby: `aws-sdk-sqs`, `shoryuken`.
|
|
51
|
+
* Compose: `localstack/localstack` (with `sqs` in `SERVICES` if present) or `roribio16/alpine-sqs`.
|
|
52
|
+
* Env keys: `SQS_QUEUE_URL`, `SQS_DLQ_URL`, `AWS_SQS_QUEUE_URL`.
|
|
53
|
+
5. **`storage:s3`:**
|
|
54
|
+
* Node: `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `multer-s3`; Python: `django-storages`, `s3fs`; Go: `github.com/aws/aws-sdk-go-v2/service/s3`; Ruby: `aws-sdk-s3`, `shrine`, `carrierwave`.
|
|
55
|
+
* Compose: `minio/minio`.
|
|
56
|
+
* Env keys: `S3_BUCKET_NAME`, `S3_BUCKET`, `AWS_S3_BUCKET`, `S3_CDN_URL`.
|
|
57
|
+
6. **`db:dynamodb`:**
|
|
58
|
+
* Node: `@aws-sdk/client-dynamodb`, `@aws-sdk/lib-dynamodb`, `dynamoose`; Python: `pynamodb`, `aioboto3`; Go: `github.com/aws/aws-sdk-go-v2/service/dynamodb`; Ruby: `aws-sdk-dynamodb`.
|
|
59
|
+
* Compose: `amazon/dynamodb-local`.
|
|
60
|
+
* Env keys: `DYNAMODB_TABLE_NAME`, `DYNAMODB_TABLE`.
|
|
61
|
+
7. **`ai:bedrock`:**
|
|
62
|
+
* Node: `@aws-sdk/client-bedrock-runtime`, `@aws-sdk/client-bedrock`, `@ai-sdk/amazon-bedrock`, `@langchain/aws`; Python: `langchain-aws`; Go: `github.com/aws/aws-sdk-go-v2/service/bedrockruntime`; Ruby: `aws-sdk-bedrockruntime`.
|
|
63
|
+
* Env keys: `BEDROCK_MODEL_ID`, `AWS_BEDROCK_MODEL_ID`.
|
|
64
|
+
8. **`email:ses`:**
|
|
65
|
+
* Node: `@aws-sdk/client-ses`, `@aws-sdk/client-sesv2`; Python: `django-ses`; Go: `github.com/aws/aws-sdk-go-v2/service/ses`, `github.com/aws/aws-sdk-go-v2/service/sesv2`; Ruby: `aws-sdk-ses`, `aws-sdk-sesv2`.
|
|
66
|
+
* Env keys: `SES_FROM_EMAIL`, `SES_REGION`, `AWS_SES_REGION`.
|
|
67
|
+
9. **`worker`:**
|
|
68
|
+
* `package.json` scripts containing `worker`, `queue:work`, or `bull` -> `suggestedCommand: "npm run <script>"`; Node `bullmq`/`bull`/`sqs-consumer`; Python `celery`/`rq`/`dramatiq`; Ruby `sidekiq`/`shoryuken`/`good_job`; or non-web worker service in `Procfile` / Docker Compose.
|
|
69
|
+
10. **`upcomingHints.vector` & `upcomingHints.cron`:**
|
|
70
|
+
* `vector`: `pgvector` package (Node/Python/Ruby/Go), `prisma/schema.prisma` containing `vector`, or `ankane/pgvector` / `pgvector/pgvector` image in Docker Compose.
|
|
71
|
+
* `cron`: `node-cron`, `cron`, `agenda`, `APScheduler`, `celery-beat`, `robfig/cron`, `whenever`, `sidekiq-cron`, `sidekiq-scheduler`, or a non-empty `crons` array in `vercel.json`.
|
|
72
|
+
* **Evidence Formatting:** Format every evidence entry concisely as `<item>` (e.g., `'ioredis'`, `'docker-compose redis'`, `'REDIS_URL'`), deduplicated per capability and rendered in prompt hints as `detected: <item1>, <item2>`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Part 2: CLI Parser, `projectName` Sanitization & Fail-Fast Validation
|
|
77
|
+
|
|
78
|
+
### 1. `sanitizeProjectName` in `src/utils/prompts.js`
|
|
79
|
+
* Export `sanitizeProjectName(rawName)`:
|
|
80
|
+
* Coerce to string, trim, lowercase, replace any sequence of characters outside `[a-z0-9-]` (including dots `.` and underscores `_`) with a single hyphen `-`, strip leading and trailing hyphens, and fall back to `'app'` if the result is empty.
|
|
81
|
+
* In `getTargetDirectory` (`src/utils/prompts.js`), apply `sanitizeProjectName` to `actualProjectName` (never to `targetDir`). If the sanitized name differs from the raw basename/input, log a dim note (`Project name sanitized to "<actualProjectName>" for AWS resource compatibility.`).
|
|
82
|
+
|
|
83
|
+
### 2. Parser & Entry-Point Plumbing (`src/utils/parser.js`, `bin/cli.js`, `src/commands/init.js`)
|
|
84
|
+
* In `parseCliArgs` (`src/utils/parser.js`):
|
|
85
|
+
* Extract `isPreconfigured: args.includes('--preconfigured')`.
|
|
86
|
+
* Always populate an `initOptions` object (in both interactive and headless modes):
|
|
87
|
+
* `with`: array of capability strings parsed from all `--with <csv>` and `--with=<csv>` occurrences, split on commas, trimmed, filtered for non-empty strings, and deduplicated in order.
|
|
88
|
+
* `model`: string or `null` (`--model` / `--model=`).
|
|
89
|
+
* `domain`: string or `null` (`--domain` / `--domain=`).
|
|
90
|
+
* `zoneId`: string or `null` (`--zone-id` / `--zone-id=`).
|
|
91
|
+
* `fromEmail`: string or `null` (`--from-email` / `--from-email=`).
|
|
92
|
+
* `setupCiMigrate`: boolean (`args.includes('--setup-ci-migrate')`).
|
|
93
|
+
* In `bin/cli.js` and `mainStack` (`src/commands/init.js`):
|
|
94
|
+
* Pass `{ isHeadless, isPreconfigured, headlessOptions, initOptions }` into `mainStack`. Keep the function name `mainStack` unchanged.
|
|
95
|
+
|
|
96
|
+
### 3. Up-Front Validation (Before Prompts, AWS Calls, or File Backups)
|
|
97
|
+
Immediately after `getTargetDirectory` in `mainStack`:
|
|
98
|
+
1. If `initOptions.with.length > 0`:
|
|
99
|
+
* Verify every entry exists in `ADDON_REGISTRY`. If any entry is unknown, fail immediately with `reason: 'UNSUPPORTED_CAPABILITY'`, listing valid `Object.keys(ADDON_REGISTRY)` keys.
|
|
100
|
+
2. Validate addon-specific flags for capabilities present in `initOptions.with` (silently ignoring `--model` when `ai:bedrock` is not selected, and silently ignoring `--domain`/`--zone-id`/`--from-email` when `email:ses` is not selected):
|
|
101
|
+
* If `ai:bedrock` is in `initOptions.with` and `initOptions.model` is provided, validate it with the existing Bedrock model validator (`INVALID_MODEL_ID`).
|
|
102
|
+
* If `email:ses` is in `initOptions.with`:
|
|
103
|
+
* Validate `initOptions.domain` (`INVALID_DOMAIN`), `initOptions.zoneId` (`INVALID_ZONE_ID`), and `initOptions.fromEmail` (`INVALID_FROM_EMAIL`) if provided.
|
|
104
|
+
* In headless/preconfigured mode (`isHeadless || isPreconfigured`), if `initOptions.domain` is missing and no existing `terraform/domain.tf` in `targetDir` provides a domain, fail immediately with `reason: 'MISSING_SES_DOMAIN'`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Part 3: Interactive Prompts & Shared Addon Scaffolding
|
|
109
|
+
|
|
110
|
+
### 1. Quiet `scaffoldAddon` Core in `src/commands/add.js`
|
|
111
|
+
Refactor `src/commands/add.js` so `runAdd` and `mainStack` share the exact same scaffolding pipeline:
|
|
112
|
+
* Extract `scaffoldAddon(capability, resolvedOpts, { cwd, region })` which renders the addon `.tf` file, writes it to `terraform/<file>`, injects environment variables into `terraform/main.tf` and `terraform/worker.tf` (if present), applies the `queue:sqs` worker `ignore_changes` lifecycle rule when `worker.tf` exists, and calls `syncReadmeCost(cwd)`—returning `{ file, envVars, costImpact }` without calling Clack `intro`/`outro`, `trackEvent('add_run')`, or `process.exit`.
|
|
113
|
+
* `runAdd` continues to call `resolveAddonOptions` + `scaffoldAddon` wrapped in its existing banners and `add_run` telemetry.
|
|
114
|
+
|
|
115
|
+
### 2. Interactive Flow (`!isHeadless && !isPreconfigured`)
|
|
116
|
+
Run `const capabilities = detectProjectCapabilities(targetDir)` before `getProjectConfig`:
|
|
117
|
+
1. **Database Prompt (`src/utils/prompts.js`):**
|
|
118
|
+
* Pass `{ capabilities }` (optional 5th parameter) to `getProjectConfig`.
|
|
119
|
+
* If `capabilities.relationalDb.detected` is `true`, set `initialValue: true` on the PostgreSQL RDS `confirm` prompt and append `(detected: ${capabilities.relationalDb.evidence.join(', ')})` to the prompt message.
|
|
120
|
+
* If `capabilities.worker.detected` is `true` and `capabilities.worker.suggestedCommand` is non-null, pre-fill the worker command text prompt's `initialValue` / placeholder.
|
|
121
|
+
2. **Pre-Deploy Migration Gate Prompt (`src/commands/init.js`):**
|
|
122
|
+
* If `config.hasDb` is `true` and `capabilities.migration.detected` is `true`:
|
|
123
|
+
* If `initOptions.setupCiMigrate` was passed, enable the migration gate automatically without prompting.
|
|
124
|
+
* Otherwise prompt with Clack `confirm`:
|
|
125
|
+
* `message: 'Enable pre-deploy database migration gate in GitHub Actions? (detected: ' + capabilities.migration.command + ')'`
|
|
126
|
+
* `initialValue: true`
|
|
127
|
+
3. **Addons `multiselect` Prompt (`src/commands/init.js`):**
|
|
128
|
+
* Skip for static sites (`isStaticSite === true`).
|
|
129
|
+
* Build `options` in canonical order (`storage:s3`, `db:dynamodb`, `db:redis`, `queue:sqs`, `ai:bedrock`, `email:ses`), setting `hint` to `detected: <evidence> · <costLabel>` when detected (or `<costLabel>` otherwise).
|
|
130
|
+
* Pre-check (`initialValues`) the union of `initOptions.with` and capabilities where `capabilities.addons[cap].detected === true`.
|
|
131
|
+
* Prompt with `multiselect({ message: 'Select cloud addons to scaffold (Space to toggle, Enter to confirm):', options, initialValues, required: false })`.
|
|
132
|
+
4. **Follow-Up Prompts for Selected Addons (`ai:bedrock` & `email:ses`):**
|
|
133
|
+
* For `ai:bedrock`: if `initOptions.model` is set, resolve with that model. Otherwise prompt with `confirm({ message: 'Use recommended Bedrock model (' + defaultModelId + ')?', initialValue: true })`. If confirmed, resolve with the default model; if declined, call `resolveAddonOptions('ai:bedrock', {}, { cwd: targetDir, region, isInteractive: true })` to launch the two-step provider/model picker.
|
|
134
|
+
* For `email:ses`: call `resolveAddonOptions('email:ses', { domain: initOptions.domain, zoneId: initOptions.zoneId, fromEmail: initOptions.fromEmail }, { cwd: targetDir, region, isInteractive: true })`.
|
|
135
|
+
* For all other selected addons: call `resolveAddonOptions(cap, {}, { cwd: targetDir, region, isInteractive: false })`.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Part 4: Generation Ordering, Visualizer Preview, Hints & Telemetry
|
|
140
|
+
|
|
141
|
+
### 1. Generation Sequence in `mainStack` (`src/commands/init.js`)
|
|
142
|
+
Execute in this exact order after prompts and `handleExistingFiles`:
|
|
143
|
+
1. **Base Templates:** Compute `ESTIMATED_COST` including `selectedAddons` (`estimateMonthlyCost({ ...config, addons: selectedAddons })`) and call `generateTemplates(targetDir, ...)` so `main.tf`, `worker.tf`, `database.tf`, `README.md`, and `.github/workflows/deploy.yml` are written first.
|
|
144
|
+
2. **Addon Scaffolding:** Iterate through `selectedAddons` in canonical registry order, calling `scaffoldAddon(cap, resolvedOptsByCap[cap], { cwd: targetDir, region })` and logging a concise per-addon confirmation line (`✅ Scaffolded terraform/<file> (<cap>)`).
|
|
145
|
+
3. **Pre-Deploy Migration Gate:** If enabled (via interactive confirmation or `--setup-ci-migrate`), read `.github/workflows/deploy.yml`, transform it via `injectMigrationGate(content, migrationCmd)`, and write it back. If `--setup-ci-migrate` was passed in headless mode but `!config.hasDb` or `!capabilities.migration.command`, log a `log.warn` note and continue without failing.
|
|
146
|
+
4. **Print-Only Visualizer Preview (When Addons Are Scaffolded):** If `selectedAddons.length > 0`, call `parseTerraformConfig(path.join(targetDir, 'terraform'))`, attach `framework`, and call `renderDryRunPreview(parsedConfig, true)` (print-only mode) so the user sees their full stack topology and cost breakdown without altering default no-addon headless stdout.
|
|
147
|
+
5. **Post-Init Capability Hints:** In the outro area, if `capabilities.upcomingHints.vector`, `capabilities.upcomingHints.cron`, or `capabilities.upcomingHints.mysql` is `true`, print concise one-line informational hints (e.g., noting `pgvector`, scheduled tasks, or that RDS currently provisions PostgreSQL when MySQL dependencies were detected).
|
|
148
|
+
|
|
149
|
+
### 2. Telemetry (`project_provisioned`)
|
|
150
|
+
Enrich the existing `project_provisioned` event in `src/commands/init.js` with:
|
|
151
|
+
* `selected_addons`: string array of scaffolded capability IDs (e.g. `['db:redis', 'queue:sqs']`, or `[]`).
|
|
152
|
+
* `detected_addons`: string array of auto-detected capability IDs from `capabilities.addons`.
|
|
153
|
+
* `migration_gate_enabled`: boolean.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Part 5: CI Validation, Documentation & Unit Tests
|
|
158
|
+
|
|
159
|
+
### 1. CI Workflow (`.github/workflows/iac-validation.yml`)
|
|
160
|
+
Add a validation step that initializes a backend project with `--headless --with storage:s3,db:dynamodb,db:redis,queue:sqs,ai:bedrock,email:ses --domain example.com --setup-ci-migrate` (with `CI_MOCK_AWS=true`) and runs `terraform init -backend=false && terraform validate`.
|
|
161
|
+
|
|
162
|
+
### 2. Documentation (`apps/docs/src/content/docs/cli/init.md` & `README.md`)
|
|
163
|
+
Document smart dependency detection, the interactive addon `multiselect`, and the new flags (`--with`, `--model`, `--domain`, `--zone-id`, `--from-email`, `--setup-ci-migrate`).
|
|
164
|
+
|
|
165
|
+
### 3. Unit Tests (`tests/capabilities.test.js`, `tests/headless.test.js`, `tests/parser.test.js`)
|
|
166
|
+
* **`tests/capabilities.test.js` (new):**
|
|
167
|
+
* Test tokenized detection across `package.json`, `requirements.txt`, `pyproject.toml`, `Pipfile`, `go.mod`, `Gemfile`, `prisma/schema.prisma` (Postgres vs MySQL vs SQLite), `docker-compose.yml` / `compose.yaml`, `vercel.json` crons, and `.env.example` key names (verifying `.env` values are never read and substring false-positives do not fire).
|
|
168
|
+
* Verify Drizzle, Alembic, Django, and Rails trigger `relationalDb.detected === true`, while MySQL-only projects set `relationalDb.detected === false` and `upcomingHints.mysql === true`.
|
|
169
|
+
* **`tests/parser.test.js`:**
|
|
170
|
+
* Test `initOptions` parsing: repeatable and comma-separated `--with` deduplication, `--model`, `--domain`, `--zone-id`, `--from-email`, `--setup-ci-migrate`, and `isPreconfigured`.
|
|
171
|
+
* **`tests/headless.test.js`:**
|
|
172
|
+
* Verify `sanitizeProjectName` converts dots and underscores (`my.app_v2` -> `my-app-v2`) without changing `targetDir`.
|
|
173
|
+
* Verify default `init --headless` (without `--with`) writes no addon `.tf` files (`redis.tf`, `ses.tf`, etc.) even when dependencies exist in `package.json`.
|
|
174
|
+
* Verify `init --headless --with db:redis,queue:sqs,ai:bedrock` scaffolds `redis.tf`, `sqs.tf`, and `bedrock.tf`, injects all env vars into `main.tf` (and `worker.tf` with `ignore_changes`), and emits `selected_addons` on `project_provisioned`.
|
|
175
|
+
* Verify fail-fast guards (`UNSUPPORTED_CAPABILITY`, `MISSING_SES_DOMAIN`, `INVALID_MODEL_ID`) trigger before `provisionStateBucket` or `handleExistingFiles` backup runs.
|
|
176
|
+
* Update any telemetry module mock in `tests/headless.test.js` if needed to preserve named exports.
|