grada-run 0.0.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,500 @@
1
+ // Project capability scanner for dependency-aware `init`.
2
+ //
3
+ // Pure, read-only, and fault-tolerant: every file read is guarded so
4
+ // missing or malformed manifests never crash scaffolding. Matching is
5
+ // tokenized (exact dependency keys, exact module paths, exact gem names,
6
+ // exact env keys) so short tokens like `pg` or `cron` never fire on
7
+ // substrings. Environment files contribute KEY NAMES ONLY — values are
8
+ // never read, parsed, or logged.
9
+ import fsSync from 'fs';
10
+ import path from 'path';
11
+ import { readFileSafe } from './resolvers.js';
12
+ import { ADDON_REGISTRY } from './addons.js';
13
+ import { parseDockerCompose } from './dockerCompose.js';
14
+ import { detectMigrationCommand } from './detector.js';
15
+
16
+ // --- Manifest tokenizers ---
17
+
18
+ function readJsonSafe(cwd, relPath) {
19
+ try {
20
+ const raw = readFileSafe(path.join(cwd, relPath));
21
+ if (!raw) return null;
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // Python ecosystem names compare lowercased with `_` normalized to `-`
29
+ // (`APScheduler` -> `apscheduler`, `psycopg2-binary` stays).
30
+ function normalizePyName(raw) {
31
+ return String(raw || '').trim().toLowerCase().replace(/_/g, '-');
32
+ }
33
+
34
+ // One normalized package name per line (`name`, `name[extra]==1.0`,
35
+ // `name @ https://...`). Skips comments, includes, and flags.
36
+ function extractRequirementNames(text) {
37
+ const names = new Set();
38
+ for (const line of String(text || '').split('\n')) {
39
+ const trimmed = line.trim();
40
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
41
+ const match = /^[a-zA-Z0-9._-]+/.exec(trimmed);
42
+ if (match) names.add(normalizePyName(match[0]));
43
+ }
44
+ return names;
45
+ }
46
+
47
+ function extractPyprojectNames(text) {
48
+ const names = new Set();
49
+ const lines = String(text || '').split('\n');
50
+ let section = '';
51
+ let inDepsArray = false;
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ const sectionMatch = /^\[(.+)\]$/.exec(trimmed);
55
+ if (sectionMatch) {
56
+ section = sectionMatch[1].trim();
57
+ inDepsArray = false;
58
+ continue;
59
+ }
60
+ if (/^dependencies\s*=/.test(trimmed)) {
61
+ inDepsArray = true;
62
+ if (trimmed.includes(']')) inDepsArray = false;
63
+ } else if (inDepsArray && trimmed.includes(']')) {
64
+ inDepsArray = false;
65
+ }
66
+ if (inDepsArray || section === 'project') {
67
+ const quoted = trimmed.match(/"([^"]+)"/) || trimmed.match(/'([^']+)'/);
68
+ if (quoted && (inDepsArray || /^dependencies\s*=/.test(trimmed))) {
69
+ const token = /^[a-zA-Z0-9._-]+/.exec(quoted[1].trim());
70
+ if (token) names.add(normalizePyName(token[0]));
71
+ }
72
+ }
73
+ if (section.startsWith('tool.poetry')) {
74
+ const key = /^([a-zA-Z0-9._-]+)\s*=/.exec(trimmed);
75
+ if (key && key[1].toLowerCase() !== 'python') names.add(normalizePyName(key[1]));
76
+ }
77
+ }
78
+ return names;
79
+ }
80
+
81
+ function extractPipfileNames(text) {
82
+ const names = new Set();
83
+ let section = '';
84
+ for (const line of String(text || '').split('\n')) {
85
+ const trimmed = line.trim();
86
+ const sectionMatch = /^\[(.+)\]$/.exec(trimmed);
87
+ if (sectionMatch) {
88
+ section = sectionMatch[1].trim().toLowerCase();
89
+ continue;
90
+ }
91
+ if (section !== 'packages' && section !== 'dev-packages') continue;
92
+ if (!trimmed || trimmed.startsWith('#')) continue;
93
+ const key = /^([a-zA-Z0-9._-]+)\s*=/.exec(trimmed);
94
+ if (key) names.add(normalizePyName(key[1]));
95
+ }
96
+ return names;
97
+ }
98
+
99
+ // Exact module paths: the first whitespace-separated token per line,
100
+ // keeping only module-path-shaped tokens (containing `.` or `/`).
101
+ function extractGoModules(text) {
102
+ const modules = new Set();
103
+ for (const line of String(text || '').split('\n')) {
104
+ const trimmed = line.trim();
105
+ if (!trimmed || trimmed.startsWith('//')) continue;
106
+ const token = trimmed.split(/\s+/)[0];
107
+ if (token && (token.includes('.') || token.includes('/'))) modules.add(token);
108
+ }
109
+ return modules;
110
+ }
111
+
112
+ function extractGemNames(text) {
113
+ const names = new Set();
114
+ const pattern = /^\s*gem\s+['"]([^'"]+)['"]/gm;
115
+ let match = pattern.exec(String(text || ''));
116
+ while (match) {
117
+ names.add(match[1].trim());
118
+ match = pattern.exec(String(text || ''));
119
+ }
120
+ return names;
121
+ }
122
+
123
+ // Variable key names only (`KEY`, `export KEY`, comments skipped). Values
124
+ // after `=` are never captured, so secrets cannot leak into evidence.
125
+ function extractEnvKeys(text) {
126
+ const keys = new Set();
127
+ for (const line of String(text || '').split('\n')) {
128
+ let trimmed = line.trim();
129
+ if (!trimmed || trimmed.startsWith('#')) continue;
130
+ trimmed = trimmed.replace(/^export\s+/, '');
131
+ const match = /^([A-Za-z0-9_]+)\s*=/.exec(trimmed);
132
+ if (match) keys.add(match[1].toUpperCase());
133
+ }
134
+ return keys;
135
+ }
136
+
137
+ function stripImageTag(image) {
138
+ const ref = String(image || '').trim().toLowerCase();
139
+ const lastSlash = ref.lastIndexOf('/');
140
+ const lastColon = ref.lastIndexOf(':');
141
+ if (lastColon > lastSlash) return ref.slice(0, lastColon);
142
+ return ref;
143
+ }
144
+
145
+ function imageMatches(image, signal) {
146
+ const repo = stripImageTag(image);
147
+ return repo === signal || repo.endsWith(`/${signal}`);
148
+ }
149
+
150
+ // --- Signal matrix (canonical evidence tokens) ---
151
+
152
+ const NODE_SIGNALS = {
153
+ relationalDb: ['pg', 'postgres', 'typeorm', 'sequelize', 'knex', 'mikro-orm', 'drizzle-orm', 'drizzle-kit', '@prisma/client', 'prisma'],
154
+ 'db:redis': ['ioredis', 'redis', '@upstash/redis', 'bull', 'bullmq'],
155
+ 'queue:sqs': ['@aws-sdk/client-sqs', 'sqs-consumer'],
156
+ 'storage:s3': ['@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner', 'multer-s3'],
157
+ 'db:dynamodb': ['@aws-sdk/client-dynamodb', '@aws-sdk/lib-dynamodb', 'dynamoose'],
158
+ 'ai:bedrock': ['@aws-sdk/client-bedrock-runtime', '@aws-sdk/client-bedrock', '@ai-sdk/amazon-bedrock', '@langchain/aws'],
159
+ 'email:ses': ['@aws-sdk/client-ses', '@aws-sdk/client-sesv2'],
160
+ };
161
+ const NODE_MYSQL = ['mysql2', 'mysql'];
162
+ const NODE_CRON = ['node-cron', 'cron', 'agenda'];
163
+ const NODE_WORKER_DEPS = ['bullmq', 'bull', 'sqs-consumer'];
164
+
165
+ const PYTHON_SIGNALS = {
166
+ relationalDb: ['psycopg2', 'psycopg2-binary', 'psycopg', 'asyncpg', 'sqlalchemy', 'sqlmodel', 'alembic', 'django'],
167
+ 'db:redis': ['redis', 'aioredis', 'rq'],
168
+ 'storage:s3': ['django-storages', 's3fs'],
169
+ 'db:dynamodb': ['pynamodb', 'aioboto3'],
170
+ 'ai:bedrock': ['langchain-aws'],
171
+ 'email:ses': ['django-ses'],
172
+ };
173
+ const PYTHON_MYSQL = ['pymysql', 'mysqlclient', 'aiomysql'];
174
+ const PYTHON_CRON = ['apscheduler', 'celery-beat'];
175
+ const PYTHON_WORKER_DEPS = ['celery', 'rq', 'dramatiq'];
176
+
177
+ const GO_SIGNALS = {
178
+ relationalDb: ['github.com/lib/pq', 'github.com/jackc/pgx', 'gorm.io/driver/postgres'],
179
+ 'db:redis': ['github.com/redis/go-redis', 'github.com/gomodule/redigo'],
180
+ 'queue:sqs': ['github.com/aws/aws-sdk-go-v2/service/sqs'],
181
+ 'storage:s3': ['github.com/aws/aws-sdk-go-v2/service/s3'],
182
+ 'db:dynamodb': ['github.com/aws/aws-sdk-go-v2/service/dynamodb'],
183
+ 'ai:bedrock': ['github.com/aws/aws-sdk-go-v2/service/bedrockruntime'],
184
+ 'email:ses': ['github.com/aws/aws-sdk-go-v2/service/ses', 'github.com/aws/aws-sdk-go-v2/service/sesv2'],
185
+ };
186
+ const GO_MYSQL = ['github.com/go-sql-driver/mysql', 'gorm.io/driver/mysql'];
187
+
188
+ const RUBY_SIGNALS = {
189
+ relationalDb: ['pg', 'rails'],
190
+ 'db:redis': ['redis', 'sidekiq', 'connection_pool'],
191
+ 'queue:sqs': ['aws-sdk-sqs', 'shoryuken'],
192
+ 'storage:s3': ['aws-sdk-s3', 'shrine', 'carrierwave'],
193
+ 'db:dynamodb': ['aws-sdk-dynamodb'],
194
+ 'ai:bedrock': ['aws-sdk-bedrockruntime'],
195
+ 'email:ses': ['aws-sdk-ses', 'aws-sdk-sesv2'],
196
+ };
197
+ const RUBY_MYSQL = ['mysql2'];
198
+ const RUBY_CRON = ['whenever', 'sidekiq-cron', 'sidekiq-scheduler'];
199
+ const RUBY_WORKER_DEPS = ['sidekiq', 'shoryuken', 'good_job'];
200
+
201
+ const ENV_KEY_SIGNALS = {
202
+ relationalDb: ['DATABASE_URL', 'POSTGRES_URL', 'POSTGRES_PRISMA_URL', 'PGHOST'],
203
+ 'db:redis': ['REDIS_URL', 'VALKEY_URL', 'REDIS_HOST', 'CELERY_BROKER_URL'],
204
+ 'queue:sqs': ['SQS_QUEUE_URL', 'SQS_DLQ_URL', 'AWS_SQS_QUEUE_URL'],
205
+ 'storage:s3': ['S3_BUCKET_NAME', 'S3_BUCKET', 'AWS_S3_BUCKET', 'S3_CDN_URL'],
206
+ 'db:dynamodb': ['DYNAMODB_TABLE_NAME', 'DYNAMODB_TABLE'],
207
+ 'ai:bedrock': ['BEDROCK_MODEL_ID', 'AWS_BEDROCK_MODEL_ID'],
208
+ 'email:ses': ['SES_FROM_EMAIL', 'SES_REGION', 'AWS_SES_REGION'],
209
+ };
210
+ const ENV_MYSQL_KEYS = ['MYSQL_URL', 'MYSQL_HOST'];
211
+
212
+ const COMPOSE_IMAGE_SIGNALS = {
213
+ relationalDb: ['postgres', 'postgis/postgis'],
214
+ 'db:redis': ['redis', 'valkey/valkey', 'bitnami/redis', 'redis/redis-stack'],
215
+ 'storage:s3': ['minio/minio'],
216
+ 'db:dynamodb': ['amazon/dynamodb-local'],
217
+ };
218
+ const COMPOSE_MYSQL_IMAGES = ['mysql', 'mariadb'];
219
+ const COMPOSE_VECTOR_IMAGES = ['ankane/pgvector', 'pgvector/pgvector'];
220
+ const COMPOSE_SQS_IMAGE = 'roribio16/alpine-sqs';
221
+ const COMPOSE_LOCALSTACK_IMAGE = 'localstack/localstack';
222
+
223
+ const DRIZZLE_CONFIG_FILES = ['drizzle.config.ts', 'drizzle.config.js', 'drizzle.config.mjs'];
224
+ const ENV_TEMPLATE_FILES = ['.env.example', '.env.sample', '.env.template', '.env'];
225
+ const NON_PG_PRISMA_PROVIDERS = ['mysql', 'sqlite', 'mongodb'];
226
+ const PG_PRISMA_PROVIDERS = ['postgresql', 'postgres', 'cockroachdb'];
227
+
228
+ function pushUnique(list, item) {
229
+ if (!list.includes(item)) list.push(item);
230
+ }
231
+
232
+ function collectSetSignals(names, signals, buckets) {
233
+ for (const [bucket, tokens] of Object.entries(signals)) {
234
+ for (const token of tokens) {
235
+ if (names.has(token)) pushUnique(buckets[bucket], token);
236
+ }
237
+ }
238
+ }
239
+
240
+ function collectGoSignals(modules, signals, buckets) {
241
+ for (const [bucket, tokens] of Object.entries(signals)) {
242
+ for (const token of tokens) {
243
+ for (const module of modules) {
244
+ // Exact module path, or a suffix match so vendored paths
245
+ // like `github.com/robfig/cron` still resolve.
246
+ if (module === token || module.endsWith(`/${token}`)) {
247
+ pushUnique(buckets[bucket], token);
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ function fileExists(cwd, relPath) {
256
+ try {
257
+ return fsSync.existsSync(path.join(cwd, relPath));
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ function detectNpmScriptWorker(pkg) {
264
+ const scripts = pkg && typeof pkg.scripts === 'object' && pkg.scripts !== null ? pkg.scripts : {};
265
+ const entries = Object.entries(scripts);
266
+ const tokenPattern = /worker|queue:work|bull/i;
267
+ for (const [name] of entries) {
268
+ if (tokenPattern.test(String(name))) return `npm run ${name}`;
269
+ }
270
+ for (const [name, body] of entries) {
271
+ if (tokenPattern.test(String(body))) return `npm run ${name}`;
272
+ }
273
+ return null;
274
+ }
275
+
276
+ // Scans manifests, configs, and env templates under `cwd` and returns
277
+ // `{ relationalDb, worker, migration, addons, upcomingHints }` (see the
278
+ // dependency-aware-init spec for the full shape). Never throws on
279
+ // malformed input and never captures secret values.
280
+ export function detectProjectCapabilities(cwd = process.cwd()) {
281
+ const dir = cwd || process.cwd();
282
+ const relationalEvidence = [];
283
+ const mysqlEvidence = [];
284
+ const workerEvidence = [];
285
+ const addonEvidence = {};
286
+ for (const cap of Object.keys(ADDON_REGISTRY)) addonEvidence[cap] = [];
287
+ let cronHint = false;
288
+ let vectorHint = false;
289
+
290
+ const recordAddon = (cap, item) => {
291
+ if (addonEvidence[cap]) pushUnique(addonEvidence[cap], item);
292
+ };
293
+ const recordRelational = (item) => pushUnique(relationalEvidence, item);
294
+ const recordMysql = (item) => pushUnique(mysqlEvidence, item);
295
+ const recordWorker = (item) => pushUnique(workerEvidence, item);
296
+
297
+ // --- package.json (exact dependency keys) ---
298
+ const pkg = readJsonSafe(dir, 'package.json');
299
+ const nodeDeps = new Set();
300
+ if (pkg && typeof pkg === 'object') {
301
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
302
+ const block = pkg[field];
303
+ if (block && typeof block === 'object') {
304
+ for (const name of Object.keys(block)) nodeDeps.add(name);
305
+ }
306
+ }
307
+ }
308
+
309
+ // Prisma provider can disqualify the Prisma relational signals.
310
+ const prismaSchema = readFileSafe(path.join(dir, 'prisma', 'schema.prisma'));
311
+ let prismaProvider = null;
312
+ if (prismaSchema) {
313
+ const providerMatch = /provider\s*=\s*"([^"]+)"/.exec(prismaSchema);
314
+ if (providerMatch) prismaProvider = providerMatch[1].trim().toLowerCase();
315
+ if (/vector/i.test(prismaSchema)) vectorHint = true;
316
+ }
317
+ const prismaCountsAsRelational = !prismaProvider || !NON_PG_PRISMA_PROVIDERS.includes(prismaProvider);
318
+ if (prismaSchema && prismaProvider && PG_PRISMA_PROVIDERS.includes(prismaProvider)) {
319
+ recordRelational('prisma/schema.prisma');
320
+ }
321
+ if (prismaSchema && prismaProvider === 'mysql') recordMysql('prisma/schema.prisma');
322
+
323
+ const buckets = { relationalDb: relationalEvidence, ...addonEvidence };
324
+ collectSetSignals(nodeDeps, NODE_SIGNALS, buckets);
325
+ if (!prismaCountsAsRelational) {
326
+ for (const prismaDep of ['@prisma/client', 'prisma']) {
327
+ const at = relationalEvidence.indexOf(prismaDep);
328
+ if (at !== -1) relationalEvidence.splice(at, 1);
329
+ }
330
+ }
331
+ for (const token of NODE_MYSQL) {
332
+ if (nodeDeps.has(token)) recordMysql(token);
333
+ }
334
+ for (const token of NODE_CRON) {
335
+ if (nodeDeps.has(token)) cronHint = true;
336
+ }
337
+ for (const token of NODE_WORKER_DEPS) {
338
+ if (nodeDeps.has(token)) recordWorker(token);
339
+ }
340
+ if (nodeDeps.has('pgvector')) vectorHint = true;
341
+
342
+ // --- Python manifests (normalized names) ---
343
+ const pyNames = new Set();
344
+ for (const name of extractRequirementNames(readFileSafe(path.join(dir, 'requirements.txt')))) pyNames.add(name);
345
+ for (const name of extractPyprojectNames(readFileSafe(path.join(dir, 'pyproject.toml')))) pyNames.add(name);
346
+ for (const name of extractPipfileNames(readFileSafe(path.join(dir, 'Pipfile')))) pyNames.add(name);
347
+ collectSetSignals(pyNames, PYTHON_SIGNALS, buckets);
348
+ for (const token of PYTHON_MYSQL) {
349
+ if (pyNames.has(token)) recordMysql(token);
350
+ }
351
+ for (const token of PYTHON_CRON) {
352
+ if (pyNames.has(token)) cronHint = true;
353
+ }
354
+ for (const token of PYTHON_WORKER_DEPS) {
355
+ if (pyNames.has(token)) recordWorker(token);
356
+ }
357
+ if (pyNames.has('celery') && pyNames.has('redis')) recordAddon('db:redis', 'celery');
358
+ if (pyNames.has('pgvector')) vectorHint = true;
359
+
360
+ // --- go.mod (exact module paths) ---
361
+ const goModules = extractGoModules(readFileSafe(path.join(dir, 'go.mod')));
362
+ collectGoSignals(goModules, GO_SIGNALS, buckets);
363
+ for (const token of GO_MYSQL) {
364
+ for (const module of goModules) {
365
+ if (module === token || module.endsWith(`/${token}`)) {
366
+ recordMysql(token);
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ for (const module of goModules) {
372
+ if (module === 'robfig/cron' || module.endsWith('/robfig/cron')) cronHint = true;
373
+ if (module.includes('pgvector')) vectorHint = true;
374
+ }
375
+
376
+ // --- Gemfile (exact gem names) ---
377
+ const gemNames = extractGemNames(readFileSafe(path.join(dir, 'Gemfile')));
378
+ collectSetSignals(gemNames, RUBY_SIGNALS, buckets);
379
+ for (const token of RUBY_MYSQL) {
380
+ if (gemNames.has(token)) recordMysql(token);
381
+ }
382
+ for (const token of RUBY_CRON) {
383
+ if (gemNames.has(token)) cronHint = true;
384
+ }
385
+ for (const token of RUBY_WORKER_DEPS) {
386
+ if (gemNames.has(token)) recordWorker(token);
387
+ }
388
+ if (gemNames.has('pgvector')) vectorHint = true;
389
+
390
+ // --- Framework marker files ---
391
+ for (const file of DRIZZLE_CONFIG_FILES) {
392
+ if (fileExists(dir, file)) recordRelational(file);
393
+ }
394
+ if (fileExists(dir, 'alembic.ini')) recordRelational('alembic.ini');
395
+ if (fileExists(dir, 'manage.py')) recordRelational('manage.py');
396
+ if (fileExists(dir, path.join('bin', 'rails'))) recordRelational('bin/rails');
397
+
398
+ // --- Docker Compose (images, worker-ish services, localstack) ---
399
+ let composeServices = null;
400
+ try {
401
+ composeServices = parseDockerCompose(dir);
402
+ } catch {
403
+ composeServices = null;
404
+ }
405
+ if (Array.isArray(composeServices)) {
406
+ for (const service of composeServices) {
407
+ if (!service || typeof service !== 'object') continue;
408
+ const image = typeof service.image === 'string' ? service.image : '';
409
+ for (const [bucket, signals] of Object.entries(COMPOSE_IMAGE_SIGNALS)) {
410
+ for (const signal of signals) {
411
+ if (image && imageMatches(image, signal)) {
412
+ const item = `docker-compose ${signal}`;
413
+ if (bucket === 'relationalDb') recordRelational(item);
414
+ else recordAddon(bucket, item);
415
+ }
416
+ }
417
+ }
418
+ for (const signal of COMPOSE_MYSQL_IMAGES) {
419
+ if (image && imageMatches(image, signal)) recordMysql(`docker-compose ${signal}`);
420
+ }
421
+ for (const signal of COMPOSE_VECTOR_IMAGES) {
422
+ if (image && imageMatches(image, signal)) vectorHint = true;
423
+ }
424
+ if (image && imageMatches(image, COMPOSE_SQS_IMAGE)) {
425
+ recordAddon('queue:sqs', `docker-compose ${COMPOSE_SQS_IMAGE}`);
426
+ }
427
+ if (image && imageMatches(image, COMPOSE_LOCALSTACK_IMAGE)) {
428
+ const env = service.environment && typeof service.environment === 'object' ? service.environment : {};
429
+ const servicesValue = env.SERVICES;
430
+ if (servicesValue === undefined || servicesValue === null) {
431
+ recordAddon('queue:sqs', 'docker-compose localstack');
432
+ } else {
433
+ const tokens = String(servicesValue).toLowerCase().split(/[,\s:;]+/).filter(Boolean);
434
+ if (tokens.includes('sqs')) recordAddon('queue:sqs', 'docker-compose localstack');
435
+ }
436
+ }
437
+ const serviceName = typeof service.name === 'string' ? service.name : '';
438
+ if (serviceName.toLowerCase().includes('worker')) recordWorker('docker-compose worker');
439
+ }
440
+ }
441
+
442
+ // --- Procfile worker processes ---
443
+ const procfileContent = readFileSafe(path.join(dir, 'Procfile'));
444
+ if (procfileContent) {
445
+ for (const line of procfileContent.split('\n')) {
446
+ const match = /^\s*([A-Za-z0-9_-]+)\s*:/.exec(line);
447
+ if (match && match[1].toLowerCase() !== 'web' && match[1].toLowerCase().includes('worker')) {
448
+ recordWorker('Procfile');
449
+ break;
450
+ }
451
+ }
452
+ }
453
+
454
+ // --- npm script worker suggestion ---
455
+ const suggestedCommand = detectNpmScriptWorker(pkg);
456
+ if (suggestedCommand) recordWorker(suggestedCommand);
457
+
458
+ // --- Env template keys (names only, values never touched) ---
459
+ const envKeys = new Set();
460
+ for (const file of ENV_TEMPLATE_FILES) {
461
+ for (const key of extractEnvKeys(readFileSafe(path.join(dir, file)))) envKeys.add(key);
462
+ }
463
+ collectSetSignals(envKeys, ENV_KEY_SIGNALS, buckets);
464
+ for (const key of ENV_MYSQL_KEYS) {
465
+ if (envKeys.has(key)) recordMysql(key);
466
+ }
467
+
468
+ // --- vercel.json crons (independent read: the edge-rule parser skips cron-only files) ---
469
+ const vercelJson = readJsonSafe(dir, 'vercel.json');
470
+ if (vercelJson && Array.isArray(vercelJson.crons) && vercelJson.crons.length > 0) cronHint = true;
471
+
472
+ // --- Migration command (shared detector) ---
473
+ let migrationCommand = null;
474
+ try {
475
+ migrationCommand = detectMigrationCommand(dir);
476
+ } catch {
477
+ migrationCommand = null;
478
+ }
479
+
480
+ const addons = {};
481
+ for (const cap of Object.keys(ADDON_REGISTRY)) {
482
+ addons[cap] = { detected: addonEvidence[cap].length > 0, evidence: [...addonEvidence[cap]] };
483
+ }
484
+
485
+ return {
486
+ relationalDb: { detected: relationalEvidence.length > 0, evidence: [...relationalEvidence] },
487
+ worker: {
488
+ detected: workerEvidence.length > 0,
489
+ suggestedCommand,
490
+ evidence: [...workerEvidence],
491
+ },
492
+ migration: { detected: migrationCommand !== null, command: migrationCommand },
493
+ addons,
494
+ upcomingHints: {
495
+ vector: vectorHint,
496
+ cron: cronHint,
497
+ mysql: mysqlEvidence.length > 0,
498
+ },
499
+ };
500
+ }
@@ -0,0 +1,65 @@
1
+ import color from 'picocolors';
2
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
3
+
4
+ function paint(tone, fallback) {
5
+ return typeof color[tone] === 'function' ? color[tone] : fallback;
6
+ }
7
+
8
+ // Shared failure path for CLI commands: prints the failure, records
9
+ // telemetry (always flushed before exit so failures are never lost),
10
+ // exits with the given code, and returns a standard `{ ok: false }`
11
+ // result for programmatic callers and unit tests.
12
+ //
13
+ // - `message`/`hint` cover the common red-message + dim-hint shape;
14
+ // pass `print` for anything custom (guidance printers, Clack cancels).
15
+ // - Omit `event` to skip telemetry (early pre-flight guards).
16
+ // - Pass `exitCode: null` to return without exiting (soft failures).
17
+ // - `errorCode` stamps `error_code` into telemetry without repeating the
18
+ // whole `telemetry` object; `extra` merges additional telemetry fields.
19
+ export async function failCommand({
20
+ message = null,
21
+ hint = null,
22
+ tone = 'red',
23
+ hintTone = 'dim',
24
+ print = null,
25
+ useErrorStream = false,
26
+ event = null,
27
+ telemetry = {},
28
+ errorCode = null,
29
+ extra = {},
30
+ reason = null,
31
+ resultExtra = {},
32
+ exitCode = 1,
33
+ } = {}) {
34
+ if (typeof print === 'function') {
35
+ print();
36
+ } else {
37
+ const write = useErrorStream ? console.error : console.log;
38
+ if (message !== null && message !== undefined) write(paint(tone, color.red)(message));
39
+ if (hint) write(paint(hintTone, color.dim)(hint));
40
+ }
41
+ if (event) {
42
+ trackEvent(event, {
43
+ ...telemetry,
44
+ ...(errorCode === null ? {} : { error_code: errorCode }),
45
+ ...extra,
46
+ success: false,
47
+ });
48
+ await flushTelemetry();
49
+ }
50
+ if (typeof exitCode === 'number') process.exit(exitCode);
51
+ return { ok: false, ...(reason === null ? {} : { reason }), ...resultExtra };
52
+ }
53
+
54
+ // Shared project-resolution failure: when a command cannot determine even
55
+ // its working directory or project identity (uninitialized directory,
56
+ // deleted cwd, programmatic misuse), fail structured instead of throwing.
57
+ export async function failProjectNotInitialized({ event }) {
58
+ return failCommand({
59
+ message: '\n✖ Could not determine the project. Run this command from a directory initialized with grada.',
60
+ hint: 'If the problem persists, re-run npx grada-run.\n',
61
+ event,
62
+ errorCode: 'PROJECT_NOT_INITIALIZED',
63
+ reason: 'project-not-initialized',
64
+ });
65
+ }