@grada-run/grada 0.0.1 → 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.
Files changed (231) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/iac-validation.yml +303 -0
  3. package/.github/workflows/publish.yml +68 -0
  4. package/.github/workflows/sync-bedrock-models.yml +57 -0
  5. package/.github/workflows/test.yml +43 -0
  6. package/.muserules +31 -0
  7. package/LICENSE +21 -0
  8. package/README.md +193 -3
  9. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  10. package/apps/docs/.astro/content-assets.mjs +4 -0
  11. package/apps/docs/.astro/content-modules.mjs +4 -0
  12. package/apps/docs/.astro/content.d.ts +179 -0
  13. package/apps/docs/.astro/data-store.json +1 -0
  14. package/apps/docs/.astro/dev.json +14 -0
  15. package/apps/docs/.astro/settings.json +5 -0
  16. package/apps/docs/.astro/types.d.ts +2 -0
  17. package/apps/docs/astro.config.mjs +97 -0
  18. package/apps/docs/package.json +17 -0
  19. package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
  20. package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
  21. package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
  22. package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
  23. package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
  24. package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
  25. package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
  26. package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
  27. package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
  28. package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
  29. package/apps/docs/src/content/docs/cli/add.md +84 -0
  30. package/apps/docs/src/content/docs/cli/apply.md +32 -0
  31. package/apps/docs/src/content/docs/cli/db.md +200 -0
  32. package/apps/docs/src/content/docs/cli/destroy.md +31 -0
  33. package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
  34. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  35. package/apps/docs/src/content/docs/cli/domain.md +57 -0
  36. package/apps/docs/src/content/docs/cli/drift.md +40 -0
  37. package/apps/docs/src/content/docs/cli/eject.md +29 -0
  38. package/apps/docs/src/content/docs/cli/exec.md +49 -0
  39. package/apps/docs/src/content/docs/cli/gc.md +37 -0
  40. package/apps/docs/src/content/docs/cli/init.md +72 -0
  41. package/apps/docs/src/content/docs/cli/logs.md +39 -0
  42. package/apps/docs/src/content/docs/cli/rollback.md +51 -0
  43. package/apps/docs/src/content/docs/cli/secrets.md +73 -0
  44. package/apps/docs/src/content/docs/cli/sleep.md +53 -0
  45. package/apps/docs/src/content/docs/cli/status.md +34 -0
  46. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  47. package/apps/docs/src/content/docs/guides/architecture.md +87 -0
  48. package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
  49. package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
  50. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
  51. package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
  52. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  53. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  54. package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
  55. package/apps/docs/src/content/docs/guides/examples.md +50 -0
  56. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  57. package/apps/docs/src/content/docs/guides/headless.md +75 -0
  58. package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
  59. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  60. package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
  61. package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
  62. package/apps/docs/src/content/docs/index.mdx +103 -0
  63. package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
  64. package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
  65. package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
  66. package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
  67. package/apps/docs/src/content/docs/roadmap.md +97 -0
  68. package/apps/docs/src/content/docs/testing-strategy.md +32 -0
  69. package/apps/docs/src/content.config.ts +7 -0
  70. package/apps/docs/src/custom.css +14 -0
  71. package/apps/docs/tsconfig.json +6 -0
  72. package/bin/cli.js +140 -0
  73. package/package.json +105 -7
  74. package/scripts/sync-bedrock-models.js +22 -0
  75. package/scripts/test-iac.js +261 -0
  76. package/specs/add-redis-sqs-bedrock.md +128 -0
  77. package/specs/add-storage-dynamodb.md +106 -0
  78. package/specs/bedrock-model-catalog.md +131 -0
  79. package/specs/ci-pipeline.md +17 -0
  80. package/specs/cost-transparency.md +115 -0
  81. package/specs/custom-domains-and-ses.md +153 -0
  82. package/specs/database-suite-expansion.md +151 -0
  83. package/specs/db-connect.md +69 -0
  84. package/specs/db-lifecycle-migrations.md +159 -0
  85. package/specs/dependency-aware-init.md +176 -0
  86. package/specs/deploy-stack-to-grada-run-rebrand.md +45 -0
  87. package/specs/deployment-safety.md +170 -0
  88. package/specs/diagnose.md +16 -0
  89. package/specs/docs-hub.md +16 -0
  90. package/specs/dx-polish.md +46 -0
  91. package/specs/exec.md +25 -0
  92. package/specs/finops-cron-drift.md +161 -0
  93. package/specs/gc.md +26 -0
  94. package/specs/integration-suite.md +16 -0
  95. package/specs/logs.md +32 -0
  96. package/specs/rollback-live-polling.md +40 -0
  97. package/specs/secrets-pull-audit.md +51 -0
  98. package/specs/serverless-lambda-target.md +133 -0
  99. package/specs/status.md +31 -0
  100. package/specs/telemetry-and-spawn-hardening.md +69 -0
  101. package/specs/telemetry-hardening.md +35 -0
  102. package/src/commands/add.js +1111 -0
  103. package/src/commands/apply.js +214 -0
  104. package/src/commands/db/backup.js +229 -0
  105. package/src/commands/db/connect.js +304 -0
  106. package/src/commands/db/enable-vector.js +344 -0
  107. package/src/commands/db/import.js +604 -0
  108. package/src/commands/db/migrate.js +477 -0
  109. package/src/commands/db/restore.js +361 -0
  110. package/src/commands/db.js +87 -0
  111. package/src/commands/destroy.js +217 -0
  112. package/src/commands/diagnose.js +460 -0
  113. package/src/commands/doctor.js +109 -0
  114. package/src/commands/domain.js +685 -0
  115. package/src/commands/drift.js +243 -0
  116. package/src/commands/eject.js +127 -0
  117. package/src/commands/exec.js +222 -0
  118. package/src/commands/gc.js +250 -0
  119. package/src/commands/init.js +649 -0
  120. package/src/commands/logs.js +256 -0
  121. package/src/commands/rollback.js +323 -0
  122. package/src/commands/secrets.js +485 -0
  123. package/src/commands/sleep.js +347 -0
  124. package/src/commands/status.js +309 -0
  125. package/src/commands/sync-ai.js +115 -0
  126. package/src/commands/wake.js +337 -0
  127. package/src/core/parser.js +126 -0
  128. package/src/core/telemetry.js +244 -0
  129. package/src/data/bedrock-models.json +896 -0
  130. package/src/utils/addons.js +126 -0
  131. package/src/utils/ai-rules.js +59 -0
  132. package/src/utils/args.js +91 -0
  133. package/src/utils/aws.js +178 -0
  134. package/src/utils/backup.js +69 -0
  135. package/src/utils/bedrock-catalog.js +511 -0
  136. package/src/utils/capabilities.js +500 -0
  137. package/src/utils/command.js +65 -0
  138. package/src/utils/db-tunnel.js +164 -0
  139. package/src/utils/detector.js +298 -0
  140. package/src/utils/dockerCompose.js +65 -0
  141. package/src/utils/domains.js +73 -0
  142. package/src/utils/ecs-runner.js +289 -0
  143. package/src/utils/ecs.js +92 -0
  144. package/src/utils/frameworks.js +55 -0
  145. package/src/utils/generator.js +527 -0
  146. package/src/utils/hcl.js +426 -0
  147. package/src/utils/lambda-ecr.js +185 -0
  148. package/src/utils/prompts.js +278 -0
  149. package/src/utils/rds.js +131 -0
  150. package/src/utils/resolvers.js +174 -0
  151. package/src/utils/sleep-state.js +140 -0
  152. package/src/utils/sleep-targets.js +139 -0
  153. package/src/utils/system.js +42 -0
  154. package/src/utils/terraform.js +70 -0
  155. package/src/utils/visualizer.js +381 -0
  156. package/src/utils/warnings.js +49 -0
  157. package/templates/README.md +150 -0
  158. package/templates/docker/django.Dockerfile +40 -0
  159. package/templates/docker/go.Dockerfile +23 -0
  160. package/templates/docker/nestjs.Dockerfile +33 -0
  161. package/templates/docker/nextjs.Dockerfile +55 -0
  162. package/templates/docker/node.Dockerfile +24 -0
  163. package/templates/docker/nuxt.Dockerfile +47 -0
  164. package/templates/docker/python.Dockerfile +38 -0
  165. package/templates/docker/rails.Dockerfile +59 -0
  166. package/templates/docker/static.Dockerfile +32 -0
  167. package/templates/docker/svelte.Dockerfile +52 -0
  168. package/templates/github/deploy-lambda.yml +120 -0
  169. package/templates/github/deploy.yml +138 -0
  170. package/templates/github/drift.yml +112 -0
  171. package/templates/github/preview-lambda.yml +86 -0
  172. package/templates/github/preview.yml +69 -0
  173. package/templates/github/teardown.yml +43 -0
  174. package/templates/terraform/addons/bedrock.tf +34 -0
  175. package/templates/terraform/addons/cron-lambda.tf +78 -0
  176. package/templates/terraform/addons/cron.tf +101 -0
  177. package/templates/terraform/addons/dynamodb.tf +73 -0
  178. package/templates/terraform/addons/redis.tf +64 -0
  179. package/templates/terraform/addons/s3.tf +143 -0
  180. package/templates/terraform/addons/ses.tf +73 -0
  181. package/templates/terraform/addons/sqs.tf +67 -0
  182. package/templates/terraform/backend.tf +22 -0
  183. package/templates/terraform/cloudfront-lambda.tf +80 -0
  184. package/templates/terraform/cloudfront.tf +80 -0
  185. package/templates/terraform/database-aurora-postgresql.tf +92 -0
  186. package/templates/terraform/database-mysql.tf +72 -0
  187. package/templates/terraform/database.tf +71 -0
  188. package/templates/terraform/main-lambda.tf +229 -0
  189. package/templates/terraform/main.tf +296 -0
  190. package/templates/terraform/network.tf +95 -0
  191. package/templates/terraform/oidc.tf +64 -0
  192. package/templates/terraform/secrets.tf +31 -0
  193. package/templates/terraform/worker.tf +69 -0
  194. package/tests/__snapshots__/generator.test.js.snap +9633 -0
  195. package/tests/add.test.js +2037 -0
  196. package/tests/ai.test.js +94 -0
  197. package/tests/apply.test.js +488 -0
  198. package/tests/args.test.js +86 -0
  199. package/tests/aws.test.js +244 -0
  200. package/tests/capabilities.test.js +307 -0
  201. package/tests/cli.test.js +29 -0
  202. package/tests/command.test.js +100 -0
  203. package/tests/commands-import.test.js +74 -0
  204. package/tests/db.test.js +2704 -0
  205. package/tests/destroy.test.js +391 -0
  206. package/tests/detector.test.js +79 -0
  207. package/tests/diagnose.test.js +779 -0
  208. package/tests/doctor.test.js +202 -0
  209. package/tests/domain.test.js +899 -0
  210. package/tests/drift.test.js +243 -0
  211. package/tests/ecs.test.js +130 -0
  212. package/tests/eject.test.js +65 -0
  213. package/tests/exec.test.js +380 -0
  214. package/tests/gc.test.js +496 -0
  215. package/tests/generator.test.js +794 -0
  216. package/tests/headless.test.js +562 -0
  217. package/tests/lambda-ecr.test.js +185 -0
  218. package/tests/logs.test.js +447 -0
  219. package/tests/parser.test.js +160 -0
  220. package/tests/rds.test.js +244 -0
  221. package/tests/resolvers.test.js +282 -0
  222. package/tests/rollback.test.js +692 -0
  223. package/tests/secrets.test.js +752 -0
  224. package/tests/sleep-wake.test.js +1016 -0
  225. package/tests/status.test.js +370 -0
  226. package/tests/system.test.js +70 -0
  227. package/tests/telemetry.test.js +520 -0
  228. package/tests/terraform.test.js +84 -0
  229. package/tests/visualizer.test.js +496 -0
  230. package/vitest.config.js +9 -0
  231. package/index.js +0 -2
@@ -0,0 +1,45 @@
1
+ # Specification: The Grada Brand Transition
2
+
3
+ ## 1. Overview
4
+ Transition the `deploy-stack` codebase to the new brand identity: **Grada**.
5
+ Existing deployments must not break; implement robust dual-read fallbacks for state files, machine markers, and tags. Version bump to `0.32.0`.
6
+
7
+ ## 2. NPM, Repository & Binary Renaming
8
+ - `package.json`: Update name to `grada-run`. Update `author` and `keywords` to include Grada terminology.
9
+ - `package.json`: Map the `bin` field to: `"grada": "./bin/cli.js"`, `"grada-run": "./bin/cli.js"`, and a deprecated fallback `"deploy-stack": "./bin/cli.js"`.
10
+ - `package.json` fields (`repository`, `bugs`, `homepage`) must point to `[https://github.com/grada-run/grada](https://github.com/grada-run/grada)`.
11
+ - Replace hardcoded `anton-codes-iac/deploy-stack` GitHub URLs across the codebase (error logs, docs) with `grada-run/grada`.
12
+ - Rename `CLI_ENTRY_BASENAMES` in `src/core/telemetry.js` to include `grada` and `grada-run`.
13
+ - Update CLI banner texts, `.muserules` title, AI rules filenames (write new ones, do not delete user legacy ones), and help texts to use `Grada` / `npx grada-run`.
14
+
15
+ ## 3. Dual-Read Local State & Env Vars
16
+ Read from `.grada/` first, fall back to `.deploy-stack/`. Write new state to `.grada/`.
17
+ - **Telemetry:** Check `~/.grada/telemetry-id` (raw text file) -> fallback to `~/.deploy-stack/telemetry-id`. Copy to new path on fallback.
18
+ - **Sleep Ledger:** Check `.grada/sleep-state.json` -> fallback to `.deploy-stack/sleep-state.json`. In `ensureSleepGitignore`, append `.grada/` but leave `.deploy-stack/`.
19
+ - **Bedrock Cache:** Check `~/.grada/bedrock-models-cache.json` -> fallback `~/.deploy-stack/bedrock-models-cache.json`.
20
+ - **Env Vars:** Rename `DEPLOY_STACK_TELEMETRY_ID_PATH` -> `GRADA_TELEMETRY_ID_PATH`, `DEPLOY_STACK_FRAMEWORK` -> `GRADA_FRAMEWORK`, and `DEPLOY_STACK_BEDROCK_CACHE_PATH` -> `GRADA_BEDROCK_CACHE_PATH` (all with legacy read fallbacks). Leave `DO_NOT_TRACK` as the sole telemetry opt-out. Rename `DEPLOY_STACK_BYPASS` in the Svelte template to `GRADA_BYPASS`.
21
+
22
+ ## 4. Machine Markers (Dual-Read)
23
+ The CLI must parse both `deploy-stack` and `grada` variants for existing files, but write `grada` for new ones.
24
+ - Domain mode: `# deploy-stack:domain-mode=` / `# grada:domain-mode=` AND `# deploy-stack:zone-id=` / `# grada:zone-id=`
25
+ - Migrate gate: `# deploy-stack:db-migrate-start` / `# grada:db-migrate-start` AND `# deploy-stack:db-migrate-end` / `# grada:db-migrate-end`
26
+ - AI Context: Dual read both HTML (`<!-- BEGIN GRADA CONTEXT -->` / `<!-- END GRADA CONTEXT -->`) and hash (`# BEGIN GRADA CONTEXT` / `# END GRADA CONTEXT`) block variants.
27
+
28
+ ## 5. Templates, Tagging & Eject
29
+ - Rename *all* cosmetic occurrences of the brand in templates (e.g., `npx deploy-stack` -> `npx grada-run`, `Generated by deploy-stack`, `# deploy-stack generated infrastructure`, `# deploy-stack addon:`, `deploy-stack-cron`, and git commit messages).
30
+ - Update `ManagedBy = "grada"` for new writes in templates, S3 state buckets, RDS snapshots, and providers. Rename `startedBy` to `grada-db-*`.
31
+ - In `src/commands/eject.js`, ensure the cleanup regex strips *both variants* of all four block types: `ManagedBy` tag blocks, cosmetic addon headers, `# deploy-stack backups` gitignore comments, and `# deploy-stack generated infrastructure` main.tf headers.
32
+
33
+ ## 6. Documentation & Generators
34
+ - **Docs Site (`apps/docs/`):** Update `astro.config.mjs` (`site: '[https://grada.run](https://grada.run)'`, remove the `base` property entirely, update `title`, `editLink`, `social` hrefs, redirects, and sidebar labels). Execute Astro build to regenerate `apps/docs/dist/`. Update internal `/deploy-stack/` links across `README.md`, `release_notes.md`, and all `apps/docs/src/` pages.
35
+ - **Generator Fallback Doc:** Rename fallback constant to `GRADA.md`. `resolveDocDest` precedence chain: `README.md` -> `DEPLOYMENT.md` -> `GRADA.md` -> legacy `DEPLOY-STACK.md` (returning the first missing-or-managed).
36
+ - **Cost Sync:** `syncDocCostEstimate` must scan `GRADA.md` and legacy `DEPLOY-STACK.md` in addition to `README.md` and `DEPLOYMENT.md`.
37
+
38
+ ## 7. GitHub Actions & CI
39
+ - Update `uses:` step from `anton-codes-iac/deploy-stack-action@v1` to `grada-run/grada-action@v1`.
40
+ - Update `scripts/test-iac.js` and `iac-validation.yml` to assert the new `grada` migrate markers.
41
+ - Validate that `.github/workflows/publish.yml` publishes `grada-run` and the scoped alias `@grada-run/grada`.
42
+
43
+ ## 8. Testing
44
+ - Use `npm run test:update` to carefully update the 131 legacy snapshot matches across the 12 test files.
45
+ - Add unit tests asserting the dual-read marker logic, telemetry fallback, sleep ledger fallback, and `eject` multi-brand cleanup.
@@ -0,0 +1,170 @@
1
+ # Deployment Safety & One-Command Rollback
2
+
3
+ This sprint delivers zero-downtime deployment safety nets across both infrastructure definition and Day-2 operations:
4
+ 1. Native ECS Deployment Circuit Breakers in Terraform (`templates/terraform/main.tf`).
5
+ 2. The `deploy-stack rollback [revision]` CLI command with interactive revision inspection.
6
+
7
+ ---
8
+
9
+ ## Part 1: Terraform Deployment Circuit Breakers
10
+
11
+ ### 1. Template Update (`templates/terraform/main.tf`)
12
+ In `templates/terraform/main.tf` (around lines 204–225), locate the existing `resource "aws_ecs_service" "app"` block. **Do NOT replace or alter existing attributes** (`${local.app_name}`, `{{DESIRED_COUNT}}`, `{{PORT}}`, `enable_execute_command = true`, or `depends_on = [aws_lb_listener.http]`). Insert **only** the `deployment_circuit_breaker` block inside the existing resource:
13
+
14
+ ```hcl
15
+ deployment_circuit_breaker {
16
+ enable = true
17
+ rollback = true
18
+ }
19
+ ```
20
+
21
+ ### 2. Generator Tests & Snapshot Update (`tests/generator.test.js`)
22
+ * In `tests/generator.test.js`, add an explicit assertion verifying that the generated `main.tf` content includes `deployment_circuit_breaker` with `enable = true` and `rollback = true`.
23
+ * Update existing snapshots in `tests/generator.test.js` by running `npx vitest run -u` and verify that the snapshot diff contains only the added `deployment_circuit_breaker` block.
24
+
25
+ ---
26
+
27
+ ## Part 2: `deploy-stack rollback` CLI Command
28
+
29
+ ### 1. CLI Routing & Argument Parsing (`bin/cli.js` & `src/commands/rollback.js`)
30
+ * In `bin/cli.js`:
31
+ * Add `rollback` to the flat `Commands:` list in `HELP_TEXT`, immediately after the `status` line:
32
+ ```
33
+ rollback [rev] Roll back ECS service to a previous task revision
34
+ ```
35
+ * Route `rollback` to `runRollback({ ...parseRollbackArgs(rawArgs), ...(isHeadless ? { isHeadless: true } : {}) })` exported from `src/commands/rollback.js`. **Important:** Only pass `isHeadless: true` when `--headless` was explicitly passed on the CLI; leave `options.isHeadless` as `undefined` otherwise so normal runs fall through to `process.env.CI || !process.stdin.isTTY` auto-detection while allowing tests to pass `isHeadless: false` explicitly.
36
+ * In `src/commands/rollback.js`:
37
+ * Implement and export `parseRollbackArgs(argv = [])`:
38
+ * Strip a leading `'rollback'` token if present.
39
+ * First non-flag positional argument: `revision` (optional string: can be numeric like `'12'`, `family:revision` like `'my-app-task:12'`, or a full task definition ARN).
40
+ * Flags (supporting both `--flag value` and `--flag=value` forms):
41
+ * `--cluster` (string)
42
+ * `--service` (string)
43
+ * `--region` (string)
44
+ * `--workspace` (string)
45
+ * `--skip-wait` (boolean, default `false`)
46
+
47
+ ### 2. Resolvers, Workspace Handling & Preconditions
48
+ * **SDK-Only Command:** Do **not** gate on `hasAwsCli` (unlike `exec` or `db connect`, `rollback` uses `@aws-sdk/client-ecs` directly, matching `status`, `logs`, and `gc`).
49
+ * **Imports:**
50
+ * Import `ECSClient`, `DescribeServicesCommand`, `ListTaskDefinitionsCommand`, `DescribeTaskDefinitionCommand`, and `UpdateServiceCommand` from `@aws-sdk/client-ecs` (do not import `DescribeTasksCommand`).
51
+ * Import `resolveRegion`, `resolveProjectName`, `resolveCluster`, and `resolveService` from `../utils/resolvers.js`.
52
+ * Import `handleAwsAuthError` from `../utils/aws.js`.
53
+ * **Workspace Resolution:**
54
+ * Support `--workspace` (or local `.terraform/environment` detection) using the exact `resolveWorkspaceSuffix` pattern from `src/commands/db.js`: if a non-`default` workspace is active, namespace `projectName` as `${baseProjectName}-${workspace}` before passing `projectName` into `resolveCluster` and `resolveService` (unless `--cluster` or `--service` was explicitly passed).
55
+ * **Injectable Dependencies & Testable Headless Override:**
56
+ * Accept `options.ecsClient` (defaulting to `new ECSClient({ region })`).
57
+ * Accept `options.pollIntervalMs` (default `5000`) and `options.timeoutMs` (default `300000`).
58
+ * **Testable Headless Resolution:** Because `bin/cli.js` only passes `isHeadless: true` when `--headless` is set, `options.isHeadless` is `undefined` in normal runs (preserving `process.env.CI || !process.stdin.isTTY` auto-detection) while allowing unit tests in Vitest to pass `isHeadless: false` explicitly:
59
+ ```javascript
60
+ const headless = typeof options.isHeadless === 'boolean'
61
+ ? options.isHeadless
62
+ : Boolean(process.env.CI || !process.stdin.isTTY);
63
+ ```
64
+ * **Spinner Lifecycle & Centralized Auth Error Handling:**
65
+ * Call `intro(color.bgCyan(color.black(' deploy-stack rollback ⏪ ')))` at the start of `runRollback`.
66
+ * Immediately instantiate `const s = spinner();` and call `s.start('Inspecting service and task revisions...');` before entering the `try` block so `s` is always initialized and safe to pass into `handleAwsAuthError`.
67
+ * In the outer `catch (error)` block, check for `UnrecognizedClientException` or `ExpiredTokenException`.
68
+ * **Critical Ordering:** Emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'AUTH_EXPIRED' })` and `await flushTelemetry()` *before* invoking `handleAwsAuthError(error, s, options)`, followed immediately by `return;`.
69
+
70
+ ### 3. Task Definition Discovery & Selection
71
+ 1. **Fetch Current Service State:**
72
+ * Call `DescribeServicesCommand({ cluster, services: [service] })`.
73
+ * If `services` is empty or `services[0].status !== 'ACTIVE'`, stop spinner (`s.stop(...)`), print an error that the service was not found or is inactive, emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'SERVICE_NOT_FOUND' })`, `await flushTelemetry()`, call `process.exit(1)`, and `return;`.
74
+ * Extract the active `currentTaskDefArn = services[0].taskDefinition` (e.g., `arn:aws:ecs:us-east-2:123456789012:task-definition/my-app-task:14`).
75
+ * **Extract Family Name & Revision Number:** Parse the substring between the last `/` and the last `:`:
76
+ ```javascript
77
+ const familyAndRev = currentTaskDefArn.split('/').pop();
78
+ const [family, currentRevStr] = familyAndRev.split(':');
79
+ const currentRevNum = Number(currentRevStr);
80
+ ```
81
+
82
+ 2. **Resolve Target Revision (Stopping Discovery Spinner in All Paths):**
83
+ * **Case A: Explicit `options.revision` provided via CLI (`rollback [rev]`):**
84
+ * Normalize the identifier:
85
+ * If `options.revision` is purely numeric (`/^\d+$/`), construct `targetInput = `${family}:${options.revision}``.
86
+ * Otherwise (it is already `family:rev` or a full ARN), use `targetInput = options.revision`.
87
+ * Validate by calling `DescribeTaskDefinitionCommand({ taskDefinition: targetInput })` directly (do **not** limit validation to the last 10 revisions from `ListTaskDefinitions`).
88
+ * If `DescribeTaskDefinitionCommand` throws or returns an inactive definition: stop spinner (`s.stop(...)`), print a clear error message, emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'REVISION_NOT_FOUND' })`, `await flushTelemetry()`, call `process.exit(1)`, and `return;`.
89
+ * Set `targetTaskDefArn = descResp.taskDefinition.taskDefinitionArn` and `targetRevisionNum = String(descResp.taskDefinition.revision)`.
90
+ * Stop the discovery spinner (`s.stop(\`Resolved revision \${targetRevisionNum}.\`)`) so `s` is stopped before Section 4.
91
+
92
+ * **Case B: No `options.revision` provided:**
93
+ * Call `ListTaskDefinitionsCommand({ familyPrefix: family, status: 'ACTIVE', sort: 'DESC', maxResults: 10 })`.
94
+ * **Strict Older-Revision Filtering:** Filter `taskDefinitionArns` so `eligibleArns` contains only revisions whose parsed revision number (`Number(arn.split(':').pop())`) is **strictly less than `currentRevNum`**. This prevents accidentally rolling *forward* if a newer task definition revision was registered in ECS but not deployed to the service.
95
+ * If `eligibleArns.length === 0`:
96
+ * Stop spinner (`s.stop(...)`), print `⚠ No previous task definition revisions found for family ${family}. Cannot roll back.`
97
+ * Emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'NO_PRIOR_REVISIONS' })`, `await flushTelemetry()`, call `process.exit(1)`, and `return;`.
98
+ * **If `headless` is true:**
99
+ * Select `targetTaskDefArn = eligibleArns[0]` (the newest revision older than `currentRevNum`) and extract `targetRevisionNum = targetTaskDefArn.split(':').pop()`.
100
+ * Stop the discovery spinner (`s.stop(\`Selected previous revision \${targetRevisionNum}.\`)`) so `s` is stopped before Section 4.
101
+ * **If `headless` is false (interactive TTY):**
102
+ * Take up to the first 5 ARNs from `eligibleArns` (`eligibleArns.slice(0, 5)`).
103
+ * Call `DescribeTaskDefinitionCommand` in parallel using `Promise.allSettled` for those 5 ARNs so a single failed describe call does not abort discovery:
104
+ * On `fulfilled`: extract `revision`, shortened image name/tag (`const rawImage = taskDefinition.containerDefinitions?.[0]?.image || 'unknown'; const shortImage = rawImage.split('/').pop();`), and `registeredAt` timestamp (formatted cleanly, e.g., ISO date string).
105
+ * On `rejected`: fallback to `revision = arn.split(':').pop()`, `shortImage = 'unknown'`, `registeredAtStr = ''`.
106
+ * Stop the discovery spinner (`s.stop('Found previous revisions.')`) before prompting.
107
+ * Prompt using `@clack/prompts` `select()`:
108
+ * `message: 'Select a task definition revision to roll back to:'`
109
+ * `options`: `{ value: arn, label: \`Revision \${rev} — \${shortImage}\`, hint: registeredAtStr }`
110
+ * If `isCancel(selectedArn)`:
111
+ * Print `outro(color.yellow('Rollback cancelled.'))` and `return { ok: false, reason: 'cancelled' }` cleanly (exit code 0, no error telemetry).
112
+ * Set `targetTaskDefArn = selectedArn` and `targetRevisionNum = targetTaskDefArn.split(':').pop()`.
113
+
114
+ ### 4. Service Update & Deployment Monitoring
115
+ 1. **Trigger Rollback:**
116
+ * Because Section 3 stops the discovery spinner on every path, cleanly start the rollback spinner before calling `UpdateServiceCommand`:
117
+ `s.start(\`Rolling back \${service} to revision \${targetRevisionNum}...\`);`
118
+ * Call `UpdateServiceCommand({ cluster, service, taskDefinition: targetTaskDefArn, forceNewDeployment: true })`.
119
+ 2. **If `options.skipWait` is true:**
120
+ * Stop spinner (`s.stop(\`Rollback to revision \${targetRevisionNum} triggered.\`)`).
121
+ * Emit `trackEvent('rollback_run', { projectName, success: true, targetRevision: String(targetRevisionNum), skipWait: true })` and `await flushTelemetry()`.
122
+ * Print `outro(color.green(\`Rollback to revision \${targetRevisionNum} initiated! 🚀\`))` and `return { ok: true, targetTaskDefArn, targetRevision: targetRevisionNum }`.
123
+ 3. **If `options.skipWait` is false (default):**
124
+ * Keep the spinner from Step 1 running and poll `DescribeServicesCommand({ cluster, services: [service] })` every `pollIntervalMs` until `timeoutMs` elapses:
125
+ * Locate the primary deployment: `const primary = svc.deployments?.find(d => d.status === 'PRIMARY' && d.taskDefinition === targetTaskDefArn);`
126
+ * **Success predicate:** If `primary` exists and (`primary.rolloutState === 'COMPLETED'` || (`primary.runningCount === primary.desiredCount && primary.desiredCount > 0 && svc.deployments.length === 1`)):
127
+ * Stop spinner with `color.green(\`Rolled back to revision \${targetRevisionNum}.\`)`.
128
+ * Emit `trackEvent('rollback_run', { projectName, success: true, targetRevision: String(targetRevisionNum) })` and `await flushTelemetry()`.
129
+ * Print `outro(color.green(\`Service successfully rolled back to revision \${targetRevisionNum}! 🚀\`))` and `return { ok: true, targetTaskDefArn, targetRevision: targetRevisionNum }`.
130
+ * **Failure predicate:** If `primary?.rolloutState === 'FAILED'`:
131
+ * Stop spinner with `color.red('❌ Rollback deployment failed.')`.
132
+ * Print diagnostic guidance (`npx deploy-stack status` and `npx deploy-stack logs`).
133
+ * Emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'ROLLOUT_FAILED', targetRevision: String(targetRevisionNum) })` and `await flushTelemetry()`.
134
+ * Call `process.exit(1)` and `return;`.
135
+ * **Timeout handling:** If the loop exceeds `timeoutMs`:
136
+ * Stop spinner with `color.yellow('⚠ Rollback timed out waiting for ECS stabilization.')`.
137
+ * Print guidance to check progress with `npx deploy-stack status`.
138
+ * Emit `trackEvent('rollback_run', { projectName, success: false, error_code: 'ROLLOUT_TIMEOUT', targetRevision: String(targetRevisionNum) })` and `await flushTelemetry()`.
139
+ * Call `process.exit(1)` and `return;`.
140
+
141
+ ---
142
+
143
+ ## Part 3: Test Suite (`tests/rollback.test.js`)
144
+
145
+ Create `tests/rollback.test.js` using Vitest:
146
+ * Inject a mocked `ecsClient` and `{ pollIntervalMs: 1, timeoutMs: 50 }` into `runRollback` so tests execute instantaneously.
147
+ * **Test Cases:**
148
+ 1. `parseRollbackArgs` parses positional `[rev]`, `--cluster`, `--service`, `--region`, `--workspace`, and `--skip-wait`.
149
+ 2. Explicit numeric revision (`rollback 12`) calls `DescribeTaskDefinitionCommand` with `${family}:12`, updates service, waits for `COMPLETED`, and tracks `rollback_run` with `{ success: true, targetRevision: '12' }`.
150
+ 3. `--skip-wait` path triggers `UpdateServiceCommand`, skips polling, emits `rollback_run` with `{ success: true, targetRevision: '12', skipWait: true }`, and returns `{ ok: true, targetTaskDefArn, targetRevision: '12' }`.
151
+ 4. Headless mode (`isHeadless: true` or `process.env.CI = 'true'` when `isHeadless` is omitted) with no positional revision filters out revisions greater than or equal to `currentRevNum` (e.g., ignores `rev 15` when service is on `rev 14` and selects `rev 13`).
152
+ 5. Interactive mode (`isHeadless: false`) calls `DescribeTaskDefinitionCommand` via `Promise.allSettled`, displays shortened image tags (`rawImage.split('/').pop()`), prompts via `@clack/prompts` `select`, and updates service with the chosen ARN (plus handles `isCancel` cleanly without exiting 1).
153
+ 6. Graceful exit (`process.exit(1)`) and telemetry `error_code` tracking for:
154
+ * Missing/inactive service (`SERVICE_NOT_FOUND`)
155
+ * Single revision with no older rollback targets (`NO_PRIOR_REVISIONS`)
156
+ * Non-existent explicit revision (`REVISION_NOT_FOUND`)
157
+ * Failed rollout state (`ROLLOUT_FAILED`)
158
+ * Poll timeout (`ROLLOUT_TIMEOUT`)
159
+ * Auth expiration (`AUTH_EXPIRED`) before calling `handleAwsAuthError`.
160
+
161
+ ---
162
+
163
+ ## Part 4: Documentation & Sidebar Registration
164
+
165
+ 1. **CLI Reference Page (`apps/docs/src/content/docs/cli/rollback.md`):**
166
+ * Use the standard Starlight frontmatter (`title`, `description`).
167
+ * Cover automatic ECS deployment circuit breakers vs. manual `npx deploy-stack rollback [revision]`.
168
+ * Document flags: `--cluster`, `--service`, `--region`, `--workspace`, and `--skip-wait`.
169
+ 2. **Sidebar Registration (`apps/docs/astro.config.mjs`):**
170
+ * In `apps/docs/astro.config.mjs`, add `{ label: 'rollback', slug: 'cli/rollback' }` to the `CLI Reference` sidebar items immediately after `{ label: 'status', slug: 'cli/status' }`.
@@ -0,0 +1,16 @@
1
+ # Spec: `deploy-stack diagnose` (alias: `wtf`)
2
+
3
+ ## Objective
4
+ Build a new CLI command that automatically queries AWS to troubleshoot why a deployed application is failing (e.g., container crash loops, ALB 502 Bad Gateway).
5
+
6
+ ## Requirements
7
+ 1. **Command Registration:** Register `diagnose` and its alias `wtf` in `bin/cli.js` using Commander.
8
+ 2. **AWS SDK Integration:** Use `@aws-sdk/client-ecs` and `@aws-sdk/client-cloudwatch-logs` to:
9
+ - Find the most recent stopped tasks in the ECS Fargate cluster.
10
+ - Extract the `stoppedReason` (e.g., OutOfMemory, Essential container in task exited).
11
+ - Fetch the last 50 lines of logs from CloudWatch for the failing container.
12
+ 3. **Output:** Format the output beautifully using `picocolors`. Highlight the exact error clearly so the user doesn't have to dig through JSON.
13
+
14
+ ## Constraints
15
+ - Mock the AWS SDK calls in the tests so they don't require real AWS credentials to pass.
16
+ - Do not modify the existing `apply` or `destroy` commands.
@@ -0,0 +1,16 @@
1
+ # Spec: Starlight Documentation Hub
2
+
3
+ ## Objective
4
+ Initialize an Astro Starlight documentation site inside the monorepo to serve as the official, version-controlled developer portal for `deploy-stack`.
5
+
6
+ ## Requirements
7
+ 1. **Scaffolding:** Initialize a new Astro Starlight project inside the `apps/docs/` directory.
8
+ 2. **Information Architecture:** Configure `astro.config.mjs` to render a sidebar with three main sections:
9
+ - **Core Concepts:** (Link to ADRs and architectural decisions).
10
+ - **Guides:** (Link to deployment guides and PaaS migrations).
11
+ - **CLI Reference:** (Command reference for init, apply, diagnose).
12
+ 3. **Migration:** Move all existing Markdown files from the root `docs/adrs/` folder into `apps/docs/src/content/docs/adrs/` so they are immediately searchable.
13
+
14
+ ## Constraints
15
+ - Use the standard Astro Starlight dependency tree.
16
+ - Ensure a script (e.g., `"docs:dev": "npm run dev --workspace=apps/docs"`) is added to the root `package.json` to allow starting the docs from the top level.
@@ -0,0 +1,46 @@
1
+ We need to resolve three UX issues identified via telemetry.
2
+
3
+ ## 1. Telemetry Noise Reduction (`src/core/telemetry.js`)
4
+ Because this is a hand-rolled fetch implementation, we are receiving rogue/junk events ('message', 'data', 'true') likely from stray event bindings or bot traffic.
5
+ * Add a strict guard at the very top of `trackEvent(eventName, properties)`.
6
+ * **The Guard:**
7
+ * Check if `typeof eventName !== 'string'`.
8
+ * Check if `eventName` includes any of these valid substrings: `['_run', '_pushed', '_pull', '_audit', '_streamed', '_executed', '_provisioned', '_ejected', '_applied', '_destroyed', 'recovery_', 'cli-error']`. Implement this using `.some(validStr => eventName.includes(validStr))`.
9
+ * If invalid or not a string, immediately `return;` (before the `DO_NOT_TRACK` check and without pushing anything to `pendingRequests`).
10
+
11
+ ## 2. Centralized AWS Auth Guidance (`src/utils/aws.js`)
12
+ We currently have `isAuthError` / `ExpiredTokenException` catch blocks scattered across 7 call sites in 5 files (`secrets.js`, `diagnose.js`, `exec.js`, `status.js`, `logs.js`).
13
+ * **Relocation:** Move `hasAwsCli(options = {})` AND the `AWS_CLI_INSTALL_URL` constant from `src/commands/exec.js` into `src/utils/aws.js` and export them. Ensure `utils/aws.js` imports `child_process` and `picocolors`.
14
+ * **Test/Source Fix:** Update `src/commands/exec.js` and `tests/exec.test.js` to import `hasAwsCli` and `AWS_CLI_INSTALL_URL` from `../utils/aws.js` (or `../src/utils/aws.js` for tests).
15
+ * **New Helper:** Create and export `handleAwsAuthError(error, clackSpinner = null, options = {})` in `src/utils/aws.js`.
16
+ * The caller is responsible for checking if the error is an auth error before calling this helper.
17
+ * **CRITICAL ORDERING:** The caller MUST emit `trackEvent` and `await flushTelemetry()` *before* invoking this helper, because this helper terminates the process.
18
+ * Inside the helper:
19
+ * If `clackSpinner` is provided, call `clackSpinner.stop(color.red('❌ AWS session expired or invalid credentials.'))`.
20
+ * If `hasAwsCli(options)` is false, print instructions to install the AWS CLI using `AWS_CLI_INSTALL_URL`.
21
+ * If `hasAwsCli(options)` is true, print: `Run aws sso login or aws configure to refresh your credentials.`
22
+ * In both cases, print the troubleshooting link: `https://github.com/anton-codes-iac/deploy-stack/blob/main/apps/docs/src/content/docs/guides/aws-credentials.md`
23
+ * Call `process.exit(1)` at the end of the helper, followed by `return;` to prevent test fall-through.
24
+ * **Refactor Call Sites:** Update all 7 call sites to invoke this helper (passing their local `opts` or `options` object so the `spawnSyncImpl` seam works).
25
+ * Remove their inline `process.exit(1)` calls.
26
+ * Add an explicit `return;` immediately after invoking the helper in the caller to ensure tests do not fall through.
27
+ * Remove redundant `spinner.stop()` calls *only* within the auth error branches (e.g., in `diagnose.js` and `status.js`). Ensure non-auth error paths keep their `spinner.stop()` calls so they don't hang the terminal.
28
+
29
+ ## 3. Graceful Secrets Onboarding (`src/commands/secrets.js` & `bin/cli.js`)
30
+ When running `secrets push`, if the `.env` file is missing, it currently throws a hard error.
31
+ * **Plumbing:** In `bin/cli.js`, pass `{ isHeadless }` to `pushSecrets` exactly like `pullSecrets` does. Update `@clack/prompts` import in `secrets.js` to include `isCancel`. Ensure `opts` is used instead of `options`, as `pushSecrets` normalizes arguments into `opts`.
32
+ * **UX Flow (in `pushSecrets` inner try/catch):** When `fsError.code === 'ENOENT'` is caught:
33
+ * Stop the spinner: `s.stop('No .env file found.');`
34
+ * **If `opts.isHeadless` OR `process.env.CI` is truthy:**
35
+ * Print exactly: `No .env file found. In automated environments, please ensure the file is generated before running secrets push.`
36
+ * Track event: `trackEvent('secrets_pushed', { success: false, reason: 'missing_env_headless' })` and `await flushTelemetry();`.
37
+ * Call `process.exit(1)` followed immediately by `return;`.
38
+ * **If interactive:**
39
+ * Use `@clack/prompts` `confirm` to ask: `Would you like to create an empty ${resolvedFilePath} file now to get started?`
40
+ * Handle `isCancel(confirmed)` by printing a cancellation message and `return;`.
41
+ * If true: Ensure parent directories exist using `await fs.mkdir(path.dirname(envPath), { recursive: true })` (using `fs/promises`). Write `# Add your environment variables here\n` to the absolute `envPath`. Print a success message to add variables and re-run. Track `secrets_pushed` event with `{ success: false, reason: 'created_empty_file' }`, `await flushTelemetry();`, then `return;`.
42
+ * If false: Print a cancellation message and `return;`.
43
+ * Ensure this inner catch block completely handles the flow and returns, so the error does not bubble up to the outer catch block.
44
+ * **Testing (`tests/secrets.test.js`):**
45
+ * GitHub Actions automatically sets `CI=true`. To prevent interactive tests from silently failing by taking the headless route, interactive test cases MUST save the original `process.env.CI`, run `delete process.env.CI`, and restore it after the test.
46
+ * Headless test cases should verify both the `opts.isHeadless` flag and the `process.env.CI` fallback.
package/specs/exec.md ADDED
@@ -0,0 +1,25 @@
1
+ # Spec: 1-Click Container Access (exec)
2
+
3
+ ## Objective
4
+ Implement `npx deploy-stack exec` to automatically drop the user into a secure, interactive shell inside their running AWS Fargate container using ECS Exec.
5
+
6
+ ## 1. Infrastructure Updates (Terraform)
7
+ To allow ECS Exec, the Fargate tasks need SSM permissions and the service needs a specific flag enabled.
8
+ - **Update `templates/terraform/main.tf` (and any other relevant tf files):**
9
+ - Add `enable_execute_command = true` to the `aws_ecs_service` resource.
10
+ - Ensure the `aws_iam_role.ecs_task_role` (the role the container runs as, NOT the execution role) has the following permissions:
11
+ - `ssmmessages:CreateControlChannel`
12
+ - `ssmmessages:CreateDataChannel`
13
+ - `ssmmessages:OpenControlChannel`
14
+ - `ssmmessages:OpenDataChannel`
15
+
16
+ ## 2. CLI Implementation (`src/commands/exec.js`)
17
+ - **Auto-Discovery:** Use `@aws-sdk/client-ecs` to dynamically find the active Cluster, Service, and a running Task ARN based on the current project directory.
18
+ - **Interactive Shell:** Use Node's `child_process.spawn` with `stdio: 'inherit'` to execute the AWS CLI command:
19
+ `aws ecs execute-command --cluster <cluster> --task <task_arn> --container <container_name> --interactive --command "/bin/sh"`
20
+ - **Prerequisite Checks:** Before spawning, quickly verify the user has the `aws` CLI installed (since ECS Exec relies on the AWS CLI and the Session Manager plugin under the hood). If missing, print a friendly error with a link to install it.
21
+ - **Error Handling:** If no tasks are running (e.g., scale is 0 or crashing), exit gracefully using `picocolors` and `@clack/prompts` explaining that a running container is required.
22
+
23
+ ## 3. Testing & Wiring
24
+ - Add `exec` to `bin/cli.js`.
25
+ - Create `tests/exec.test.js` using `vitest` to mock the ECS client and the `spawn` call, ensuring the correct AWS CLI arguments are constructed and prerequisite failures are handled smoothly.
@@ -0,0 +1,161 @@
1
+ # Spec: FinOps Hibernation (`sleep` / `wake`), Scheduled Cron Jobs, and IaC Drift Detection
2
+
3
+ ## Overview
4
+ Deliver three Day-2 operational and FinOps capabilities in Phase 10 to complete the ECS Fargate + RDS operational lifecycle:
5
+ 1. **Environment Hibernation & FinOps (`deploy-stack sleep [env]` & `deploy-stack wake [env]`):** Pause non-production or idle environments with a single command by scaling ECS services (`app` and `worker`) to `0` and stopping RDS instances or Aurora Serverless v2 clusters, displaying exact hourly/monthly cost savings and guarding against AWS's 7-day RDS auto-restart behavior.
6
+ 2. **Scheduled Cron Jobs (`deploy-stack add cron`):** Provision Amazon EventBridge Scheduler rules (`aws_scheduler_schedule`) that invoke ephemeral ECS Fargate tasks inside the private VPC on a `cron(...)` or `rate(...)` schedule with a `$0.00/mo` fixed baseline.
7
+ 3. **Scheduled IaC Drift Detection (`deploy-stack init --setup-ci-drift` / `.github/workflows/drift.yml`):** Scaffold an automated GitHub Actions workflow that periodically runs `terraform plan -detailed-exitcode` against live AWS state and automatically opens/updates a GitHub Issue (and dispatches an optional Slack webhook) when out-of-band AWS Console changes are detected.
8
+
9
+ ---
10
+
11
+ ## Part 1: Environment Hibernation & FinOps (`deploy-stack sleep` & `deploy-stack wake`)
12
+
13
+ ### 1. CLI Surface & Routing
14
+ * Register two new top-level commands in `bin/cli.js` and `src/core/parser.js`:
15
+ * `deploy-stack sleep [env]` → `src/commands/sleep.js` (`runSleep`)
16
+ * `deploy-stack wake [env]` → `src/commands/wake.js` (`runWake`)
17
+ * Supported flags for both commands:
18
+ * Positional `[env]` or `--workspace <name>`: Optional workspace/environment name (e.g. `staging`, `dev`, `pr-42`). If provided, resolves resource names with the workspace suffix (`${projectName}-${env}`) matching Terraform's `local.app_name` convention (`terraform.workspace == "default" ? var.project_name : "${var.project_name}-${terraform.workspace}"`).
19
+ * `--project-name <name>`, `--cluster <name>`, `--service <name>`, `--db-identifier <id>`, `--region <region>`.
20
+ * `--skip-db`: Scale ECS services only and leave the database running.
21
+ * `--wait` / `--no-wait`: On `wake`, wait for RDS/Aurora to become `available` and ECS tasks to reach `runningCount >= desiredCount` (default: wait with `pollUntil`, skippable with `--no-wait`).
22
+ * `--yes` / `--force` / `--headless`: Skip interactive confirmation on `sleep` when targeting the default (`production`/`default`) environment.
23
+
24
+ ### 2. Safety Guard on Production / Default Environment
25
+ * If no `[env]` / `--workspace` is specified (or `env === 'default'` / `'prod'` / `'production'`) and `--yes` / `--force` / `--headless` is not passed:
26
+ * Prompt interactively via `@clack/prompts` `confirm` (`"Put environment to sleep? This will take the web service offline until you run 'deploy-stack wake'."`, default `false`).
27
+ * When a non-default environment is passed (e.g., `deploy-stack sleep staging`), proceed without requiring an extra confirmation prompt.
28
+
29
+ ### 3. `deploy-stack sleep` Execution Flow
30
+ 1. **Resolve Target Context:**
31
+ * Resolve region (`resolveAwsRegion`), project name (`resolveProjectName`), and effective app prefix (`env && env !== 'default' ? `${projectName}-${env}` : projectName`).
32
+ * Inspect `terraform/` files locally (if present) via `parseTerraformConfig` to know whether `worker.tf` and `database.tf` exist and which `dbEngine` is configured.
33
+ 2. **Scale ECS Services to Zero:**
34
+ * Query `DescribeServicesCommand` on cluster `${appPrefix}-cluster` for `${appPrefix}-service` and (if present in `terraform/worker.tf` or discovered in the cluster) `${appPrefix}-worker`.
35
+ * Record the pre-sleep `desiredCount` (or default to `1` for web, `1` for worker) in memory/output, and call `UpdateServiceCommand({ cluster, service, desiredCount: 0 })` for each active service whose `desiredCount > 0`.
36
+ * If a service is already at `desiredCount === 0`, report it as already asleep (idempotent).
37
+ 3. **Stop Database (Unless `--skip-db`):**
38
+ * Use `findDbTarget(rdsClient, dbIdentifier, cwd)` from `src/utils/rds.js`.
39
+ * **Standard RDS Instance (`kind === 'instance'`):**
40
+ * If `status === 'available'`, call `StopDBInstanceCommand({ DBInstanceIdentifier: id })`.
41
+ * If `status === 'stopped'` or `'stopping'`, treat as idempotent and report already stopped/stopping.
42
+ * **Aurora Cluster (`kind === 'cluster'`):**
43
+ * If `status === 'available'`, call `StopDBClusterCommand({ DBClusterIdentifier: id })` (or if it is already configured with `0 ACU` auto-pause, note that stopping the cluster halts compute immediately and prevents wake-ups until `deploy-stack wake`).
44
+ 4. **7-Day AWS RDS Auto-Restart Guard & Savings Summary:**
45
+ * AWS automatically restarts stopped RDS instances and Aurora clusters after **7 consecutive days**.
46
+ * Print a clear warning with the exact UTC timestamp (`now + 7 days`):
47
+ * `⚠ AWS Note: Stopped RDS databases automatically restart after 7 days (<YYYY-MM-DD HH:mm UTC>). Re-run "npx deploy-stack sleep" or "npx deploy-stack destroy" for longer archiving.`
48
+ * Persist a lightweight state marker in `.deploy-stack/sleep-state.json` (gitignored if `.deploy-stack/` is local, or purely advisory) recording `{ env, sleptAt, autoRestartAt, services: { app: prevDesiredCount, worker: prevWorkerDesiredCount }, dbId, dbKind }` so `deploy-stack wake` (and `deploy-stack status`) can restore exact replica counts and warn if the 7-day window has elapsed.
49
+ * **FinOps Savings Calculation:**
50
+ * Calculate and display the exact hourly and monthly compute savings using the rates from `src/utils/visualizer.js`:
51
+ * Web Fargate task (`$9.01/mo` = `~$0.0123/hr` per replica)
52
+ * Worker Fargate task (`$9.01/mo` = `~$0.0123/hr` per replica when active)
53
+ * RDS `db.t4g.micro` (`$13.98/mo` = `~$0.0192/hr` compute paused; note that `20 GB` gp3 storage `~$2.30/mo` and ALB `$22.27/mo` remain active while infrastructure exists).
54
+ * Example output:
55
+ `💰 Estimated Savings While Asleep: ~$0.032/hr (~$22.99/mo in Fargate + RDS compute paused)`
56
+ `Wake anytime with: npx deploy-stack wake [env]`
57
+ * Emit `trackSuccess('sleep_run', { env_kind: isDefault ? 'default' : 'named', ecs_scaled: count, db_stopped: boolean, db_kind })`.
58
+
59
+ ### 4. `deploy-stack wake` Execution Flow
60
+ 1. **Start Database First (Unless `--skip-db`):**
61
+ * Resolve database via `findDbTarget`.
62
+ * If `status === 'stopped'`, call `StartDBInstanceCommand` (`kind === 'instance'`) or `StartDBClusterCommand` (`kind === 'cluster'`).
63
+ * Unless `--no-wait` is passed, poll with `pollUntil` (`DescribeDBInstancesCommand` / `DescribeDBClustersCommand`) until `status === 'available'` before starting ECS tasks so booting containers never crash-loop on an unreachable database.
64
+ 2. **Restore ECS Service Desired Counts:**
65
+ * Read `.deploy-stack/sleep-state.json` if present for the target `env` to restore previous `desiredCount` values (defaulting to `desiredCount = 1` for `${appPrefix}-service` and `desiredCount = 1` for `${appPrefix}-worker` if no state file exists).
66
+ * Call `UpdateServiceCommand({ cluster, service, desiredCount })`.
67
+ * Remove the entry for `env` from `.deploy-stack/sleep-state.json`.
68
+ * Emit `trackSuccess('wake_run', { env_kind, ecs_restored: count, db_started: boolean, waited: boolean })`.
69
+
70
+ ---
71
+
72
+ ## Part 2: Scheduled Cron Jobs (`deploy-stack add cron`)
73
+
74
+ ### 1. Capability Registration & CLI Flags
75
+ * Add `cron` (or `cron:scheduler` with alias `cron`) to `SUPPORTED_CAPABILITIES` in `src/utils/addons.js` and `src/commands/add.js`.
76
+ * Supported flags on `deploy-stack add cron`:
77
+ * `--schedule <expression>`: EventBridge Scheduler expression, e.g. `"cron(0 2 * * ? *)"` or `"rate(1 hour)"`. If omitted in interactive mode, prompt via `@clack/prompts` with common presets (`Hourly — rate(1 hour)`, `Daily at 00:00 UTC — cron(0 0 * * ? *)`, `Every 15 minutes — rate(15 minutes)`, `Custom expression...`). In headless mode without `--schedule`, default to `"cron(0 0 * * ? *)"` (daily at midnight UTC).
78
+ * `--cmd <command>` / `--cron-command <command>`: Command to execute inside the container when triggered (e.g. `"npm run cron"` or `"node scripts/cleanup.js"`). In headless mode without `--cmd`, default to `"npm run cron"`.
79
+ * `--name <job-name>`: Optional job slug (default: `"default"` or `"daily-job"`, sanitized to `[a-z0-9-]`), allowing multiple named schedules or updating in place with `--force`.
80
+ * `--timezone <tz>`: Optional IANA timezone string for `schedule_expression_timezone` (default: `"UTC"`).
81
+ * Validate `--schedule` before the Terraform guard: must match `/^(cron|rate|at)\(.+\)$/` (`INVALID_CRON_SCHEDULE`).
82
+ * Validate `--timezone` against safe characters `/^[A-Za-z0-9/_+-]{1,64}$/` (`INVALID_TIMEZONE`).
83
+
84
+ ### 2. Generated Terraform (`templates/terraform/addons/cron.tf` → `terraform/cron.tf`)
85
+ * Provision:
86
+ 1. **`aws_iam_role.scheduler_cron_role`:** Assumed by `scheduler.amazonaws.com`.
87
+ 2. **`aws_iam_role_policy.scheduler_cron_policy`:** Grants `ecs:RunTask` on `aws_ecs_task_definition.app.arn` (both revision-specific and family wildcard `${aws_ecs_task_definition.app.family}:*`) and `iam:PassRole` on `aws_iam_role.execution_role.arn` and `aws_iam_role.task_role.arn` (with `Condition: { StringLike: { "iam:PassedToService": "ecs-tasks.amazonaws.com" } }`).
88
+ 3. **`aws_scheduler_schedule.cron`:**
89
+ * `name = "${local.app_name}-cron-__CRON_NAME__"`
90
+ * `flexible_time_window { mode = "OFF" }`
91
+ * `schedule_expression = "__SCHEDULE_EXPRESSION__"`
92
+ * `schedule_expression_timezone = "__SCHEDULE_TIMEZONE__"`
93
+ * `target`:
94
+ * `arn = aws_ecs_cluster.main.arn`
95
+ * `role_arn = aws_iam_role.scheduler_cron_role.arn`
96
+ * `ecs_parameters`:
97
+ * `task_definition_arn = aws_ecs_task_definition.app.arn`
98
+ * `launch_type = "FARGATE"`
99
+ * `task_count = 1`
100
+ * `network_configuration` using `aws_subnet.public[*].id`, `[aws_security_group.ecs_tasks.id]`, and `assign_public_ip = true` (matching `aws_ecs_service.app` so it can pull from ECR and reach RDS without a NAT Gateway).
101
+ * `input = jsonencode({ containerOverrides = [{ name = "${local.app_name}-container", command = ["sh", "-c", __CRON_COMMAND_JSON__] }] })`
102
+ * Ensure all generated HCL in `cron.tf` passes `terraform validate` and `tflint` with zero warnings.
103
+ * **Cost Metadata (`src/utils/addons.js`):**
104
+ * `monthlyCost: 0.0`
105
+ * `pricingModel: 'usage'`
106
+ * `costImpact: '$0/mo fixed baseline (first 14M EventBridge Scheduler invocations/mo free); billed only for Fargate seconds while the cron task runs'`
107
+
108
+ ---
109
+
110
+ ## Part 3: Scheduled IaC Drift Detection (`.github/workflows/drift.yml`)
111
+
112
+ ### 1. CLI Integration (`deploy-stack drift` or `init --setup-ci-drift`)
113
+ * Support two entrypoints so both new and existing projects can enable Scheduled IaC Drift Detection:
114
+ 1. **During `deploy-stack init`:**
115
+ * New flag `--setup-ci-drift` in `src/core/parser.js` and `src/commands/init.js`.
116
+ * When `--setup-ci-drift` is passed (or offered in interactive `init`), scaffold `.github/workflows/drift.yml`.
117
+ 2. **On existing projects (`deploy-stack drift` or `deploy-stack drift --setup`):**
118
+ * Register `drift` in `bin/cli.js` → `src/commands/drift.js` (`runDrift`).
119
+ * `deploy-stack drift --setup` (or `deploy-stack drift init`): Scaffolds `.github/workflows/drift.yml` into an already-initialized project (reading region and state bucket from `terraform/main.tf` and `terraform/backend.tf`).
120
+ * `deploy-stack drift` (default check mode): Executes `terraform init -input=false` + `terraform plan -detailed-exitcode -no-color` locally in `terraform/`, interpreting exit code `0` (`✅ No infrastructure drift detected`), exit code `2` (`⚠ Infrastructure drift detected!` with the resource diff summary and `error_code: 'DRIFT_DETECTED'`), and exit code `1` (`TERRAFORM_PLAN_FAILED`).
121
+
122
+ ### 2. Generated GitHub Actions Workflow (`templates/github/drift.yml` → `.github/workflows/drift.yml`)
123
+ * **Triggers:**
124
+ * `schedule: [{ cron: '0 6 * * *' }]` (daily at 06:00 UTC)
125
+ * `workflow_dispatch: {}` (manual one-click trigger in GitHub Actions UI)
126
+ * **Permissions:**
127
+ * `id-token: write` (for AWS OIDC role assumption)
128
+ * `contents: read`
129
+ * `issues: write` (to open/update GitHub Issues when drift is detected)
130
+ * **Steps:**
131
+ 1. Checkout repository (`actions/checkout@v4`).
132
+ 2. Configure AWS credentials via the existing GitHub OIDC role (`aws-actions/configure-aws-credentials@v4` with `${{ secrets.AWS_ROLE_ARN }}` and the project's `AWS_REGION`).
133
+ 3. Setup Terraform (`hashicorp/setup-terraform@v3`, `terraform_wrapper: false`).
134
+ 4. Run `terraform init -input=false` in `working-directory: ./terraform`.
135
+ 5. Run `terraform plan -detailed-exitcode -no-color -out=tfplan` in `./terraform`, capturing the exit code (`0`, `1`, or `2`) and saving human-readable diff output (`terraform show -no-color tfplan > plan.txt`).
136
+ 6. **Automated Issue Creation / Update on Exit Code `2`:**
137
+ * Use `actions/github-script@v7` (or `gh issue`) when `steps.plan.outputs.exitcode == '2'` to search for an open issue labeled `iac-drift` (creating the label if needed):
138
+ * If an open `iac-drift` issue already exists, update its body/comment with the latest timestamp and Terraform plan diff (avoiding duplicate daily issue spam).
139
+ * Otherwise, open a new issue titled `"⚠️ Infrastructure Drift Detected in AWS"` containing the truncated Terraform plan diff and remediation commands (`npx deploy-stack apply` to reconcile or `terraform import`).
140
+ * If `steps.plan.outputs.exitcode == '0'` and an open `iac-drift` issue exists, automatically comment `"✅ Drift resolved"` and close the issue!
141
+ 7. **Optional Slack Notification:**
142
+ * If `${{ secrets.SLACK_WEBHOOK_URL }}` is configured and exit code is `2`, POST a JSON alert payload with the repository name, workflow run link, and drift summary.
143
+
144
+ ---
145
+
146
+ ## Part 4: Documentation, Roadmap & Validation
147
+
148
+ ### 1. Documentation & Roadmap Updates
149
+ * Create `apps/docs/src/content/docs/cli/sleep.md` (covering `deploy-stack sleep` and `deploy-stack wake`) and `apps/docs/src/content/docs/cli/drift.md` (covering `deploy-stack drift` and `.github/workflows/drift.yml`), and register them in the Starlight sidebar if configured.
150
+ * Update `apps/docs/src/content/docs/cli/add.md` with `deploy-stack add cron` (`--schedule`, `--cmd`, `--name`, `--timezone`).
151
+ * Check off the 3 items in `apps/docs/src/content/docs/roadmap.md`:
152
+ * `[x] Scheduled Cron Jobs`
153
+ * `[x] Environment Hibernation & FinOps`
154
+ * `[x] Scheduled IaC Drift Detection`
155
+
156
+ ### 2. Tests & IaC Validation
157
+ * Add unit tests:
158
+ * `tests/sleep-wake.test.js`: Verify ECS scaling to `0`, RDS `StopDBInstanceCommand` / `StopDBClusterCommand`, `--skip-db`, 7-day auto-restart warning output, `.deploy-stack/sleep-state.json` persistence, `wake` RDS polling before ECS restore, and production confirmation guard.
159
+ * `tests/add.test.js`: Verify `deploy-stack add cron` renders `terraform/cron.tf`, validates `--schedule` and `--timezone`, supports `--cmd` and `--name`, and emits `add_run` telemetry.
160
+ * `tests/drift.test.js`: Verify `deploy-stack drift --setup` generates valid `.github/workflows/drift.yml` YAML, and `deploy-stack drift` handles `terraform plan -detailed-exitcode` exit codes `0`, `1`, and `2`.
161
+ * Extend `scripts/test-iac.js` and `.github/workflows/iac-validation.yml` so `cron` is included in the `--with` addons validation run (`terraform validate` + `tflint`).
package/specs/gc.md ADDED
@@ -0,0 +1,26 @@
1
+ # Spec: Orphaned Resource Garbage Collection (`gc`)
2
+
3
+ ## Overview
4
+ The `gc` (Garbage Collection) command protects the user's AWS bill by scanning for lingering, orphaned infrastructure left behind by failed deployments, deleted PR previews, or manual AWS Console interventions.
5
+
6
+ ## UX & Flow
7
+ 1. Run `npx deploy-stack gc`.
8
+ 2. The CLI enters a "Dry Run / Discovery" phase, showing a spinner while it scans specific AWS services.
9
+ 3. It presents a categorized list of orphaned resources found, along with an estimated cost-saving or disk-space reclaimed.
10
+ 4. CLI prompts the user: `Are you sure you want to permanently delete these orphaned resources? (y/N)`.
11
+ 5. If confirmed, it deletes them and outputs a success summary.
12
+
13
+ ## Scan Targets (The Scope)
14
+ * **Target 1: Untagged ECR Images:** Find all images in ECR repositories matching the project prefix (`<project-name>-*`) that lack a tag, and batch delete them.
15
+ * **Target 2: Orphaned CloudWatch Log Groups:** Find log groups starting with `/ecs/<project-name>-` where the corresponding ECS cluster or service no longer exists.
16
+ * **Target 3: Unattached Elastic IPs (EIPs):** Find any EIPs in the VPC that are not associated with a running NAT Gateway or EC2 instance (to stop the hourly unused EIP charge).
17
+
18
+ ## Technical Contract
19
+ * **File:** `src/commands/gc.js`
20
+ * **Dependencies:** `@aws-sdk/client-ecr`, `@aws-sdk/client-cloudwatch-logs`, `@aws-sdk/client-ec2`, `@clack/prompts`.
21
+ * **Safety First:** The command must *never* delete a resource without explicit interactive confirmation. There is no `--yes` flag for this command to prevent CI pipeline accidents.
22
+
23
+ ## Test Requirements (`tests/gc.test.js`)
24
+ * Mock AWS SDKs to return a mix of active and orphaned resources.
25
+ * Prove that the dry-run output accurately counts only the orphaned resources.
26
+ * Prove that rejecting the confirmation prompt prevents any `Delete*` commands from firing.
@@ -0,0 +1,16 @@
1
+ # Spec: Integration Stability Suite (Headless Contracts)
2
+
3
+ ## Objective
4
+ Expand our Vitest coverage to guarantee that `deploy-stack` can be consumed by automated scripts and ecosystem wrappers (like `nest-deploy-stack`) without hanging on interactive prompts.
5
+
6
+ ## Requirements
7
+ 1. **Target:** Create a new test file at `tests/headless.test.js`.
8
+ 2. **Mocking:** Deep-mock `@clack/prompts` using `vi.mock()`.
9
+ 3. **Execution Assertions:**
10
+ - Execute the core CLI generation logic passing `--headless` and `--preconfigured` alongside necessary configuration flags (e.g., `--framework=nestjs --port=3000`).
11
+ - Assert that the mocked prompt functions (`text`, `select`, `confirm`, etc.) are **never** called during execution.
12
+ 4. **Output Assertions:** Ensure the CLI correctly applies the values passed via the flags rather than falling back to interactive defaults.
13
+
14
+ ## Constraints
15
+ - The test suite must run purely in memory or within a temporary directory to avoid polluting the local development environment with generated Terraform files.
16
+ - It must execute quickly alongside the existing snapshot tests.
package/specs/logs.md ADDED
@@ -0,0 +1,32 @@
1
+ # Spec: Context-Aware Log Streaming (`deploy-stack logs`)
2
+
3
+ ## Objective
4
+ Provide real-time and historical CloudWatch log streaming directly in the terminal, automatically mapped to the ECS task and cluster without requiring the user to look up log group names or open the AWS Management Console.
5
+
6
+ ## CLI Command & Flags
7
+ \`\`\`bash
8
+ deploy-stack logs [service] [options]
9
+ \`\`\`
10
+ - `[service]` (optional): Name of the service/container. Defaults to the primary application service defined in `terraform/main.tf`.
11
+ - `--tail <lines>`: Number of recent log lines to display (default: `50`).
12
+ - `-f, --follow`: Stream logs continuously in real time (polling interval: 2 seconds).
13
+ - `--error`: Filter output for common failure keywords (`ERROR`, `FATAL`, `Exception`, `fail`, `500`, `502`).
14
+ - `--since <duration>`: Filter logs from a relative time window (e.g., `5m`, `1h`, `1d`). Default: `1h` when not following.
15
+ - `--region <region>`: Explicit AWS region override.
16
+
17
+ ## Precedence & Resolution
18
+ 1. **Region Precedence:** `--region` flag -> `AWS_REGION` environment variable -> `terraform/main.tf` (`region = "..."`) -> fallback `us-east-2`.
19
+ 2. **Log Group Resolution:** Infer the CloudWatch log group `/ecs/<project-name>` by reading `terraform/main.tf` or project config. If not found, list log groups matching the project prefix.
20
+ 3. **AWS Session Handling:** On `ExpiredTokenException` or `UnrecognizedClientException`, print the standard recovery guidance (`aws sso login` / `aws configure`) and exit with code 1.
21
+
22
+ ## Output Formatting
23
+ - Standardize on `picocolors`.
24
+ - Prefix each line with formatted ISO timestamp (dimmed/gray) and task ID (dimmed).
25
+ - Highlight lines containing errors or HTTP 5XX statuses in red; warnings in yellow.
26
+ - Handle `SIGINT` (Ctrl+C) gracefully when following streams, printing a clean exit message without uncaught rejection traces.
27
+
28
+ ## Testing & Stability Requirements
29
+ - Add comprehensive Vitest unit tests in `tests/logs.test.js`.
30
+ - Mock `@aws-sdk/client-cloudwatch-logs` (`FilterLogEventsCommand`, `DescribeLogStreamsCommand`).
31
+ - Verify region resolution precedence, error filtering, line count truncation, and graceful exit on missing log groups.
32
+ - Zero network dependencies during test execution.