@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,40 @@
1
+ # Live Progress Updates for `deploy-stack rollback`
2
+
3
+ Enhance the existing `DescribeServicesCommand` polling loop in `src/commands/rollback.js` to emit live spinner updates via `s.message(...)` so the user has continuous visibility into task provisioning, container crashes, and old-task draining.
4
+
5
+ ## 1. Preserve Existing Contracts (`src/commands/rollback.js`)
6
+ Keep the existing polling architecture and test contracts intact:
7
+ * **Keep the in-place loop:** Do not extract a new signature or alter how `service` and spinner instance `s` are referenced.
8
+ * **Keep timing seams:** Preserve `pollIntervalMs` (default `5000`) and deadline-based `timeoutMs` (default `300000`) so all existing `{ pollIntervalMs: 1, timeoutMs: 50 }` tests continue to work without modification.
9
+ * **Keep `PRIMARY` identity guard:** Continue matching the active deployment via `d.status === 'PRIMARY' && d.taskDefinition === targetTaskDefArn`. Never match a bare `PRIMARY` with a stale task definition ARN.
10
+ * **Keep terminal states, UX, and telemetry:** Preserve the existing `rolloutState === 'COMPLETED'` success path, `rolloutState === 'FAILED'` failure path (`error_code: 'ROLLOUT_FAILED'`), and deadline timeout path (`error_code: 'ROLLOUT_TIMEOUT'`), along with their existing user-facing messages and guidance (`status` + `logs`).
11
+
12
+ ## 2. Elapsed Time & Live Counter Extraction
13
+ 1. Record `const startTime = Date.now();` when initializing the polling deadline (`const deadline = startTime + timeoutMs;`).
14
+ 2. On each poll iteration, after calling `DescribeServicesCommand`, compute:
15
+ * `const elapsedSec = Math.floor((Date.now() - startTime) / 1000);`
16
+ 3. If the matched `primary` deployment is not yet in a terminal state (`COMPLETED` or `FAILED`), extract its counters with safe zero defaults:
17
+ * `const runningCount = primary?.runningCount ?? 0;`
18
+ * `const desiredCount = primary?.desiredCount ?? 0;`
19
+ * `const pendingCount = primary?.pendingCount ?? 0;`
20
+ * `const failedTasks = primary?.failedTasks ?? 0;`
21
+
22
+ ## 3. Dynamic Spinner Messages (`s.message(...)`)
23
+ On each non-terminal poll tick (right before checking the deadline / sleeping for `pollIntervalMs`), update the active spinner instance `s.message(...)` using the following priority order:
24
+
25
+ 1. **Target `PRIMARY` not yet observed (`!primary`):**
26
+ `Rolling back ${service} to revision ${targetRevision}... [${elapsedSec}s] (registering deployment...)`
27
+ 2. **Tasks are failing (`failedTasks > 0`):**
28
+ `Rolling back ${service} to revision ${targetRevision}... [${elapsedSec}s] (${runningCount}/${desiredCount} running, ${pendingCount} pending, ${failedTasks} failed ⚠️ — container crashing)`
29
+ 3. **New tasks are running, waiting on ECS cleanup/deprovisioning (`runningCount >= desiredCount && desiredCount > 0 && pendingCount === 0`):**
30
+ `Rolling back ${service} to revision ${targetRevision}... [${elapsedSec}s] (${runningCount}/${desiredCount} running — draining previous tasks)`
31
+ 4. **Standard provisioning (default in-progress state):**
32
+ `Rolling back ${service} to revision ${targetRevision}... [${elapsedSec}s] (${runningCount}/${desiredCount} running, ${pendingCount} pending)`
33
+
34
+ ## 4. Unit Tests (`tests/rollback.test.js`)
35
+ * Keep all existing unit tests, timing options (`{ pollIntervalMs: 1, timeoutMs: 50 }`), and telemetry assertions (`error_code: 'ROLLOUT_FAILED'` / `'ROLLOUT_TIMEOUT'`) unchanged.
36
+ * Add unit tests verifying that `s.message` is called with the expected progress strings during multi-poll sequences:
37
+ 1. When the first poll has Not-Yet-Matching `PRIMARY` (`(registering deployment...)`) before transitioning to `COMPLETED`.
38
+ 2. Standard provisioning progress (`(0/1 running, 1 pending)`).
39
+ 3. Draining progress when `runningCount >= desiredCount` and `pendingCount === 0` (`(1/1 running — draining previous tasks)`).
40
+ 4. Crashing container warning when `failedTasks > 0` (`(0/1 running, 0 pending, 2 failed ⚠️ — container crashing)`).
@@ -0,0 +1,51 @@
1
+ # Spec: Secrets Pull and Audit Commands
2
+
3
+ ## 1. Overview
4
+ Introduce two new CLI commands to manage Day-2 configuration drift:
5
+ - `npx deploy-stack secrets pull`: Fetches the JSON payload from AWS Secrets Manager and merges it into the local `.env` file.
6
+ - `npx deploy-stack secrets audit`: Compares the local `.env` file against the remote AWS Secrets Manager payload and displays a colored drift report (missing locally, missing remotely, conflicting values).
7
+
8
+ ## 2. CLI Interface & UX
9
+ Both commands must use `@clack/prompts` and `picocolors` to match the existing CLI design system.
10
+
11
+ ### `secrets pull`
12
+ - **Command:** `npx deploy-stack secrets pull [file]` (defaults to `.env`).
13
+ - **Flow:**
14
+ 1. Show a spinner: "Fetching secrets from AWS..."
15
+ 2. If the local `.env` file exists and has conflicting values, use Clack's `confirm` prompt to ask: "Conflicting variables found. Overwrite local values with remote?"
16
+ 3. If headless (`--headless`), automatically overwrite.
17
+ 4. Write/append the variables to the `.env` file in standard `KEY="VALUE"` format.
18
+ 5. Show an outro: "Successfully synced X secrets to .env".
19
+
20
+ ### `secrets audit`
21
+ - **Command:** `npx deploy-stack secrets audit [file]` (defaults to `.env`).
22
+ - **Flow:**
23
+ 1. Show a spinner: "Auditing local environment against AWS..."
24
+ 2. Parse local `.env` and fetch the remote JSON secret.
25
+ 3. Print a visual diff:
26
+ - Use green for keys only present in AWS (Missing locally).
27
+ - Use yellow for keys with mismatched values.
28
+ - Use gray/dim for keys only present locally (Not tracked in AWS).
29
+ 4. Show an outro summarizing the drift count.
30
+
31
+ ## 3. AWS SDK v3 Integration
32
+ - Use `@aws-sdk/client-secrets-manager` (`GetSecretValueCommand`).
33
+ - The secret name convention should match the one established in `secrets push` (e.g., `<projectName>-secrets`).
34
+ - Handle `ResourceNotFoundException` gracefully by informing the user that no remote secrets exist yet and suggesting they run `secrets push` first.
35
+
36
+ ## 4. File System & Parsing
37
+ - Use the built-in `fs` module to read and write `.env`.
38
+ - To parse the `.env` file safely, you can either implement a standard regex parser or use a lightweight zero-dependency approach. Do not introduce bloated dependencies if a simple parser suffices.
39
+ - When writing to `.env`, preserve existing local variables that do not exist in AWS. Do not delete local-only overrides unless explicitly instructed.
40
+
41
+ ## 5. Telemetry
42
+ - Call `trackEvent('secrets_pull', { projectName, variablesCount })`.
43
+ - Call `trackEvent('secrets_audit', { projectName, driftCount })`.
44
+ - Await `flushTelemetry()` before exiting.
45
+
46
+ ## 6. Testing Requirements (Strictly Headless)
47
+ - Add tests in a new file (e.g., `test/secrets-pull.test.js`).
48
+ - Use `vitest`.
49
+ - **Deep Mocking Required:** You must completely mock `fs` (or use a temp directory), `@clack/prompts`, and `@aws-sdk/client-secrets-manager`.
50
+ - The test suite must not pause for user input. Simulate the `confirm` prompt returning both `true` and `false`.
51
+ - Ensure tests run at 0 CVEs and pass in a clean CI environment.
@@ -0,0 +1,133 @@
1
+ # Spec: Serverless Compute Primitives (`deploy-stack --target lambda`)
2
+
3
+ ## Overview
4
+ Provide an alternate **AWS Lambda + Amazon API Gateway HTTP API (v2)** deployment target (`--target lambda`) alongside the default ECS Fargate + ALB architecture (`--target ecs` / `fargate`). Using the **AWS Lambda Web Adapter** extension inside the generated container image, existing web applications (Node.js/Express, Next.js, FastAPI/Python, Go, Rust, etc.) run on AWS Lambda with zero application code changes and a **$0.00/mo fixed compute and load-balancer baseline**.
5
+
6
+ ---
7
+
8
+ ## 1. CLI Flags & Interactive Prompt (`src/core/parser.js` & `src/commands/init.js`)
9
+
10
+ ### 1.1 Flag Parsing (`src/core/parser.js`)
11
+ * Support `--target <target>` in `parseArgs` / `extractInitOptions`:
12
+ * Canonical values: `'ecs'` (default) and `'lambda'`.
13
+ * Accept `'fargate'` as a synonym for `'ecs'`.
14
+ * Reject unknown values before provisioning with `INVALID_COMPUTE_TARGET` (`✖ Invalid compute target "<value>". Supported targets: ecs, lambda`).
15
+
16
+ ### 1.2 Interactive & Headless `init` (`src/commands/init.js`)
17
+ * In headless mode (`--headless`), default `target` to `'ecs'` unless `--target lambda` is explicitly passed.
18
+ * In interactive mode, if `--target` was not explicitly passed, prompt via `@clack/prompts` `select`:
19
+ * Message: `"Select your AWS compute target:"`
20
+ * Options:
21
+ * `ecs` — `"ECS Fargate + ALB (Always-on containers, zero cold starts — ~$31.28/mo compute+ALB baseline)"`
22
+ * `lambda` — `"AWS Lambda + API Gateway v2 (Scale-to-zero serverless containers — $0.00/mo fixed compute baseline)"`
23
+ * Pass `target` (`'ecs' | 'lambda'`) into `generateInfrastructure`, `renderPreFlightCard`, and `init_run` telemetry (`target: 'ecs' | 'lambda'`).
24
+
25
+ ---
26
+
27
+ ## 2. Container Image & AWS Lambda Web Adapter (`src/templates/dockerfile.js`)
28
+
29
+ * When `target === 'lambda'`, inject the official **AWS Lambda Web Adapter** layer into the generated `Dockerfile` (final runtime stage):
30
+ ```dockerfile
31
+ COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
32
+ ENV AWS_LWA_PORT={{PORT}}
33
+ ENV PORT={{PORT}}
34
+ ```
35
+ * This allows standard HTTP servers (`app.listen(process.env.PORT)`) to handle API Gateway HTTP API v2 payloads transparently on Lambda without changing application code or handlers.
36
+
37
+ ---
38
+
39
+ ## 3. Generated Terraform Topology (`templates/terraform/main-lambda.tf` → `terraform/main.tf`)
40
+
41
+ When `target === 'lambda'`, write `templates/terraform/main-lambda.tf` to `terraform/main.tf`:
42
+
43
+ ### 3.1 Preserved Core Resources (Addon & Command Compatibility)
44
+ * Keep the same resource names for shared primitives so `database.tf` and addons (`s3.tf`, `dynamodb.tf`, `bedrock.tf`, `ses.tf`, `sqs.tf`, `redis.tf`) remain compatible:
45
+ * `aws_ecr_repository.app` and `aws_ecr_lifecycle_policy.app`
46
+ * `aws_iam_role.task_role` (assumed by `["lambda.amazonaws.com", "ecs-tasks.amazonaws.com"]` so all addon `aws_iam_role_policy` attachments targeting `aws_iam_role.task_role.id` work out of the box)
47
+ * `aws_iam_role.execution_role`
48
+ * `aws_secretsmanager_secret.app_env` and `aws_secretsmanager_secret_version.app_env`
49
+ * `aws_vpc.main`, `aws_subnet.public`, `aws_internet_gateway.main`, `aws_route_table.public`, `aws_security_group.ecs_tasks` (kept so RDS/Aurora `database.tf` and `redis.tf` security group references `aws_security_group.ecs_tasks.id` continue to validate cleanly)
50
+ * GitHub OIDC provider & `aws_iam_role.github_actions` (updated to include `lambda:UpdateFunctionCode`, `lambda:GetFunction`, `lambda:GetFunctionConfiguration` alongside existing permissions).
51
+
52
+ ### 3.2 Serverless Compute & Routing Resources
53
+ * **`aws_cloudwatch_log_group.app`:**
54
+ * `name = "/aws/lambda/${local.app_name}-fn"` (or retain `/ecs/${local.app_name}` / alias so `deploy-stack logs` can discover it).
55
+ * `retention_in_days = 30`
56
+ * **`aws_lambda_function.app`:**
57
+ * `function_name = "${local.app_name}-fn"`
58
+ * `role = aws_iam_role.task_role.arn`
59
+ * `architectures = ["x86_64"]`
60
+ * `memory_size = 512`
61
+ * `timeout = 30`
62
+ * `environment { variables = { PORT = tostring(var.container_port), AWS_LWA_PORT = tostring(var.container_port), APP_SECRETS_ARN = aws_secretsmanager_secret.app_env.arn, ... } }`
63
+ * **Day-0 Bootstrap Compatibility:**
64
+ * *Important AWS API note:* AWS Lambda `CreateFunction` with `package_type = "Image"` fails on Day-0 `terraform apply` if the newly created ECR repository has no image yet (`Source image does not exist`), unlike ECS `aws_ecs_task_definition`.
65
+ * Evaluate and use the cleanest Day-0-safe pattern that passes `terraform validate`, `tflint`, and live `apply` before the first `git push` (or document how `apply` / `main-lambda.tf` handles Day-0 creation vs container deployment).
66
+ * **`aws_apigatewayv2_api.main`:**
67
+ * `name = "${local.app_name}-http-api"`
68
+ * `protocol_type = "HTTP"`
69
+ * **`aws_apigatewayv2_integration.lambda`:**
70
+ * `api_id = aws_apigatewayv2_api.main.id`
71
+ * `integration_type = "AWS_PROXY"`
72
+ * `integration_uri = aws_lambda_function.app.invoke_arn`
73
+ * `payload_format_version = "2.0"`
74
+ * **`aws_apigatewayv2_route.default` & `aws_apigatewayv2_stage.default`:**
75
+ * `$default` route forwarding to `integrations/${aws_apigatewayv2_integration.lambda.id}` and `$default` stage with `auto_deploy = true`.
76
+ * **`aws_lambda_permission.apigw`:**
77
+ * Grants `apigateway.amazonaws.com` `lambda:InvokeFunction` on `aws_lambda_function.app.function_name`.
78
+ * **`aws_cloudfront_distribution.cdn`:**
79
+ * Fronts the API Gateway HTTP API domain (`replace(aws_apigatewayv2_api.main.api_endpoint, "https://", "")`) with `origin_protocol_policy = "https-only"` (since API Gateway v2 endpoints are HTTPS-only).
80
+ * **Outputs:**
81
+ * `cloudfront_url = "https://${aws_cloudfront_distribution.cdn.domain_name}"`
82
+ * `api_gateway_url = aws_apigatewayv2_api.main.api_endpoint` (replaces or complements `alb_dns_name`).
83
+
84
+ ---
85
+
86
+ ## 4. CI/CD Deployment Workflow (`templates/github/deploy-lambda.yml` → `.github/workflows/deploy.yml`)
87
+
88
+ * When `target === 'lambda'`, generate `.github/workflows/deploy.yml` from `templates/github/deploy-lambda.yml`:
89
+ * Builds the Docker image (`linux/amd64`) with the AWS Lambda Web Adapter layer and pushes `${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}` and `:latest`.
90
+ * Deploys to AWS Lambda via:
91
+ ```bash
92
+ aws lambda update-function-code \
93
+ --function-name "${PROJECT_NAME}-fn" \
94
+ --image-uri "${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}"
95
+ aws lambda wait function-updated \
96
+ --function-name "${PROJECT_NAME}-fn"
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 5. Pre-Flight Cost & Topology Visualizer (`src/utils/visualizer.js`)
102
+
103
+ * Detect `computeTarget: 'lambda'` in `parseTerraformConfig` (when `terraform/main.tf` contains `resource "aws_lambda_function"` or via `config.target === 'lambda'`) and in `buildConfigFromInit`.
104
+ * When `computeTarget === 'lambda'`:
105
+ * **Topology lines:**
106
+ * Replace `🌐 ALB (Public Entry & Health: ...)` with `🌐 API Gateway HTTP API v2 (Scale-to-zero HTTPS entry)`.
107
+ * Replace `📦 ECS Web Service ...` with `⚡ AWS Lambda Web Service 🟢 <framework> [512 MB · Scale-to-zero]`.
108
+ * **Cost Calculation (`calculateMonthlyCost`):**
109
+ * `fargate`: `$0.00` (for the web service; if a Fargate worker exists, only the worker bills Fargate).
110
+ * `alb`: `$0.00` (API Gateway HTTP API v2 is usage-based at `$1.00` per million requests, `$0.00/mo` fixed baseline).
111
+ * A Lambda + No-DB project shows `Fixed Baseline: ~$0.80/mo (Secrets: $0.80)`.
112
+ * A Lambda + Aurora Serverless v2 (`0 ACU`) project shows `Fixed Baseline: ~$0.80/mo (RDS: $0.00, Secrets: $0.80)`.
113
+
114
+ ---
115
+
116
+ ## 6. Addon & Day-2 Command Compatibility
117
+
118
+ * **`deploy-stack add` (`src/commands/add.js`):**
119
+ * When injecting container environment variables into `terraform/main.tf`, support both ECS (`environment = [` JSON array inside `container_definitions`) and Lambda (`environment { variables = { ... } }` block inside `aws_lambda_function.app`), or keep a shared local/map so `deploy-stack add s3`, `dynamodb`, `bedrock`, `ses`, `redis`, `sqs` work on `--target lambda` projects.
120
+ * For `add cron` on a `--target lambda` project without ECS: either target `aws_lambda_function.app.arn` (`lambda:InvokeFunction`) or fail with a clear message if `cron` requires ECS.
121
+ * **`deploy-stack logs` (`src/commands/logs.js`):**
122
+ * If `terraform/main.tf` is a Lambda target (`parseTerraformConfig(cwd).computeTarget === 'lambda'`), default the CloudWatch log group to `/aws/lambda/${appName}-fn`.
123
+
124
+ ---
125
+
126
+ ## 7. Documentation, Roadmap & Validation
127
+
128
+ 1. **Documentation & Roadmap:**
129
+ * Update `apps/docs/src/content/docs/cli/init.md` and `apps/docs/src/content/docs/guides/architecture.md` to document `--target lambda` and the scale-to-zero Lambda Web Adapter + API Gateway v2 topology.
130
+ * Mark `[x] Serverless Compute Primitives: deploy-stack --target lambda` as completed in `apps/docs/src/content/docs/roadmap.md` (completing 19/19 items in Phase 10!).
131
+ 2. **Tests & IaC Validation:**
132
+ * Add unit tests in `tests/generator.test.js`, `tests/visualizer.test.js`, `tests/headless.test.js`, and `tests/add.test.js` covering `--target lambda`.
133
+ * Add a `target: lambda` project check to `scripts/test-iac.js` and `.github/workflows/iac-validation.yml` so `templates/terraform/main-lambda.tf` is continuously validated with `terraform validate` and `tflint`.
@@ -0,0 +1,31 @@
1
+ # Spec: Health Dashboard & Auto-Diagnostics (`deploy-stack status`)
2
+
3
+ ## Objective
4
+ Serve as the single Day-2 entry point for infrastructure health. Display an instant terminal dashboard of task lifecycle states and CloudWatch alarms. If the infrastructure is healthy, exit cleanly. If a degraded state is detected, automatically transition into the `diagnose` workflow to find the root cause.
5
+
6
+ ## CLI Command & Flags
7
+ \`\`\`bash
8
+ deploy-stack status [options]
9
+ \`\`\`
10
+ - `--region <region>`: Explicit AWS region override.
11
+ - `--json`: Output raw status payload as JSON (disables auto-diagnose).
12
+
13
+ ## Core Checks (Phase 1)
14
+ 1. **ECS Service (`@aws-sdk/client-ecs`):** Fetch `desiredCount`, `runningCount`, and `pendingCount`.
15
+ 2. **CloudWatch Alarms (`@aws-sdk/client-cloudwatch`):** Fetch state of project-prefixed alarms (e.g., `alb_5xx_errors`).
16
+
17
+ ## Terminal UI & The Handoff (Phase 2)
18
+ - Print a clear, color-coded summary block using `picocolors`:
19
+ - **Service:** Name, Status (`ACTIVE`).
20
+ - **Replicas:** `runningCount / desiredCount` (green if matching, yellow if pending, red if 0 running).
21
+ - **Alarms:** List configured alarms with current state badges (`[OK]`, `[ALARM]`).
22
+ - **The Trigger:** If `runningCount < desiredCount` (after a brief grace period) OR any alarm is in the `ALARM` state:
23
+ 1. Print: `⚠️ Degraded state detected. Running automated diagnostics...`
24
+ 2. Automatically invoke the existing `runDiagnose()` function from `src/commands/diagnose.js`.
25
+ 3. Exit with code 1 after diagnostics complete.
26
+
27
+ ## Testing & Stability Requirements
28
+ - Add unit tests in `tests/status.test.js`.
29
+ - Mock ECS and CloudWatch clients.
30
+ - Verify status calculation for healthy states (clean exit, code 0).
31
+ - Verify the auto-diagnose handoff triggers correctly during a crash loop or 5XX alarm state.
@@ -0,0 +1,69 @@
1
+ # Spec: Telemetry Identity Separation, Context Alignment & Binary Check Deduplication
2
+
3
+ ## Overview
4
+ Resolve three telemetry and process-spawning issues observed in live PostHog exports:
5
+ 1. **Machine-Scoped `distinct_id` vs. Project-Scoped `project_id` (`src/core/telemetry.js`):**
6
+ * Previously, `distinct_id` hashed `properties.projectName || 'unknown'`, causing commands that omitted `projectName` (`doctor_run` and early `add_run` guards) to hash `'unknown'` (`b23a6a8439c0dde5`) and merge unrelated users across geographies into one PostHog Person.
7
+ 2. **Consistent `is_ci` & `cli_command` Context (`src/core/telemetry.js`):**
8
+ * Align `is_ci` with `ci_provider` so unsetting `CI` while leaving `GITHUB_ACTIONS` set never produces `is_ci: false` alongside `ci_provider: 'github_actions'`.
9
+ * Guard `cli_command` on `is_cli_entry` so programmatic module imports report `'module_import'` while preserving multi-word CLI command names (`db connect`, `secrets push`) on real CLI invocations.
10
+ 3. **Binary Check Deduplication (`src/utils/aws.js`, `src/commands/doctor.js`):**
11
+ * Prevent redundant child-process spawns when `hasAwsCli()` or `runDoctor()` is called ~32 times in rapid succession inside PID-limited containers, while keeping unit tests isolated.
12
+
13
+ ---
14
+
15
+ ## Part 1: Machine-Scoped `distinct_id`, Project-Scoped `project_id` & Test Seams (`src/core/telemetry.js`)
16
+
17
+ 1. **`distinct_id` Resolution & 16-Hex Format:**
18
+ * `distinct_id` must always be a 16-character lowercase hexadecimal SHA-256 prefix (`sha256(rawIdentity)[:16]`), and must never hash `'unknown'` or a bare project folder name.
19
+ * **Test Seam & Cache Reset:**
20
+ * Export a `resetTelemetryIdentityCache()` function that clears the in-memory cached `distinct_id` between unit tests.
21
+ * Support an environment variable override `process.env.DEPLOY_STACK_TELEMETRY_ID_PATH` for the persistent identity file path (defaulting to `path.join(os.homedir(), '.deploy-stack', 'telemetry-id')`).
22
+ * **Resolution Precedence (Cached in Memory Once Resolved):**
23
+ * **Explicit Override Precedence:** If `process.env.DEPLOY_STACK_TELEMETRY_ID_PATH` is explicitly set and non-empty, always take **Branch B (Persistent File)** using that path—even when `is_ci` or `is_test_env` is `true`. This guarantees persistent-file unit tests run deterministically both locally and inside GitHub Actions (`CI=true`).
24
+ * **Branch A — Ephemeral / CI / Unconfigured Test Environment:** When `DEPLOY_STACK_TELEMETRY_ID_PATH` is not explicitly set and either `is_ci` is `true` or `is_test_env` is `true`, compute `distinct_id` by hashing a colon-delimited string of exact machine/CI attributes: `os.hostname()`, `os.platform()`, `os.arch()`, `process.env.GITHUB_REPOSITORY || ''`, `process.env.GITHUB_RUN_ID || ''`, and `process.env.GITLAB_PROJECT_PATH || ''`. Do **not** include `process.cwd()` in `distinct_id` so multiple commands run from different directories on the same runner share one Person within a run.
25
+ * **Branch B — Persistent File Environment:** When `DEPLOY_STACK_TELEMETRY_ID_PATH` is explicitly set, or when neither `is_ci` nor `is_test_env` is `true`, attempt to read the trimmed UUID string from the identity file path. If the file does not exist or is empty, generate a `crypto.randomUUID()`, create the parent directory (`recursive: true`), and write the UUID to the file. Hash the read or newly generated UUID string with SHA-256 (16-hex prefix) and cache it in memory. If reading or writing throws any filesystem error, fall back cleanly to Branch A and cache that result.
26
+ 2. **`project_id` Property Derivation:**
27
+ * Keep the existing `String(...)` coercion on `properties.projectName` (so numeric inputs like `42` coerce to `'42'`), delete `projectName` from the outgoing event properties, and attach `project_id` set to the 16-character SHA-256 hex prefix of the resolved project name string.
28
+ * When `properties.projectName` is `undefined`, `null`, or empty string `''`, derive the fallback string from `path.basename(process.cwd())`; if that basename is also empty (e.g., `/`), use `'unknown'`.
29
+ * Update the existing `tests/telemetry.test.js` test that previously asserted `distinct_id === sha256('42')[:16]` so it now asserts `properties.project_id === sha256('42')[:16]` and verifies `distinct_id` is a stable 16-hex machine ID not equal to `sha256('unknown')[:16]`.
30
+
31
+ ---
32
+
33
+ ## Part 2: Telemetry Context Alignment (`src/core/telemetry.js`)
34
+
35
+ 1. **Boolean Env Helper & `is_ci` / `ci_provider` Consistency:**
36
+ * Treat an environment variable as active/truthy only when it is defined and its trimmed lowercase value is not `''`, `'0'`, or `'false'`.
37
+ * Apply this check in `detectCiProvider()` as well (fixing the pre-existing quirk where `GITHUB_ACTIONS='false'` matched `'github_actions'`).
38
+ * Compute `ci_provider` first, then set `is_ci` to `true` if `process.env.CI` passes the truthy check **or** `ci_provider !== 'none'`.
39
+ 2. **`cli_command` Precedence Guarded by `is_cli_entry`:**
40
+ * Compute `is_cli_entry` first (`['cli.js', 'deploy-stack'].includes(path.basename(process.argv?.[1] || ''))`).
41
+ * When `is_cli_entry` is `true`, preserve the existing precedence: use `CLI_COMMAND` (which captures two-word commands like `db connect` and `secrets push`) falling back to `process.argv.slice(2).join(' ') || 'unknown'`.
42
+ * When `is_cli_entry` is `false`, set `cli_command` to `'module_import'`.
43
+
44
+ ---
45
+
46
+ ## Part 3: Binary Pre-Flight Caching & Deduplication (`src/utils/aws.js` & `src/commands/doctor.js`)
47
+
48
+ 1. **Synchronous `hasAwsCli(options = {})` Result Caching (`src/utils/aws.js`):**
49
+ * Preserve the exact existing signature `hasAwsCli(options = {})` where callers pass `options.spawnSyncImpl` (used across `src/commands/exec.js`, `src/commands/db.js`, `tests/exec.test.js`, and `tests/db.test.js`).
50
+ * **Bypass Cache on Custom `options.spawnSyncImpl`:** Only read/populate the module-level boolean cache when `options.spawnSyncImpl` is not provided (`options.spawnSyncImpl === undefined`). Whenever a caller or test passes a custom `options.spawnSyncImpl` (such as `tests/exec.test.js` calling `hasAwsCli({ spawnSyncImpl: mockSpawn })` three times expecting `false`/`true`/`false`), execute `options.spawnSyncImpl` directly without reading or writing the cache.
51
+ * For default calls (`options.spawnSyncImpl === undefined`), cache the boolean result for `5000` ms (`AWS_CLI_CACHE_TTL_MS = 5000`) so 32 rapid calls in a container without injected mocks spawn `aws --version` only once instead of 32 times.
52
+ * Export `resetAwsCliCache()` to allow explicit cache clearing in unit tests.
53
+ 2. **In-Flight `checkDependency` Deduplication in `runDoctor()` (`src/commands/doctor.js`):**
54
+ * Keep the deduplication scoped to `src/commands/doctor.js` (leaving sequential single calls in `init.js` and `destroy.js` untouched).
55
+ * Maintain a module-scoped `Map` of in-flight promises keyed by the binary name (`cmd`). Perform the `Map` lookup and `.set(cmd, promise)` synchronously before any `await` in the wrapper, and remove the key in `.finally(() => inFlight.delete(cmd))` once the promise settles.
56
+
57
+ ---
58
+
59
+ ## Part 4: Unit Tests (`tests/telemetry.test.js`, `tests/doctor.test.js`, `tests/aws.test.js`)
60
+
61
+ 1. **`tests/telemetry.test.js`:**
62
+ * Call `resetTelemetryIdentityCache()` in `beforeEach`.
63
+ * Update the numeric `projectName: 42` test to assert `properties.project_id === sha256('42')[:16]` and verify `distinct_id` is a 16-hex string that stays identical across events with and without `projectName` and never equals `b23a6a8439c0dde5` (`sha256('unknown')[:16]`).
64
+ * Test persistent file identity using a temporary directory via `DEPLOY_STACK_TELEMETRY_ID_PATH` (verifying that a UUID file is created and subsequent calls after `resetTelemetryIdentityCache()` produce the same `distinct_id`, even when `CI=true`).
65
+ * Verify `is_ci` is `true` when `CI` is unset (`delete process.env.CI`) and `GITHUB_ACTIONS='true'`, and `false` when `CI='false'` and `GITHUB_ACTIONS='false'`.
66
+ * Verify `cli_command` is `'module_import'` when `is_cli_entry` is `false`, and preserves `CLI_COMMAND` / subcommand args when `is_cli_entry` is `true`.
67
+ 2. **`tests/doctor.test.js` & `tests/aws.test.js`:**
68
+ * In `tests/doctor.test.js`, verify that calling `runDoctor()` 5 times concurrently via `Promise.all` invokes `checkDependency` only once per binary (4 calls total, not 20) while the promises are in flight, and invokes it fresh on a subsequent sequential call.
69
+ * In `tests/aws.test.js`, use a `child_process` module mock (`vi.mock`) to verify that repeated default `hasAwsCli()` calls within the TTL spawn `aws --version` only once and reuse the cached boolean until `resetAwsCliCache()` is called, while leaving `tests/exec.test.js` undisturbed.
@@ -0,0 +1,35 @@
1
+ # Spec: Telemetry Observability & Schema Protection
2
+
3
+ ## Overview
4
+ Recent PostHog exports showed an automated analyzer importing `deploy-stack` modules directly and calling `trackEvent` with primitive values (e.g., `trackEvent(true, "test")`). We want to **keep all events** (do not drop unknown or fuzzer-generated events, as they provide valuable intelligence), while preventing primitive string/array arguments from polluting the PostHog column schema via object spreading or crashing `crypto.createHash`. We also want to add invocation context tags (`is_tty`, `is_cli_entry`).
5
+
6
+ ---
7
+
8
+ ## Part 1: Capture & Normalize in `src/core/telemetry.js`
9
+
10
+ ### 1. Remove the Event Allowlist & Normalize `eventName`
11
+ * Remove the allowlist filtering in `trackEvent(eventName, properties)` so that all events (including unexpected or fuzzer-generated event names like `'test'`, `'data'`, `true`, or `1`) are captured and sent to PostHog.
12
+ * Only return early if `process.env.DO_NOT_TRACK` is enabled or if `eventName === undefined || eventName === null || String(eventName).trim() === ''`.
13
+ * Otherwise, normalize the event name via `const normalizedEvent = String(eventName);` so booleans or numbers are safely serialized.
14
+
15
+ ### 2. Protect Schema by Wrapping Non-Object `properties` & Safe Project Name Hashing
16
+ * Check if `properties` is a plain object:
17
+ * If `properties === undefined || properties === null`, treat it as `{}`.
18
+ * If `typeof properties === 'object' && !Array.isArray(properties)`, spread it normally.
19
+ * If `properties` is a primitive (string, number, boolean) or an Array, preserve the data without spreading by wrapping it as `{ raw_properties: properties }`.
20
+ * **Safe Project Name Coercion:** Before passing `rawProjectName` to `crypto.createHash('sha256').update(...)`, coerce it via `String(rawProjectName ?? 'unknown')` so non-string `projectName` properties (e.g., `{ projectName: 42 }`) never throw a `TypeError`.
21
+
22
+ ### 3. Invocation Context Properties
23
+ Add two boolean properties to the default telemetry payload (alongside `is_ci` and `is_test_env`) so PostHog dashboards can segment direct module imports from interactive CLI runs:
24
+ * `is_tty`: `Boolean(process.stdout && process.stdout.isTTY)`
25
+ * `is_cli_entry`: Check the basename of `process.argv[1]` using `path.basename`:
26
+ `Boolean(process.argv && typeof process.argv[1] === 'string' && ['cli.js', 'deploy-stack'].includes(path.basename(process.argv[1])))`
27
+
28
+ ---
29
+
30
+ ## Part 2: Test Requirements (`tests/telemetry.test.js`)
31
+
32
+ * Update the existing allowlist drop-tests to verify that arbitrary event names (e.g., `'test'`, `true`, `1`) **are** sent to PostHog with `event: String(eventName)`.
33
+ * Verify that passing a primitive or array as `properties` (e.g., `trackEvent('test', 'test')` or `trackEvent('test', ['a', 'b'])`) sends `{ raw_properties: ... }` and does **not** create indexed keys (`0`, `1`, `2`, `3`).
34
+ * Verify that passing a non-string `projectName` (e.g., `trackEvent('test', { projectName: 42 })`) hashes `'42'` cleanly without throwing.
35
+ * Verify that events include boolean `is_tty` and `is_cli_entry` properties (asserting `typeof === 'boolean'` and testing `is_cli_entry` with stubbed `process.argv[1]`).