@grada-run/grada 0.0.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-docs.yml +37 -0
- package/.github/workflows/e2e.yml +73 -0
- package/.github/workflows/iac-validation.yml +303 -0
- package/.github/workflows/publish.yml +68 -0
- package/.github/workflows/sync-bedrock-models.yml +57 -0
- package/.github/workflows/test.yml +43 -0
- package/.muserules +31 -0
- package/LICENSE +21 -0
- package/README.md +193 -3
- package/apps/docs/.astro/collections/docs.schema.json +644 -0
- package/apps/docs/.astro/content-assets.mjs +4 -0
- package/apps/docs/.astro/content-modules.mjs +4 -0
- package/apps/docs/.astro/content.d.ts +179 -0
- package/apps/docs/.astro/data-store.json +1 -0
- package/apps/docs/.astro/dev.json +14 -0
- package/apps/docs/.astro/settings.json +5 -0
- package/apps/docs/.astro/types.d.ts +2 -0
- package/apps/docs/astro.config.mjs +97 -0
- package/apps/docs/package.json +17 -0
- package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
- package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
- package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
- package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
- package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
- package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
- package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
- package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
- package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
- package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
- package/apps/docs/src/content/docs/cli/add.md +85 -0
- package/apps/docs/src/content/docs/cli/apply.md +35 -0
- package/apps/docs/src/content/docs/cli/db.md +200 -0
- package/apps/docs/src/content/docs/cli/destroy.md +35 -0
- package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
- package/apps/docs/src/content/docs/cli/doctor.md +28 -0
- package/apps/docs/src/content/docs/cli/domain.md +57 -0
- package/apps/docs/src/content/docs/cli/drift.md +40 -0
- package/apps/docs/src/content/docs/cli/eject.md +33 -0
- package/apps/docs/src/content/docs/cli/exec.md +49 -0
- package/apps/docs/src/content/docs/cli/gc.md +37 -0
- package/apps/docs/src/content/docs/cli/init.md +72 -0
- package/apps/docs/src/content/docs/cli/logs.md +39 -0
- package/apps/docs/src/content/docs/cli/rollback.md +51 -0
- package/apps/docs/src/content/docs/cli/secrets.md +73 -0
- package/apps/docs/src/content/docs/cli/sleep.md +53 -0
- package/apps/docs/src/content/docs/cli/status.md +34 -0
- package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
- package/apps/docs/src/content/docs/guides/architecture.md +87 -0
- package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
- package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
- package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
- package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
- package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
- package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
- package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
- package/apps/docs/src/content/docs/guides/examples.md +50 -0
- package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
- package/apps/docs/src/content/docs/guides/headless.md +87 -0
- package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
- package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
- package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
- package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
- package/apps/docs/src/content/docs/index.mdx +103 -0
- package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
- package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
- package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
- package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
- package/apps/docs/src/content/docs/roadmap.md +99 -0
- package/apps/docs/src/content/docs/testing-strategy.md +37 -0
- package/apps/docs/src/content.config.ts +7 -0
- package/apps/docs/src/custom.css +14 -0
- package/apps/docs/tsconfig.json +6 -0
- package/bin/cli.js +140 -0
- package/package.json +107 -7
- package/scripts/sync-bedrock-models.js +22 -0
- package/scripts/test-iac.js +261 -0
- package/specs/add-redis-sqs-bedrock.md +128 -0
- package/specs/add-storage-dynamodb.md +106 -0
- package/specs/bedrock-model-catalog.md +131 -0
- package/specs/ci-pipeline.md +17 -0
- package/specs/cost-transparency.md +115 -0
- package/specs/custom-domains-and-ses.md +153 -0
- package/specs/database-suite-expansion.md +151 -0
- package/specs/db-connect.md +69 -0
- package/specs/db-lifecycle-migrations.md +159 -0
- package/specs/dependency-aware-init.md +176 -0
- package/specs/deployment-safety.md +170 -0
- package/specs/diagnose.md +16 -0
- package/specs/docs-hub.md +16 -0
- package/specs/dx-polish.md +46 -0
- package/specs/e2e-testing.md +50 -0
- package/specs/exec.md +25 -0
- package/specs/finops-cron-drift.md +161 -0
- package/specs/gc.md +26 -0
- package/specs/integration-suite.md +16 -0
- package/specs/logs.md +32 -0
- package/specs/rollback-live-polling.md +40 -0
- package/specs/secrets-pull-audit.md +51 -0
- package/specs/serverless-lambda-target.md +133 -0
- package/specs/status.md +31 -0
- package/specs/telemetry-and-spawn-hardening.md +69 -0
- package/specs/telemetry-hardening.md +35 -0
- package/specs/test-suite-deduplication.md +42 -0
- package/src/commands/add.js +1111 -0
- package/src/commands/apply.js +214 -0
- package/src/commands/db/backup.js +229 -0
- package/src/commands/db/connect.js +304 -0
- package/src/commands/db/enable-vector.js +344 -0
- package/src/commands/db/import.js +604 -0
- package/src/commands/db/migrate.js +477 -0
- package/src/commands/db/restore.js +361 -0
- package/src/commands/db.js +87 -0
- package/src/commands/destroy.js +217 -0
- package/src/commands/diagnose.js +460 -0
- package/src/commands/doctor.js +109 -0
- package/src/commands/domain.js +685 -0
- package/src/commands/drift.js +243 -0
- package/src/commands/eject.js +130 -0
- package/src/commands/exec.js +222 -0
- package/src/commands/gc.js +250 -0
- package/src/commands/init.js +649 -0
- package/src/commands/logs.js +256 -0
- package/src/commands/rollback.js +323 -0
- package/src/commands/secrets.js +485 -0
- package/src/commands/sleep.js +347 -0
- package/src/commands/status.js +309 -0
- package/src/commands/sync-ai.js +115 -0
- package/src/commands/wake.js +337 -0
- package/src/core/parser.js +128 -0
- package/src/core/telemetry.js +244 -0
- package/src/data/bedrock-models.json +896 -0
- package/src/utils/addons.js +126 -0
- package/src/utils/ai-rules.js +59 -0
- package/src/utils/args.js +91 -0
- package/src/utils/aws.js +178 -0
- package/src/utils/backup.js +69 -0
- package/src/utils/bedrock-catalog.js +511 -0
- package/src/utils/capabilities.js +500 -0
- package/src/utils/command.js +78 -0
- package/src/utils/db-tunnel.js +164 -0
- package/src/utils/detector.js +298 -0
- package/src/utils/dockerCompose.js +65 -0
- package/src/utils/domains.js +73 -0
- package/src/utils/ecs-runner.js +289 -0
- package/src/utils/ecs.js +92 -0
- package/src/utils/frameworks.js +55 -0
- package/src/utils/generator.js +527 -0
- package/src/utils/hcl.js +426 -0
- package/src/utils/lambda-ecr.js +185 -0
- package/src/utils/prompts.js +278 -0
- package/src/utils/rds.js +131 -0
- package/src/utils/resolvers.js +174 -0
- package/src/utils/sleep-state.js +140 -0
- package/src/utils/sleep-targets.js +139 -0
- package/src/utils/system.js +42 -0
- package/src/utils/terraform.js +70 -0
- package/src/utils/visualizer.js +381 -0
- package/src/utils/warnings.js +49 -0
- package/templates/README.md +150 -0
- package/templates/docker/django.Dockerfile +40 -0
- package/templates/docker/go.Dockerfile +23 -0
- package/templates/docker/nestjs.Dockerfile +33 -0
- package/templates/docker/nextjs.Dockerfile +55 -0
- package/templates/docker/node.Dockerfile +24 -0
- package/templates/docker/nuxt.Dockerfile +47 -0
- package/templates/docker/python.Dockerfile +38 -0
- package/templates/docker/rails.Dockerfile +59 -0
- package/templates/docker/static.Dockerfile +32 -0
- package/templates/docker/svelte.Dockerfile +52 -0
- package/templates/github/deploy-lambda.yml +120 -0
- package/templates/github/deploy.yml +138 -0
- package/templates/github/drift.yml +112 -0
- package/templates/github/preview-lambda.yml +86 -0
- package/templates/github/preview.yml +69 -0
- package/templates/github/teardown.yml +43 -0
- package/templates/terraform/addons/bedrock.tf +34 -0
- package/templates/terraform/addons/cron-lambda.tf +78 -0
- package/templates/terraform/addons/cron.tf +101 -0
- package/templates/terraform/addons/dynamodb.tf +73 -0
- package/templates/terraform/addons/redis.tf +64 -0
- package/templates/terraform/addons/s3.tf +143 -0
- package/templates/terraform/addons/ses.tf +73 -0
- package/templates/terraform/addons/sqs.tf +67 -0
- package/templates/terraform/backend.tf +22 -0
- package/templates/terraform/cloudfront-lambda.tf +80 -0
- package/templates/terraform/cloudfront.tf +80 -0
- package/templates/terraform/database-aurora-postgresql.tf +92 -0
- package/templates/terraform/database-mysql.tf +72 -0
- package/templates/terraform/database.tf +71 -0
- package/templates/terraform/main-lambda.tf +229 -0
- package/templates/terraform/main.tf +296 -0
- package/templates/terraform/network.tf +95 -0
- package/templates/terraform/oidc.tf +64 -0
- package/templates/terraform/secrets.tf +31 -0
- package/templates/terraform/worker.tf +69 -0
- package/tests/__snapshots__/generator.test.js.snap +9633 -0
- package/tests/add.test.js +2018 -0
- package/tests/ai.test.js +91 -0
- package/tests/apply.test.js +446 -0
- package/tests/args.test.js +86 -0
- package/tests/aws.test.js +244 -0
- package/tests/capabilities.test.js +304 -0
- package/tests/cli.test.js +29 -0
- package/tests/command.test.js +99 -0
- package/tests/commands-import.test.js +74 -0
- package/tests/db.test.js +2673 -0
- package/tests/destroy.test.js +365 -0
- package/tests/detector.test.js +79 -0
- package/tests/diagnose.test.js +765 -0
- package/tests/doctor.test.js +195 -0
- package/tests/domain.test.js +882 -0
- package/tests/drift.test.js +228 -0
- package/tests/e2e/helpers.js +122 -0
- package/tests/e2e/tier0.e2e.test.js +142 -0
- package/tests/e2e/tier1.live.e2e.test.js +118 -0
- package/tests/ecs.test.js +130 -0
- package/tests/eject.test.js +65 -0
- package/tests/exec.test.js +366 -0
- package/tests/gc.test.js +466 -0
- package/tests/generator.test.js +794 -0
- package/tests/headless.test.js +491 -0
- package/tests/helpers/clack.js +103 -0
- package/tests/helpers/console.js +41 -0
- package/tests/helpers/telemetry.js +37 -0
- package/tests/helpers/tmpdir.js +36 -0
- package/tests/lambda-ecr.test.js +185 -0
- package/tests/logs.test.js +436 -0
- package/tests/parser.test.js +160 -0
- package/tests/rds.test.js +244 -0
- package/tests/resolvers.test.js +279 -0
- package/tests/rollback.test.js +670 -0
- package/tests/secrets.test.js +729 -0
- package/tests/sleep-wake.test.js +998 -0
- package/tests/status.test.js +356 -0
- package/tests/system.test.js +70 -0
- package/tests/telemetry.test.js +520 -0
- package/tests/terraform.test.js +84 -0
- package/tests/visualizer.test.js +480 -0
- package/vitest.config.js +10 -0
- package/vitest.e2e.tier0.config.js +8 -0
- package/vitest.e2e.tier1.config.js +8 -0
- package/index.js +0 -2
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { intro, outro, spinner, log, confirm, multiselect, select, cancel } from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
|
|
6
|
+
import { checkDependency } from '../utils/system.js';
|
|
7
|
+
import { normalizeOptions } from '../utils/args.js';
|
|
8
|
+
import {
|
|
9
|
+
detectFramework,
|
|
10
|
+
parseProcfile,
|
|
11
|
+
parseVercelConfig,
|
|
12
|
+
analyzeNextConfig,
|
|
13
|
+
analyzeSvelteConfig,
|
|
14
|
+
analyzeAstroConfig,
|
|
15
|
+
analyzeNestApp,
|
|
16
|
+
splitProcfileCommand
|
|
17
|
+
} from '../utils/detector.js';
|
|
18
|
+
import { detectProjectCapabilities } from '../utils/capabilities.js';
|
|
19
|
+
import { ADDON_REGISTRY } from '../utils/addons.js';
|
|
20
|
+
import { validateAddonFlags, resolveAddonOptions, scaffoldAddon, DEFAULT_BEDROCK_MODEL } from './add.js';
|
|
21
|
+
import { injectMigrationGate } from './db/migrate.js';
|
|
22
|
+
import { scaffoldDriftWorkflow } from './drift.js';
|
|
23
|
+
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
24
|
+
import { failCommand } from '../utils/command.js';
|
|
25
|
+
import { getFrameworkWarning } from '../utils/warnings.js';
|
|
26
|
+
import { provisionStateBucket } from '../utils/aws.js';
|
|
27
|
+
import { generateTemplates } from '../utils/generator.js';
|
|
28
|
+
import { handleExistingFiles } from '../utils/backup.js';
|
|
29
|
+
import { estimateMonthlyCost, parseTerraformConfig, renderDryRunPreview } from '../utils/visualizer.js';
|
|
30
|
+
import { getTargetDirectory, getProjectConfig, promptWorkerCommand } from '../utils/prompts.js';
|
|
31
|
+
import { resolveDjangoWsgi, handleRailsCI } from '../utils/frameworks.js';
|
|
32
|
+
import { parseDockerCompose } from '../utils/dockerCompose.js';
|
|
33
|
+
import { getBaseRules, getCursorRules, injectManagedBlock } from '../utils/ai-rules.js';
|
|
34
|
+
import { readFileSafe } from '../utils/resolvers.js';
|
|
35
|
+
import { parseDomainTf } from '../utils/domains.js';
|
|
36
|
+
|
|
37
|
+
const pkg = JSON.parse(fsSync.readFileSync(new URL('../../package.json', import.meta.url)));
|
|
38
|
+
const CLI_VERSION = pkg.version;
|
|
39
|
+
|
|
40
|
+
export async function mainStack(input = {}) {
|
|
41
|
+
const options = normalizeOptions(input);
|
|
42
|
+
const { isHeadless = false } = options;
|
|
43
|
+
const isPreconfigured = options.isPreconfigured === true || process.argv.includes('--preconfigured');
|
|
44
|
+
const headlessOptions = normalizeOptions(options.headlessOptions);
|
|
45
|
+
const initOptions = normalizeOptions(options.initOptions);
|
|
46
|
+
const isInteractive = !isHeadless && !isPreconfigured;
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
|
|
49
|
+
// 1. Silent Pre-flight check
|
|
50
|
+
const hasTerraform = await checkDependency('terraform');
|
|
51
|
+
if (!hasTerraform) {
|
|
52
|
+
await failCommand({
|
|
53
|
+
print: () => {
|
|
54
|
+
console.error(color.red('✖ Terraform is not installed.'));
|
|
55
|
+
console.log(color.yellow('Please run "npx grada-run doctor" to check your environment.'));
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isHeadless) intro(color.bgCyan(color.black(' grada ☁️ ')));
|
|
61
|
+
|
|
62
|
+
// 2. Resolve Target & Scan Codebase
|
|
63
|
+
const dirConfig = await getTargetDirectory(isHeadless, headlessOptions);
|
|
64
|
+
|
|
65
|
+
// 2b. Fail-fast validation of init composition flags — before any
|
|
66
|
+
// prompts, AWS calls, backups, or file writes.
|
|
67
|
+
const withList = Array.isArray(initOptions.with)
|
|
68
|
+
? initOptions.with.filter((cap) => typeof cap === 'string')
|
|
69
|
+
: [];
|
|
70
|
+
const unknownCaps = withList.filter((cap) => !ADDON_REGISTRY[cap]);
|
|
71
|
+
if (unknownCaps.length > 0) {
|
|
72
|
+
return failCommand({
|
|
73
|
+
message: `\n✖ Unknown addon capabilities: ${unknownCaps.join(', ')}.`,
|
|
74
|
+
hint: ` Valid capabilities: ${Object.keys(ADDON_REGISTRY).join(', ')}.\n`,
|
|
75
|
+
event: 'cli-error',
|
|
76
|
+
telemetry: { step: 'init_validation', error_code: 'UNSUPPORTED_CAPABILITY' },
|
|
77
|
+
reason: 'unsupported-capability',
|
|
78
|
+
resultExtra: { capabilities: withList },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const VALID_DB_ENGINES = ['postgres', 'mysql', 'aurora-postgresql'];
|
|
82
|
+
const explicitDbEngine = typeof initOptions.dbEngine === 'string' && initOptions.dbEngine.trim() !== ''
|
|
83
|
+
? initOptions.dbEngine.trim()
|
|
84
|
+
: null;
|
|
85
|
+
if (explicitDbEngine !== null && !VALID_DB_ENGINES.includes(explicitDbEngine)) {
|
|
86
|
+
return failCommand({
|
|
87
|
+
message: `\n✖ Invalid --db-engine "${explicitDbEngine}".`,
|
|
88
|
+
hint: ` Valid engines: ${VALID_DB_ENGINES.join(', ')}.\n`,
|
|
89
|
+
event: 'cli-error',
|
|
90
|
+
telemetry: { step: 'init_validation', error_code: 'INVALID_DB_ENGINE' },
|
|
91
|
+
reason: 'invalid-db-engine',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const VALID_COMPUTE_TARGETS = ['ecs', 'lambda'];
|
|
95
|
+
const explicitTarget = typeof initOptions.target === 'string' && initOptions.target.trim() !== ''
|
|
96
|
+
? initOptions.target.trim()
|
|
97
|
+
: null;
|
|
98
|
+
const normalizedExplicitTarget = explicitTarget === 'fargate' ? 'ecs' : explicitTarget;
|
|
99
|
+
if (explicitTarget !== null && !VALID_COMPUTE_TARGETS.includes(normalizedExplicitTarget)) {
|
|
100
|
+
return failCommand({
|
|
101
|
+
message: `\n✖ Invalid compute target "${explicitTarget}". Supported targets: ecs, lambda.`,
|
|
102
|
+
hint: ' Use --target ecs (or fargate) for always-on containers, or --target lambda for scale-to-zero serverless.\n',
|
|
103
|
+
event: 'cli-error',
|
|
104
|
+
telemetry: { step: 'init_validation', error_code: 'INVALID_COMPUTE_TARGET' },
|
|
105
|
+
reason: 'invalid-compute-target',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const addonFlagOptions = {
|
|
109
|
+
model: initOptions.model ?? undefined,
|
|
110
|
+
domain: initOptions.domain ?? undefined,
|
|
111
|
+
zoneId: initOptions.zoneId ?? undefined,
|
|
112
|
+
fromEmail: initOptions.fromEmail ?? undefined,
|
|
113
|
+
};
|
|
114
|
+
for (const cap of withList) {
|
|
115
|
+
// Per-capability scoped: irrelevant flags are silently ignored.
|
|
116
|
+
const flagError = validateAddonFlags(cap, addonFlagOptions);
|
|
117
|
+
if (flagError) {
|
|
118
|
+
return failCommand({
|
|
119
|
+
message: flagError.message,
|
|
120
|
+
hint: flagError.hint ?? null,
|
|
121
|
+
event: 'cli-error',
|
|
122
|
+
telemetry: { step: 'init_validation', error_code: flagError.errorCode },
|
|
123
|
+
reason: flagError.reason,
|
|
124
|
+
resultExtra: { capability: cap },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (withList.includes('email:ses') && !isInteractive) {
|
|
129
|
+
const hasDomainFlag = typeof initOptions.domain === 'string' && initOptions.domain.trim() !== '';
|
|
130
|
+
let domainTfHasDomain = false;
|
|
131
|
+
if (!hasDomainFlag) {
|
|
132
|
+
const domainTf = readFileSafe(path.join(dirConfig.targetDir, 'terraform', 'domain.tf'));
|
|
133
|
+
domainTfHasDomain = Boolean(domainTf && parseDomainTf(domainTf)?.domain);
|
|
134
|
+
}
|
|
135
|
+
if (!hasDomainFlag && !domainTfHasDomain) {
|
|
136
|
+
return failCommand({
|
|
137
|
+
message: '\n✖ No domain for Amazon SES. Pass --domain <domain> or run from a project with terraform/domain.tf.\n',
|
|
138
|
+
event: 'cli-error',
|
|
139
|
+
telemetry: { step: 'init_validation', error_code: 'MISSING_SES_DOMAIN' },
|
|
140
|
+
reason: 'missing-ses-domain',
|
|
141
|
+
resultExtra: { capability: 'email:ses' },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Compute target: explicit flag wins (headless defaults to ECS);
|
|
147
|
+
// interactive runs prompt when the flag was not passed.
|
|
148
|
+
let target = normalizedExplicitTarget || 'ecs';
|
|
149
|
+
if (isInteractive && normalizedExplicitTarget === null) {
|
|
150
|
+
log.info(`${color.gray('📖 Compare tradeoffs (cost crossover, cold starts, DB connections):')} ${color.underline('https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/guides/architecture.md')}`);
|
|
151
|
+
const targetAnswer = await select({
|
|
152
|
+
message: 'Select your AWS compute target:',
|
|
153
|
+
options: [
|
|
154
|
+
{ value: 'ecs', label: 'ECS Fargate + ALB', hint: 'Always-on, persistent DB connections, zero cold starts (Starts at ~$31/mo)' },
|
|
155
|
+
{ value: 'lambda', label: 'AWS Lambda + API Gateway v2', hint: 'Scale-to-zero, usage-based compute, 3-5s VPC cold starts ($0/mo idle)' },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
if (typeof targetAnswer === 'symbol') process.exit(0);
|
|
159
|
+
target = targetAnswer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const detectedFramework = detectFramework(dirConfig.targetDir);
|
|
163
|
+
const procfile = parseProcfile(dirConfig.targetDir);
|
|
164
|
+
const vercelRules = parseVercelConfig(dirConfig.targetDir);
|
|
165
|
+
const dockerCompose = parseDockerCompose(dirConfig.targetDir);
|
|
166
|
+
const capabilities = detectProjectCapabilities(dirConfig.targetDir);
|
|
167
|
+
|
|
168
|
+
if (!isHeadless) {
|
|
169
|
+
if (detectedFramework) log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
170
|
+
if (procfile && procfile.web) log.success(`Auto-detected Procfile (web command: ${procfile.web.join(' ')})`);
|
|
171
|
+
if (vercelRules) log.success(`Auto-detected vercel.json (Migrating edge network rules)`);
|
|
172
|
+
if (dockerCompose) log.success(`Auto-detected docker-compose.yml (${dockerCompose.length} services mapped)`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isHeadless) console.log(color.cyan(`🤖 Running grada in headless mode`));
|
|
176
|
+
|
|
177
|
+
// 3. Gather Configuration & Framework Quirks
|
|
178
|
+
const config = await getProjectConfig(isHeadless, headlessOptions, dirConfig.targetDir, detectedFramework, { capabilities, dbEngine: explicitDbEngine, target });
|
|
179
|
+
config.target = target;
|
|
180
|
+
// Explicit flag wins everywhere (headless defaults to postgres, the
|
|
181
|
+
// interactive prompt is skipped); anything else falls back to postgres.
|
|
182
|
+
if (explicitDbEngine) {
|
|
183
|
+
config.dbEngine = explicitDbEngine;
|
|
184
|
+
} else if (!VALID_DB_ENGINES.includes(config.dbEngine)) {
|
|
185
|
+
config.dbEngine = 'postgres';
|
|
186
|
+
}
|
|
187
|
+
const djangoWsgi = await resolveDjangoWsgi(dirConfig.targetDir, procfile, config.framework, isHeadless);
|
|
188
|
+
const disableDefaultCI = await handleRailsCI(dirConfig.targetDir, config.framework, isHeadless);
|
|
189
|
+
|
|
190
|
+
// 3b. Dependency-aware composition (worker, migration gate, addons).
|
|
191
|
+
// Everything here resolves before backups, AWS calls, or file writes.
|
|
192
|
+
const isStaticSite = config.framework === 'static';
|
|
193
|
+
const failAddonResolution = (cap, resolved) => {
|
|
194
|
+
if (!resolved.ok && resolved.cancelled) {
|
|
195
|
+
return failCommand({
|
|
196
|
+
print: () => cancel('Selection cancelled.'),
|
|
197
|
+
event: 'cli-error',
|
|
198
|
+
telemetry: { step: 'init_addons', reason: 'cancelled' },
|
|
199
|
+
reason: 'cancelled',
|
|
200
|
+
exitCode: null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return failCommand({
|
|
204
|
+
message: resolved.message,
|
|
205
|
+
hint: resolved.hint ?? null,
|
|
206
|
+
event: 'cli-error',
|
|
207
|
+
telemetry: { step: 'init_addons', error_code: resolved.errorCode },
|
|
208
|
+
reason: resolved.reason,
|
|
209
|
+
resultExtra: { capability: cap },
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Worker command: Procfile-driven as before, plus detection-driven.
|
|
214
|
+
// Lambda targets have no ECS worker service, so the prompt is skipped
|
|
215
|
+
// and any Procfile worker process is left out of the generated stack.
|
|
216
|
+
let workerCommandHcl = '';
|
|
217
|
+
if (isInteractive && !isStaticSite && target !== 'lambda' && (procfile?.worker || capabilities.worker.detected)) {
|
|
218
|
+
const workerAnswer = await promptWorkerCommand(procfile, capabilities);
|
|
219
|
+
if (workerAnswer.trim() !== '') {
|
|
220
|
+
workerCommandHcl = `command = ${JSON.stringify(splitProcfileCommand(workerAnswer.trim()))}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (target === 'lambda' && (procfile?.worker || capabilities.worker.detected)) {
|
|
224
|
+
log.warn(color.yellow('⚠️ Skipping the background worker: Lambda targets run a single scale-to-zero function with no ECS worker service.'));
|
|
225
|
+
}
|
|
226
|
+
if (target === 'lambda' && config.needsDatabase) {
|
|
227
|
+
log.warn(color.yellow('⚠️ Lambda opens a database connection per concurrent execution with no proxy in between — bursts can exhaust RDS limits. Keep pools tiny (tradeoffs: https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/guides/architecture.md#fargate-vs-lambda-tradeoffs).'));
|
|
228
|
+
}
|
|
229
|
+
const willHaveWorker = target !== 'lambda' && (Boolean(procfile?.worker) || workerCommandHcl !== '');
|
|
230
|
+
|
|
231
|
+
// Pre-deploy migration gate: explicit flag wins, else prompt.
|
|
232
|
+
const migrationCmd = capabilities.migration.command;
|
|
233
|
+
const setupCiMigrateFlag = initOptions.setupCiMigrate === true;
|
|
234
|
+
let migrationGateEnabled = false;
|
|
235
|
+
if (target === 'lambda' && (setupCiMigrateFlag || (config.needsDatabase && migrationCmd))) {
|
|
236
|
+
log.warn(color.yellow('⚠️ Skipping the pre-deploy migration gate: it runs migrations as an ephemeral ECS task, which Lambda targets don\'t provision. Run migrations from CI against your database endpoint instead.'));
|
|
237
|
+
} else if (config.needsDatabase && migrationCmd) {
|
|
238
|
+
if (setupCiMigrateFlag) {
|
|
239
|
+
migrationGateEnabled = true;
|
|
240
|
+
} else if (isInteractive) {
|
|
241
|
+
const gateAnswer = await confirm({
|
|
242
|
+
message: `Enable pre-deploy database migration gate in GitHub Actions? (detected: ${migrationCmd})`,
|
|
243
|
+
initialValue: true,
|
|
244
|
+
});
|
|
245
|
+
if (typeof gateAnswer === 'symbol') process.exit(0);
|
|
246
|
+
migrationGateEnabled = gateAnswer === true;
|
|
247
|
+
}
|
|
248
|
+
} else if (setupCiMigrateFlag) {
|
|
249
|
+
log.warn(color.yellow('⚠️ --setup-ci-migrate was passed but no database is configured or no migration command was detected; skipping the migration gate.'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Scheduled IaC drift detection: explicit flag wins, else offer.
|
|
253
|
+
// Opt-in by default — the workflow opens GitHub Issues on its own.
|
|
254
|
+
const setupCiDriftFlag = initOptions.setupCiDrift === true;
|
|
255
|
+
let driftEnabled = false;
|
|
256
|
+
if (setupCiDriftFlag) {
|
|
257
|
+
driftEnabled = true;
|
|
258
|
+
} else if (isInteractive) {
|
|
259
|
+
const driftAnswer = await confirm({
|
|
260
|
+
message: 'Set up scheduled IaC drift detection (daily terraform plan + GitHub Issues)?',
|
|
261
|
+
initialValue: false,
|
|
262
|
+
});
|
|
263
|
+
if (typeof driftAnswer === 'symbol') process.exit(0);
|
|
264
|
+
driftEnabled = driftAnswer === true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Addon selection: interactive multiselect, or explicit --with.
|
|
268
|
+
let selectedAddons;
|
|
269
|
+
if (isInteractive && !isStaticSite) {
|
|
270
|
+
const addonOptions = Object.keys(ADDON_REGISTRY).map((cap) => {
|
|
271
|
+
const entry = ADDON_REGISTRY[cap];
|
|
272
|
+
const addonState = capabilities.addons[cap];
|
|
273
|
+
const detected = addonState?.detected === true;
|
|
274
|
+
const evidence = detected ? (addonState.evidence || []).join(', ') : '';
|
|
275
|
+
const monthlyFixed = entry.cost.monthlyFixed || 0;
|
|
276
|
+
const costLabel = monthlyFixed > 0 ? `~$${monthlyFixed}/mo` : '$0/mo + usage';
|
|
277
|
+
return {
|
|
278
|
+
value: cap,
|
|
279
|
+
label: `${cap} — ${entry.label}`,
|
|
280
|
+
hint: detected ? `detected: ${evidence} · ${costLabel}` : costLabel,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
const initialValues = Object.keys(ADDON_REGISTRY).filter((cap) =>
|
|
284
|
+
withList.includes(cap) || capabilities.addons[cap]?.detected === true
|
|
285
|
+
);
|
|
286
|
+
const selection = await multiselect({
|
|
287
|
+
message: 'Select cloud addons to scaffold (Space to toggle, Enter to confirm):',
|
|
288
|
+
options: addonOptions,
|
|
289
|
+
initialValues,
|
|
290
|
+
required: false,
|
|
291
|
+
});
|
|
292
|
+
if (typeof selection === 'symbol') {
|
|
293
|
+
cancel('Operation cancelled.');
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
selectedAddons = Array.isArray(selection) ? selection : [];
|
|
297
|
+
} else {
|
|
298
|
+
selectedAddons = [...withList];
|
|
299
|
+
}
|
|
300
|
+
const selectedSet = new Set(selectedAddons);
|
|
301
|
+
selectedAddons = Object.keys(ADDON_REGISTRY).filter((cap) => selectedSet.has(cap));
|
|
302
|
+
|
|
303
|
+
// Resolve per-addon options (template vars, env, follow-up prompts).
|
|
304
|
+
const resolvedByCap = {};
|
|
305
|
+
const explicitModel = typeof initOptions.model === 'string' && initOptions.model !== '' ? initOptions.model : null;
|
|
306
|
+
for (const cap of selectedAddons) {
|
|
307
|
+
if (cap === 'ai:bedrock' && isInteractive && !explicitModel) {
|
|
308
|
+
const useDefault = await confirm({
|
|
309
|
+
message: `Use recommended Bedrock model (${DEFAULT_BEDROCK_MODEL})?`,
|
|
310
|
+
initialValue: true,
|
|
311
|
+
});
|
|
312
|
+
if (typeof useDefault === 'symbol') process.exit(0);
|
|
313
|
+
const resolved = await resolveAddonOptions(cap, {}, {
|
|
314
|
+
cwd: dirConfig.targetDir, region: config.region, isInteractive: !useDefault,
|
|
315
|
+
});
|
|
316
|
+
if (!resolved.ok) return failAddonResolution(cap, resolved);
|
|
317
|
+
resolvedByCap[cap] = resolved;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const opts = cap === 'ai:bedrock'
|
|
321
|
+
? { model: explicitModel ?? undefined, modelProvided: explicitModel !== null }
|
|
322
|
+
: cap === 'email:ses'
|
|
323
|
+
? { domain: initOptions.domain ?? undefined, zoneId: initOptions.zoneId ?? undefined, fromEmail: initOptions.fromEmail ?? undefined }
|
|
324
|
+
: {};
|
|
325
|
+
const resolved = await resolveAddonOptions(cap, opts, {
|
|
326
|
+
cwd: dirConfig.targetDir,
|
|
327
|
+
region: config.region,
|
|
328
|
+
isInteractive: isInteractive && cap === 'email:ses',
|
|
329
|
+
});
|
|
330
|
+
if (!resolved.ok) return failAddonResolution(cap, resolved);
|
|
331
|
+
resolvedByCap[cap] = resolved;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 3.5 Docker Compose Overrides
|
|
335
|
+
if (dockerCompose && dockerCompose.length > 0) {
|
|
336
|
+
// Find the first service that has an exposed port
|
|
337
|
+
const webService = dockerCompose.find(s => s.port);
|
|
338
|
+
if (webService && webService.port) {
|
|
339
|
+
// Override the config port with the one from Docker Compose
|
|
340
|
+
config.port = webService.port.toString();
|
|
341
|
+
if (!isHeadless) {
|
|
342
|
+
console.log(color.cyan(` 🐳 Docker Compose overrides: Port set to ${config.port} via service "${webService.name}"`));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 4. Framework Migration Checks (Vercel Escape Hatch)
|
|
348
|
+
if (target === 'lambda' && vercelRules?.redirects?.length > 0) {
|
|
349
|
+
log.warn(color.yellow('⚠️ vercel.json redirects were detected, but Lambda targets have no ALB listener rules to translate them into. Recreate them as API Gateway routes after provisioning.'));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (config.framework === 'nestjs') {
|
|
353
|
+
const nestAnalysis = analyzeNestApp(dirConfig.targetDir);
|
|
354
|
+
if (nestAnalysis.hasMain && !nestAnalysis.listensOnAllInterfaces) {
|
|
355
|
+
log.warn(color.yellow(`⚠️ NestJS must listen on 0.0.0.0 to receive traffic in ${target === 'lambda' ? 'AWS Lambda' : 'AWS Fargate'}.`));
|
|
356
|
+
console.log(color.cyan(` In ${nestAnalysis.filePath}, update your bootstrap:`));
|
|
357
|
+
console.log(color.green(` await app.listen(process.env.PORT ?? 3000, '0.0.0.0');\n`));
|
|
358
|
+
}
|
|
359
|
+
} else if (config.framework === 'nextjs') {
|
|
360
|
+
const nextConfig = analyzeNextConfig(dirConfig.targetDir);
|
|
361
|
+
if (nextConfig.hasConfig && !nextConfig.isStandalone) {
|
|
362
|
+
log.warn(color.yellow('⚠️ Next.js config is missing "output: \'standalone\'".'));
|
|
363
|
+
console.log(color.cyan(' Fix it here: https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md'));
|
|
364
|
+
}
|
|
365
|
+
} else if (detectedFramework?.name === 'SvelteKit') {
|
|
366
|
+
const svelteConfig = analyzeSvelteConfig(dirConfig.targetDir);
|
|
367
|
+
if (svelteConfig.adapter === 'vercel' || svelteConfig.adapter === 'auto') {
|
|
368
|
+
log.warn(color.yellow('⚠️ SvelteKit is locked into the Vercel/Auto adapter.'));
|
|
369
|
+
console.log(color.cyan(' Fix it here: https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md'));
|
|
370
|
+
}
|
|
371
|
+
} else if (detectedFramework?.name === 'Astro') {
|
|
372
|
+
const astroConfig = analyzeAstroConfig(dirConfig.targetDir);
|
|
373
|
+
if (astroConfig.adapter === 'vercel') {
|
|
374
|
+
log.warn(color.yellow('⚠️ Astro is locked into the Vercel adapter.'));
|
|
375
|
+
console.log(color.cyan(' Fix it here: https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md'));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 5. Calculate Derived Values
|
|
380
|
+
const cpu = config.size === 'small' ? '512' : '256';
|
|
381
|
+
const memory = config.size === 'small' ? '1024' : '512';
|
|
382
|
+
const computeTier = config.size === 'small' ? 'Small (0.5 vCPU, 1GB RAM)' : 'Micro (0.25 vCPU, 512MB RAM)';
|
|
383
|
+
|
|
384
|
+
const costs = estimateMonthlyCost({
|
|
385
|
+
cpu: parseInt(cpu),
|
|
386
|
+
memory: parseInt(memory),
|
|
387
|
+
hasDb: config.needsDatabase,
|
|
388
|
+
dbEngine: config.dbEngine,
|
|
389
|
+
hasWorker: willHaveWorker,
|
|
390
|
+
hasSecrets: true,
|
|
391
|
+
addons: selectedAddons,
|
|
392
|
+
computeTarget: target,
|
|
393
|
+
});
|
|
394
|
+
const estimatedCost = costs.totalMonthly;
|
|
395
|
+
const buildDir = detectedFramework?.buildDir || 'dist';
|
|
396
|
+
|
|
397
|
+
// 6. Handle Backups & Provision Remote State
|
|
398
|
+
await handleExistingFiles(dirConfig.targetDir, isHeadless);
|
|
399
|
+
|
|
400
|
+
const s = spinner();
|
|
401
|
+
s.start('Provisioning infrastructure...');
|
|
402
|
+
|
|
403
|
+
let awsAccountId, stateBucketName;
|
|
404
|
+
try {
|
|
405
|
+
const bucketData = await provisionStateBucket(config.region, dirConfig.actualProjectName);
|
|
406
|
+
awsAccountId = bucketData.awsAccountId;
|
|
407
|
+
stateBucketName = bucketData.stateBucketName;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
s.stop('❌ Failed to provision remote state or authenticate with AWS.');
|
|
410
|
+
return failCommand({
|
|
411
|
+
message: `AWS Error: ${error.message}`,
|
|
412
|
+
useErrorStream: true,
|
|
413
|
+
event: 'cli-error',
|
|
414
|
+
telemetry: { step: 'aws_provisioning', error_code: error.name || 'UNKNOWN', error_message: error.message },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 7. Synthesize Templates
|
|
419
|
+
s.message('Synthesizing Terraform templates...');
|
|
420
|
+
await generateTemplates(dirConfig.targetDir, {
|
|
421
|
+
PROJECT_NAME: dirConfig.actualProjectName,
|
|
422
|
+
REGION: config.region,
|
|
423
|
+
PORT: config.port,
|
|
424
|
+
CPU: cpu,
|
|
425
|
+
MEMORY: memory,
|
|
426
|
+
COMPUTE_TIER: computeTier,
|
|
427
|
+
ESTIMATED_COST: estimatedCost,
|
|
428
|
+
STATE_BUCKET: stateBucketName,
|
|
429
|
+
AWS_ACCOUNT_ID: awsAccountId,
|
|
430
|
+
HEALTH_CHECK_PATH: config.healthCheckPath,
|
|
431
|
+
DESIRED_COUNT: config.desiredCount,
|
|
432
|
+
DEPLOY_BRANCH: config.branch,
|
|
433
|
+
BUILD_DIR: buildDir,
|
|
434
|
+
finalFramework: config.framework,
|
|
435
|
+
NEEDS_DATABASE: config.needsDatabase,
|
|
436
|
+
DB_ENGINE: config.dbEngine,
|
|
437
|
+
DJANGO_WSGI: djangoWsgi,
|
|
438
|
+
DISABLE_DEFAULT_CI: disableDefaultCI,
|
|
439
|
+
PROCFILE: procfile,
|
|
440
|
+
VERCEL_RULES: vercelRules,
|
|
441
|
+
DOCKER_COMPOSE: dockerCompose,
|
|
442
|
+
ENABLE_PR_PREVIEWS: config.enablePrPreviews,
|
|
443
|
+
WORKER_COMMAND: workerCommandHcl,
|
|
444
|
+
TARGET: target
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// 7b. Scaffold selected addons (registry order), then wire the
|
|
448
|
+
// pre-deploy migration gate into the generated workflow.
|
|
449
|
+
for (const cap of selectedAddons) {
|
|
450
|
+
const scaffolded = await scaffoldAddon(cap, resolvedByCap[cap], { cwd: dirConfig.targetDir, region: config.region });
|
|
451
|
+
console.log(color.green(`✅ Scaffolded terraform/${scaffolded.file} (${cap})`));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (migrationGateEnabled && migrationCmd) {
|
|
455
|
+
const deployYmlPath = path.join(dirConfig.targetDir, '.github', 'workflows', 'deploy.yml');
|
|
456
|
+
const workflowContent = readFileSafe(deployYmlPath);
|
|
457
|
+
const gated = workflowContent ? injectMigrationGate(workflowContent, { cmd: migrationCmd }) : null;
|
|
458
|
+
if (gated) {
|
|
459
|
+
fsSync.writeFileSync(deployYmlPath, gated);
|
|
460
|
+
console.log(color.green('✅ Wired the pre-deploy database migration gate into .github/workflows/deploy.yml'));
|
|
461
|
+
} else {
|
|
462
|
+
log.warn(color.yellow('⚠️ Could not wire the migration gate: the deploy workflow anchor was not found.'));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (driftEnabled) {
|
|
467
|
+
const drifted = scaffoldDriftWorkflow(dirConfig.targetDir, {
|
|
468
|
+
region: config.region,
|
|
469
|
+
roleArn: `arn:aws:iam::${awsAccountId}:role/${dirConfig.actualProjectName}-github-actions-role`,
|
|
470
|
+
});
|
|
471
|
+
if (drifted.ok) {
|
|
472
|
+
console.log(color.green('✅ Scaffolded .github/workflows/drift.yml (scheduled IaC drift detection)'));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 7c. Print-only stack preview when addons were scaffolded (default
|
|
477
|
+
// no-addon output is untouched).
|
|
478
|
+
if (selectedAddons.length > 0) {
|
|
479
|
+
const parsed = parseTerraformConfig(path.join(dirConfig.targetDir, 'terraform'));
|
|
480
|
+
parsed.projectName = dirConfig.actualProjectName;
|
|
481
|
+
parsed.framework = detectedFramework?.name || config.framework;
|
|
482
|
+
await renderDryRunPreview(parsed, true);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 8. Telemetry
|
|
486
|
+
trackEvent('project_provisioned', {
|
|
487
|
+
// 1. Core & Context
|
|
488
|
+
projectName: dirConfig.actualProjectName,
|
|
489
|
+
cli_version: CLI_VERSION,
|
|
490
|
+
is_headless: isHeadless,
|
|
491
|
+
setup_mode: config.setupType,
|
|
492
|
+
duration_ms: Date.now() - startTime,
|
|
493
|
+
|
|
494
|
+
// 2. Infrastructure Shape
|
|
495
|
+
target,
|
|
496
|
+
framework: config.framework,
|
|
497
|
+
specific_framework: detectedFramework?.name || config.framework,
|
|
498
|
+
region: config.region,
|
|
499
|
+
size: config.size,
|
|
500
|
+
desired_count: parseInt(config.desiredCount),
|
|
501
|
+
has_database: config.needsDatabase,
|
|
502
|
+
db_engine: config.needsDatabase ? config.dbEngine : 'none',
|
|
503
|
+
has_custom_health_check: config.healthCheckPath !== '/',
|
|
504
|
+
|
|
505
|
+
// 3. Advanced Features & PaaS Context
|
|
506
|
+
has_worker: willHaveWorker,
|
|
507
|
+
is_heroku_migration: !!procfile,
|
|
508
|
+
is_vercel_migration: !!vercelRules,
|
|
509
|
+
is_docker_compose: !!dockerCompose,
|
|
510
|
+
has_pr_previews: config.enablePrPreviews,
|
|
511
|
+
ai_assistants_configured: config.aiAssistants || [],
|
|
512
|
+
|
|
513
|
+
// 4. Dependency-aware composition
|
|
514
|
+
selected_addons: selectedAddons,
|
|
515
|
+
detected_addons: Object.keys(ADDON_REGISTRY).filter((cap) => capabilities.addons[cap]?.detected === true),
|
|
516
|
+
migration_gate_enabled: migrationGateEnabled,
|
|
517
|
+
drift_detection_enabled: driftEnabled,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
s.stop('Infrastructure provisioned successfully!');
|
|
521
|
+
|
|
522
|
+
// 8.5 Configure AI Context
|
|
523
|
+
s.start('Configuring AI workspace rules...');
|
|
524
|
+
const aiContext = { region: config.region, port: config.port };
|
|
525
|
+
const cwd = dirConfig.targetDir;
|
|
526
|
+
|
|
527
|
+
if (config.setupType === 'advanced') {
|
|
528
|
+
// --- ADVANCED MODE: Explicitly respect user choices ---
|
|
529
|
+
if (config.aiAssistants.includes('cursor')) {
|
|
530
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
531
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
532
|
+
fsSync.writeFileSync(path.join(cursorDir, 'grada.mdc'), getCursorRules(aiContext));
|
|
533
|
+
}
|
|
534
|
+
if (config.aiAssistants.includes('roo')) {
|
|
535
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
536
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
537
|
+
fsSync.writeFileSync(path.join(rooDir, 'grada.md'), getBaseRules(aiContext));
|
|
538
|
+
}
|
|
539
|
+
if (config.aiAssistants.includes('trae')) {
|
|
540
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
541
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
542
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(aiContext), true);
|
|
543
|
+
}
|
|
544
|
+
if (config.aiAssistants.includes('continue')) {
|
|
545
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
546
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
547
|
+
fsSync.writeFileSync(path.join(promptsDir, 'grada.prompt'), getBaseRules(aiContext));
|
|
548
|
+
}
|
|
549
|
+
if (config.aiAssistants.includes('windsurf')) {
|
|
550
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(aiContext), false);
|
|
551
|
+
}
|
|
552
|
+
if (config.aiAssistants.includes('copilot')) {
|
|
553
|
+
const copilotPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
554
|
+
if (!fsSync.existsSync(path.dirname(copilotPath))) fsSync.mkdirSync(path.dirname(copilotPath), { recursive: true });
|
|
555
|
+
injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
|
|
556
|
+
}
|
|
557
|
+
if (config.aiAssistants.includes('claude')) {
|
|
558
|
+
injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(aiContext), true);
|
|
559
|
+
}
|
|
560
|
+
if (config.aiAssistants.includes('goose')) {
|
|
561
|
+
injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(aiContext), true);
|
|
562
|
+
}
|
|
563
|
+
if (config.aiAssistants.includes('aider')) {
|
|
564
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(aiContext), false);
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
// --- QUICKSTART MODE: Silent Auto-Detection ---
|
|
568
|
+
if (fsSync.existsSync(path.join(cwd, '.cursor'))) {
|
|
569
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
570
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
571
|
+
fsSync.writeFileSync(path.join(cursorDir, 'grada.mdc'), getCursorRules(aiContext));
|
|
572
|
+
}
|
|
573
|
+
if (fsSync.existsSync(path.join(cwd, '.roo')) || fsSync.existsSync(path.join(cwd, '.roorules'))) {
|
|
574
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
575
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
576
|
+
fsSync.writeFileSync(path.join(rooDir, 'grada.md'), getBaseRules(aiContext));
|
|
577
|
+
}
|
|
578
|
+
if (fsSync.existsSync(path.join(cwd, '.trae'))) {
|
|
579
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
580
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
581
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(aiContext), true);
|
|
582
|
+
}
|
|
583
|
+
if (fsSync.existsSync(path.join(cwd, '.continue')) || fsSync.existsSync(path.join(cwd, '.prompts'))) {
|
|
584
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
585
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
586
|
+
fsSync.writeFileSync(path.join(promptsDir, 'grada.prompt'), getBaseRules(aiContext));
|
|
587
|
+
}
|
|
588
|
+
if (fsSync.existsSync(path.join(cwd, '.windsurf')) || fsSync.existsSync(path.join(cwd, '.windsurfrules'))) {
|
|
589
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(aiContext), false);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const copilotPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
593
|
+
if (fsSync.existsSync(copilotPath)) injectManagedBlock(copilotPath, getBaseRules(aiContext), true);
|
|
594
|
+
if (fsSync.existsSync(path.join(cwd, 'CLAUDE.md'))) injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(aiContext), true);
|
|
595
|
+
if (fsSync.existsSync(path.join(cwd, '.goosehints'))) injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(aiContext), true);
|
|
596
|
+
|
|
597
|
+
if (fsSync.existsSync(path.join(cwd, '.aider.conf.yml'))) {
|
|
598
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(aiContext), false);
|
|
599
|
+
} else if (fsSync.existsSync(path.join(cwd, '.aider.model.settings.yml'))) {
|
|
600
|
+
injectManagedBlock(path.join(cwd, '.aider.model.settings.yml'), getBaseRules(aiContext), false);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
s.stop('AI rules configured successfully!');
|
|
604
|
+
|
|
605
|
+
// 9. Output
|
|
606
|
+
let frameworkWarnings = '';
|
|
607
|
+
|
|
608
|
+
if (!isPreconfigured && !(config.framework === 'static' && detectedFramework?.buildDir)) {
|
|
609
|
+
frameworkWarnings = getFrameworkWarning(config.framework);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 9b. Post-init capability hints (interactive only, so headless
|
|
613
|
+
// stdout stays byte-identical).
|
|
614
|
+
if (isInteractive) {
|
|
615
|
+
const upcoming = capabilities.upcomingHints;
|
|
616
|
+
if (upcoming.vector) {
|
|
617
|
+
console.log(color.dim(' 💡 Vector-search dependencies detected: managed pgvector support is coming soon.'));
|
|
618
|
+
}
|
|
619
|
+
if (upcoming.cron) {
|
|
620
|
+
console.log(color.dim(' 💡 Scheduled-task dependencies detected: scaffold a schedule with "grada add cron".'));
|
|
621
|
+
}
|
|
622
|
+
if (upcoming.mysql) {
|
|
623
|
+
console.log(color.dim(' 💡 MySQL dependencies detected: RDS currently provisions PostgreSQL; MySQL support is coming soon.'));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const isGitInitialized = fsSync.existsSync(path.join(dirConfig.targetDir, '.git'));
|
|
628
|
+
const needsCd = dirConfig.projectName && dirConfig.projectName !== '.';
|
|
629
|
+
const applyStep = needsCd ? `cd ${dirConfig.projectName} && npx --yes grada-run apply` : 'npx --yes grada-run apply';
|
|
630
|
+
const gitInstructions = isGitInitialized
|
|
631
|
+
? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
|
|
632
|
+
: `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${config.branch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${config.branch}`;
|
|
633
|
+
|
|
634
|
+
let docsTip = '';
|
|
635
|
+
if (procfile) {
|
|
636
|
+
docsTip = `\n ${color.blue('📘 Read the Heroku Migration Guide:')} ${color.underline('https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md')}`;
|
|
637
|
+
} else if (config.needsDatabase) {
|
|
638
|
+
docsTip = `\n ${color.blue('📘 Read the Database Connections Guide:')} ${color.underline('https://github.com/grada-run/grada/blob/main/apps/docs/src/content/docs/guides/database-connections.md')}`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
outro(`${color.green('✅ Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
|
|
642
|
+
${frameworkWarnings ? `\n ${frameworkWarnings}` : ''}
|
|
643
|
+
${color.yellow('Next steps:')}
|
|
644
|
+
1. ${color.cyan(applyStep)}
|
|
645
|
+
2. ${color.cyan(gitInstructions)}
|
|
646
|
+
${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`);
|
|
647
|
+
|
|
648
|
+
await flushTelemetry();
|
|
649
|
+
}
|