@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,164 @@
|
|
|
1
|
+
import { ListTasksCommand, DescribeTasksCommand } from '@aws-sdk/client-ecs';
|
|
2
|
+
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { pickRuntimeContainer } from './ecs.js';
|
|
6
|
+
import { pollUntil } from './system.js';
|
|
7
|
+
|
|
8
|
+
// SSM port-forwarding session arguments for tunneling to a database
|
|
9
|
+
// through an ECS container. `remotePort` is the database port (5432 for
|
|
10
|
+
// PostgreSQL/Aurora, 3306 for MySQL); `localPort` is the loopback port.
|
|
11
|
+
export function buildSsmArgs({ cluster, taskId, runtimeId, dbHost, remotePort = '5432', localPort, region }) {
|
|
12
|
+
const args = [
|
|
13
|
+
'ssm', 'start-session',
|
|
14
|
+
'--target', `ecs:${cluster}_${taskId}_${runtimeId}`,
|
|
15
|
+
'--document-name', 'AWS-StartPortForwardingSessionToRemoteHost',
|
|
16
|
+
'--parameters', `{"host":["${dbHost}"],"portNumber":["${remotePort}"],"localPortNumber":["${localPort}"]}`,
|
|
17
|
+
];
|
|
18
|
+
if (region) args.push('--region', region);
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fetches the managed master credentials for `secretArn`. Returns
|
|
23
|
+
// `{ username, password }`, or `null` when the secret is missing or
|
|
24
|
+
// malformed (callers report SECRET_MALFORMED).
|
|
25
|
+
export async function fetchManagedDbCredentials(secretsClient, secretArn) {
|
|
26
|
+
const secretResp = await secretsClient.send(
|
|
27
|
+
new GetSecretValueCommand({ SecretId: secretArn })
|
|
28
|
+
);
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(secretResp.SecretString || '{}');
|
|
31
|
+
if (parsed.username && parsed.password) {
|
|
32
|
+
return { username: parsed.username, password: parsed.password };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Picks a running ECS task to act as the SSM jump host. Returns
|
|
41
|
+
// `{ taskArn, taskId, containerName, runtimeId }`, or `{ error }` with
|
|
42
|
+
// `NO_RUNNING_TASKS` / `NO_RUNTIME_ID` for the caller to report.
|
|
43
|
+
export async function findJumpHostTarget(ecsClient, { cluster, service, expectedContainer }) {
|
|
44
|
+
const listResp = await ecsClient.send(
|
|
45
|
+
new ListTasksCommand({ cluster, serviceName: service, desiredStatus: 'RUNNING', maxResults: 10 })
|
|
46
|
+
);
|
|
47
|
+
const taskArns = listResp.taskArns || [];
|
|
48
|
+
if (taskArns.length === 0) return { error: 'NO_RUNNING_TASKS' };
|
|
49
|
+
const descResp = await ecsClient.send(
|
|
50
|
+
new DescribeTasksCommand({ cluster, tasks: taskArns.slice(0, 1) })
|
|
51
|
+
);
|
|
52
|
+
const task = (descResp.tasks || [])[0] || null;
|
|
53
|
+
if (!task?.taskArn) return { error: 'NO_RUNNING_TASKS' };
|
|
54
|
+
const container = pickRuntimeContainer(task, expectedContainer);
|
|
55
|
+
const runtimeId = container?.runtimeId;
|
|
56
|
+
if (!runtimeId) return { error: 'NO_RUNTIME_ID' };
|
|
57
|
+
return {
|
|
58
|
+
taskArn: task.taskArn,
|
|
59
|
+
taskId: task.taskArn.split('/').pop(),
|
|
60
|
+
containerName: container?.name,
|
|
61
|
+
runtimeId,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function probeTcpPort(host, port, timeoutMs = 2000) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const socket = net.connect({ host, port });
|
|
68
|
+
const done = (open) => {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
resolve(open);
|
|
71
|
+
};
|
|
72
|
+
socket.once('connect', () => done(true));
|
|
73
|
+
socket.once('error', () => done(false));
|
|
74
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Waits until `host:port` accepts TCP connections (an SSM tunnel coming
|
|
79
|
+
// up). Resolves `{ done: true }`, or `{ timedOut: true }` after `timeoutMs`.
|
|
80
|
+
export async function waitForTcpPort(host, port, { timeoutMs = 30000, pollIntervalMs = 500 } = {}) {
|
|
81
|
+
return pollUntil({
|
|
82
|
+
intervalMs: pollIntervalMs,
|
|
83
|
+
timeoutMs,
|
|
84
|
+
onTick: async () => {
|
|
85
|
+
const open = await probeTcpPort(host, port);
|
|
86
|
+
return open ? { done: true, value: true } : { done: false };
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Allocates an ephemeral free loopback port so background tunnels never
|
|
92
|
+
// collide with a local Postgres/MySQL on 5432/3306.
|
|
93
|
+
export function getFreeLocalPort(host = '127.0.0.1') {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const server = net.createServer();
|
|
96
|
+
server.once('error', reject);
|
|
97
|
+
server.listen(0, host, () => {
|
|
98
|
+
const { port } = server.address();
|
|
99
|
+
server.close(() => resolve(port));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Masks the password in a `scheme://user:password@host/...` URI for safe
|
|
105
|
+
// terminal and error output. URIs without userinfo pass through unchanged.
|
|
106
|
+
export function redactUri(uri) {
|
|
107
|
+
const text = String(uri ?? '');
|
|
108
|
+
const schemeIdx = text.indexOf('://');
|
|
109
|
+
if (schemeIdx === -1) return text;
|
|
110
|
+
const authStart = schemeIdx + 3;
|
|
111
|
+
const slashIdx = text.indexOf('/', authStart);
|
|
112
|
+
const authEnd = slashIdx === -1 ? text.length : slashIdx;
|
|
113
|
+
const authority = text.slice(authStart, authEnd);
|
|
114
|
+
const atIdx = authority.lastIndexOf('@');
|
|
115
|
+
if (atIdx === -1) return text;
|
|
116
|
+
const userinfo = authority.slice(0, atIdx);
|
|
117
|
+
const colonIdx = userinfo.indexOf(':');
|
|
118
|
+
if (colonIdx === -1) return text;
|
|
119
|
+
return text.slice(0, authStart) + userinfo.slice(0, colonIdx) + ':****' + text.slice(authStart + atIdx);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parses a `--from` source URI into discrete connection parts (so passwords
|
|
123
|
+
// travel via process env, never argv). Returns `{ scheme, user, password,
|
|
124
|
+
// host, port, database }`, or `null` when the URI is unusable.
|
|
125
|
+
export function parseSourceUri(uri) {
|
|
126
|
+
const text = String(uri ?? '').trim();
|
|
127
|
+
const match = /^(postgres(?:ql)?|mysql):\/\/(.*)$/i.exec(text);
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
const scheme = match[1].toLowerCase() === 'mysql' ? 'mysql' : 'postgresql';
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
// WHATWG URL requires a valid scheme; normalize postgres:// first.
|
|
133
|
+
parsed = new URL(text.replace(/^[a-z]+:\/\//i, `${scheme}://`));
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (!parsed.hostname) return null;
|
|
138
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, '').split('/')[0] || '');
|
|
139
|
+
if (!database) return null;
|
|
140
|
+
return {
|
|
141
|
+
scheme,
|
|
142
|
+
user: decodeURIComponent(parsed.username || ''),
|
|
143
|
+
password: decodeURIComponent(parsed.password || ''),
|
|
144
|
+
host: parsed.hostname,
|
|
145
|
+
port: parsed.port || (scheme === 'mysql' ? '3306' : '5432'),
|
|
146
|
+
database,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reports which of `names` are missing from PATH (via `<bin> --version`,
|
|
151
|
+
// mirroring the Session Manager plugin check). Pure ENOENT detection:
|
|
152
|
+
// a binary that spawns at all counts as present.
|
|
153
|
+
export function findMissingBinaries(names, { spawnSyncImpl = spawnSync } = {}) {
|
|
154
|
+
const missing = [];
|
|
155
|
+
for (const name of names || []) {
|
|
156
|
+
try {
|
|
157
|
+
const result = spawnSyncImpl(name, ['--version'], { stdio: 'ignore' });
|
|
158
|
+
if (result && result.error && result.error.code === 'ENOENT') missing.push(name);
|
|
159
|
+
} catch {
|
|
160
|
+
missing.push(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return missing;
|
|
164
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export { detectProjectCapabilities } from './capabilities.js';
|
|
5
|
+
|
|
6
|
+
// Detects the framework based on the presence of framework-specific files.
|
|
7
|
+
export function detectFramework(targetDir) {
|
|
8
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
9
|
+
const requirementsTxtPath = path.join(targetDir, 'requirements.txt');
|
|
10
|
+
const managePyPath = path.join(targetDir, 'manage.py');
|
|
11
|
+
const goModPath = path.join(targetDir, 'go.mod');
|
|
12
|
+
const gemfilePath = path.join(targetDir, 'Gemfile');
|
|
13
|
+
|
|
14
|
+
// 1. Detect Node.js Frameworks
|
|
15
|
+
if (fsSync.existsSync(packageJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf-8'));
|
|
18
|
+
// Merge dependencies and devDependencies to check both
|
|
19
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
20
|
+
|
|
21
|
+
// Fullstack / API
|
|
22
|
+
if (deps['@nestjs/core']) return { id: 'nestjs', name: 'NestJS' };
|
|
23
|
+
if (deps['next']) return { id: 'nextjs', name: 'Next.js' };
|
|
24
|
+
if (deps['nuxt']) return { id: 'nuxt', name: 'Nuxt 3 (SSR)' };
|
|
25
|
+
if (deps['express']) return { id: 'node', name: 'Node.js / Express' };
|
|
26
|
+
|
|
27
|
+
// Explicit SvelteKit SSR Detection
|
|
28
|
+
if (deps['@sveltejs/kit']) return { id: 'svelte', name: 'SvelteKit SSR', buildDir: 'build' };
|
|
29
|
+
|
|
30
|
+
// Static Site Generators & SPAs (with precise build directories)
|
|
31
|
+
if (deps['@sveltejs/kit']) return { id: 'static', name: 'SvelteKit', buildDir: 'build' };
|
|
32
|
+
if (deps['react-scripts']) return { id: 'static', name: 'Create React App', buildDir: 'build' };
|
|
33
|
+
if (deps['gatsby']) return { id: 'static', name: 'Gatsby', buildDir: 'public' };
|
|
34
|
+
if (deps['astro']) return { id: 'static', name: 'Astro', buildDir: 'dist' };
|
|
35
|
+
if (deps['vite']) return { id: 'static', name: 'Vite', buildDir: 'dist' };
|
|
36
|
+
if (deps['@vue/cli-service']) return { id: 'static', name: 'Vue.js', buildDir: 'dist' };
|
|
37
|
+
if (deps['@angular/cli']) return { id: 'static', name: 'Angular', buildDir: 'dist' };
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Silently fail if package.json is malformed
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Detect Python Frameworks
|
|
44
|
+
if (fsSync.existsSync(requirementsTxtPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const reqs = fsSync.readFileSync(requirementsTxtPath, 'utf-8').toLowerCase();
|
|
47
|
+
if (reqs.includes('fastapi')) return { id: 'python', name: 'Python FastAPI' };
|
|
48
|
+
if (reqs.includes('django')) return { id: 'django', name: 'Django' };
|
|
49
|
+
} catch (e) {
|
|
50
|
+
// Silently fail
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback Django detection (sometimes requirements.txt is named differently)
|
|
55
|
+
if (fsSync.existsSync(managePyPath)) return { id: 'django', name: 'Django' };
|
|
56
|
+
|
|
57
|
+
// 3. Detect Ruby on Rails
|
|
58
|
+
if (fsSync.existsSync(gemfilePath)) {
|
|
59
|
+
try {
|
|
60
|
+
const gemfile = fsSync.readFileSync(gemfilePath, 'utf-8').toLowerCase();
|
|
61
|
+
if (/gem\s+['"]rails['"]/i.test(gemfile)) { return { id: 'rails', name: 'Ruby on Rails' }; }
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Silently fail
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Detect Go
|
|
68
|
+
if (fsSync.existsSync(goModPath)) return { id: 'go', name: 'Go' };
|
|
69
|
+
|
|
70
|
+
// 5. Fallback
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Splits a Procfile-style command into Terraform's JSON-array form,
|
|
75
|
+
// respecting single and double quotes. Shared by Procfile parsing and
|
|
76
|
+
// the interactive worker-command prompt so both produce identical arrays.
|
|
77
|
+
export function splitProcfileCommand(rawCommand) {
|
|
78
|
+
const match = String(rawCommand || '').match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
|
|
79
|
+
if (!match) return [];
|
|
80
|
+
return match.map(str => str.replace(/^["']|["']$/g, '')); // Strip the quotes
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Parses a Heroku/Render Procfile and formats the commands for Terraform ECS.
|
|
84
|
+
export function parseProcfile(targetDir) {
|
|
85
|
+
const procfilePath = path.join(targetDir, 'Procfile');
|
|
86
|
+
|
|
87
|
+
if (!fsSync.existsSync(procfilePath)) return null;
|
|
88
|
+
|
|
89
|
+
const content = fsSync.readFileSync(procfilePath, 'utf-8');
|
|
90
|
+
const processes = {};
|
|
91
|
+
|
|
92
|
+
// Match lines like "web: gunicorn myapp.wsgi"
|
|
93
|
+
const lines = content.split('\n');
|
|
94
|
+
const procRegex = /^([A-Za-z0-9_-]+):\s*(.+)$/;
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const match = line.trim().match(procRegex);
|
|
98
|
+
if (match) {
|
|
99
|
+
const type = match[1].toLowerCase();
|
|
100
|
+
const rawCommand = match[2].trim();
|
|
101
|
+
|
|
102
|
+
// Terraform requires the command as a JSON array of strings
|
|
103
|
+
const commandArray = splitProcfileCommand(rawCommand);
|
|
104
|
+
|
|
105
|
+
processes[type] = commandArray;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.keys(processes).length > 0 ? processes : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parses a vercel.json file to extract routing and edge rules
|
|
113
|
+
export function parseVercelConfig(targetDir) {
|
|
114
|
+
const vercelConfigPath = path.join(targetDir, 'vercel.json');
|
|
115
|
+
if (!fsSync.existsSync(vercelConfigPath)) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const content = fsSync.readFileSync(vercelConfigPath, 'utf-8');
|
|
119
|
+
const vercelJson = JSON.parse(content);
|
|
120
|
+
|
|
121
|
+
// We only care about network-level edge rules that AWS needs to handle
|
|
122
|
+
const rules = {
|
|
123
|
+
redirects: vercelJson.redirects || null,
|
|
124
|
+
headers: vercelJson.headers || null,
|
|
125
|
+
rewrites: vercelJson.rewrites || null
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// If it's just an empty vercel.json, return null
|
|
129
|
+
if (!rules.redirects && !rules.headers && !rules.rewrites) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return rules;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// Silently fail on malformed JSON
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Checks if Next.js is configured for 'standalone' output
|
|
141
|
+
export function analyzeNextConfig(targetDir) {
|
|
142
|
+
const extensions = ['js', 'mjs', 'cjs', 'ts'];
|
|
143
|
+
let configPath = null;
|
|
144
|
+
let configContent = '';
|
|
145
|
+
|
|
146
|
+
for (const ext of extensions) {
|
|
147
|
+
const tempPath = path.join(targetDir, `next.config.${ext}`);
|
|
148
|
+
if (fsSync.existsSync(tempPath)) {
|
|
149
|
+
configPath = tempPath;
|
|
150
|
+
configContent = fsSync.readFileSync(tempPath, 'utf-8');
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!configPath) return { hasConfig: false, isStandalone: false };
|
|
156
|
+
|
|
157
|
+
// Regex looks for output: 'standalone' or output: "standalone" (handling spacing)
|
|
158
|
+
const isStandalone = /output\s*:\s*['"`]standalone['"`]/.test(configContent);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
hasConfig: true,
|
|
162
|
+
isStandalone: isStandalone,
|
|
163
|
+
configPath: configPath
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Checks if SvelteKit is locked into Vercel
|
|
168
|
+
export function analyzeSvelteConfig(targetDir) {
|
|
169
|
+
const configPath = path.join(targetDir, 'svelte.config.js');
|
|
170
|
+
if (!fsSync.existsSync(configPath)) return { hasConfig: false, adapter: 'unknown' };
|
|
171
|
+
|
|
172
|
+
const content = fsSync.readFileSync(configPath, 'utf-8');
|
|
173
|
+
|
|
174
|
+
let adapter = 'unknown';
|
|
175
|
+
if (content.includes('@sveltejs/adapter-vercel')) adapter = 'vercel';
|
|
176
|
+
else if (content.includes('@sveltejs/adapter-node')) adapter = 'node';
|
|
177
|
+
else if (content.includes('@sveltejs/adapter-static')) adapter = 'static';
|
|
178
|
+
else if (content.includes('@sveltejs/adapter-auto')) adapter = 'auto'; // Vercel's default
|
|
179
|
+
|
|
180
|
+
return { hasConfig: true, adapter };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Checks if Astro is locked into Vercel
|
|
184
|
+
export function analyzeAstroConfig(targetDir) {
|
|
185
|
+
const extensions = ['mjs', 'js', 'ts', 'cjs'];
|
|
186
|
+
let configPath = null;
|
|
187
|
+
let content = '';
|
|
188
|
+
|
|
189
|
+
for (const ext of extensions) {
|
|
190
|
+
const tempPath = path.join(targetDir, `astro.config.${ext}`);
|
|
191
|
+
if (fsSync.existsSync(tempPath)) {
|
|
192
|
+
configPath = tempPath;
|
|
193
|
+
content = fsSync.readFileSync(tempPath, 'utf-8');
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!configPath) return { hasConfig: false, adapter: 'unknown' };
|
|
199
|
+
|
|
200
|
+
let adapter = 'unknown';
|
|
201
|
+
if (content.includes('@astrojs/vercel')) adapter = 'vercel';
|
|
202
|
+
else if (content.includes('@astrojs/node')) adapter = 'node';
|
|
203
|
+
|
|
204
|
+
return { hasConfig: true, adapter };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Detects the project's schema-migration command from well-known ORM and
|
|
208
|
+
// framework markers. Pure: reads files under `cwd`, never throws (malformed
|
|
209
|
+
// or unreadable files fall through to the next marker). Returns the shell
|
|
210
|
+
// command string or null when nothing matches.
|
|
211
|
+
export function detectMigrationCommand(cwd = process.cwd()) {
|
|
212
|
+
const readJson = (file) => {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(fsSync.readFileSync(path.join(cwd, file), 'utf-8'));
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const exists = (file) => {
|
|
220
|
+
try {
|
|
221
|
+
return fsSync.existsSync(path.join(cwd, file));
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// 1. Explicit package.json migration scripts win over framework markers.
|
|
228
|
+
const pkg = readJson('package.json');
|
|
229
|
+
const scripts = pkg && typeof pkg === 'object' ? pkg.scripts || {} : {};
|
|
230
|
+
if (typeof scripts['db:migrate'] === 'string' && scripts['db:migrate'].trim()) {
|
|
231
|
+
return 'npm run db:migrate';
|
|
232
|
+
}
|
|
233
|
+
if (typeof scripts.migrate === 'string' && scripts.migrate.trim()) {
|
|
234
|
+
return 'npm run migrate';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 2. Prisma: schema file or dependency in either dep block.
|
|
238
|
+
const deps = pkg && typeof pkg === 'object'
|
|
239
|
+
? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
|
240
|
+
: {};
|
|
241
|
+
if (exists(path.join('prisma', 'schema.prisma')) || deps.prisma) {
|
|
242
|
+
return 'npx prisma migrate deploy';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3. Drizzle: config file in any supported extension.
|
|
246
|
+
if (exists('drizzle.config.ts') || exists('drizzle.config.js') || exists('drizzle.config.mjs')) {
|
|
247
|
+
return 'npx drizzle-kit migrate';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 4. Alembic.
|
|
251
|
+
if (exists('alembic.ini')) {
|
|
252
|
+
return 'alembic upgrade head';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 5. Django.
|
|
256
|
+
if (exists('manage.py')) {
|
|
257
|
+
return 'python manage.py migrate --noinput';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 6. Ruby on Rails: binstub or Gemfile rails dependency.
|
|
261
|
+
if (exists(path.join('bin', 'rails'))) {
|
|
262
|
+
return 'bundle exec rails db:migrate';
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const gemfilePath = path.join(cwd, 'Gemfile');
|
|
266
|
+
if (fsSync.existsSync(gemfilePath)) {
|
|
267
|
+
const gemfile = fsSync.readFileSync(gemfilePath, 'utf-8');
|
|
268
|
+
if (/gem\s+['"]rails['"]/i.test(gemfile)) {
|
|
269
|
+
return 'bundle exec rails db:migrate';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Fall through to null below.
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Checks if a NestJS app explicitly listens on 0.0.0.0 for Docker networking
|
|
280
|
+
export function analyzeNestApp(targetDir) {
|
|
281
|
+
const mainTsPath = path.join(targetDir, 'src', 'main.ts');
|
|
282
|
+
const mainJsPath = path.join(targetDir, 'src', 'main.js');
|
|
283
|
+
|
|
284
|
+
const filePath = fsSync.existsSync(mainTsPath) ? mainTsPath : (fsSync.existsSync(mainJsPath) ? mainJsPath : null);
|
|
285
|
+
if (!filePath) return { hasMain: false, listensOnAllInterfaces: true };
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
289
|
+
const hasHostBinding = content.includes('0.0.0.0');
|
|
290
|
+
return {
|
|
291
|
+
hasMain: true,
|
|
292
|
+
listensOnAllInterfaces: hasHostBinding,
|
|
293
|
+
filePath: path.relative(targetDir, filePath)
|
|
294
|
+
};
|
|
295
|
+
} catch {
|
|
296
|
+
return { hasMain: false, listensOnAllInterfaces: true };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { load } from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
// Parse docker-compose.yml (or its aliases) and normalize it into an
|
|
6
|
+
// array of services. Filenames are checked in canonical order.
|
|
7
|
+
const COMPOSE_FILENAMES = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
|
|
8
|
+
|
|
9
|
+
export function parseDockerCompose(targetDir) {
|
|
10
|
+
let composePath = null;
|
|
11
|
+
for (const filename of COMPOSE_FILENAMES) {
|
|
12
|
+
const candidate = path.join(targetDir, filename);
|
|
13
|
+
if (fs.existsSync(candidate)) {
|
|
14
|
+
composePath = candidate;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!composePath) return null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const fileContents = fs.readFileSync(composePath, 'utf8');
|
|
23
|
+
const data = load(fileContents);
|
|
24
|
+
|
|
25
|
+
if (!data || !data.services) return null;
|
|
26
|
+
|
|
27
|
+
const services = [];
|
|
28
|
+
|
|
29
|
+
for (const [name, config] of Object.entries(data.services)) {
|
|
30
|
+
// Extract the container port (e.g., "8080:80" -> 80)
|
|
31
|
+
let exposedPort = null;
|
|
32
|
+
if (config.ports && config.ports.length > 0) {
|
|
33
|
+
// Handle different port formats like "3000" or "8000:8000" or "127.0.0.1:8001:8001"
|
|
34
|
+
const portMapping = config.ports[0].toString();
|
|
35
|
+
const parts = portMapping.split(':');
|
|
36
|
+
exposedPort = parseInt(parts[parts.length - 1].replace(/[^0-9]/g, ''), 10);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Normalize environment variables from Array or Object
|
|
40
|
+
let envVars = {};
|
|
41
|
+
if (Array.isArray(config.environment)) {
|
|
42
|
+
config.environment.forEach(env => {
|
|
43
|
+
const [key, ...val] = env.split('=');
|
|
44
|
+
if (key) envVars[key] = val.join('=');
|
|
45
|
+
});
|
|
46
|
+
} else if (typeof config.environment === 'object') {
|
|
47
|
+
envVars = config.environment;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
services.push({
|
|
51
|
+
name,
|
|
52
|
+
build: config.build || null,
|
|
53
|
+
image: config.image || null,
|
|
54
|
+
port: exposedPort,
|
|
55
|
+
command: config.command || null,
|
|
56
|
+
environment: envVars
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return services;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('⚠️ Failed to parse Docker Compose file:', e.message);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Shared domain/zone/email validation and `domain.tf` parsing for the
|
|
2
|
+
// `domain` command and `add email:ses`. Both commands consume this module
|
|
3
|
+
// so their normalization rules can never drift apart. Dependency-free.
|
|
4
|
+
export const DOMAIN_REGEX = /^(?=.{1,253}$)(\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
5
|
+
export const ZONE_ID_REGEX = /^(?:\/hostedzone\/)?(Z[A-Z0-9]{1,32})$/;
|
|
6
|
+
export const FROM_EMAIL_REGEX = /^[^\s@"'\\]+@[^\s@"'\\]+\.[^\s@"'\\]+$/;
|
|
7
|
+
|
|
8
|
+
export const DOMAIN_MODES = ['route53', 'external-pending', 'external-active'];
|
|
9
|
+
|
|
10
|
+
// Trims whitespace, strips one trailing FQDN dot, and lowercases.
|
|
11
|
+
// Non-string input normalizes to '' so callers fail validation instead
|
|
12
|
+
// of throwing.
|
|
13
|
+
export function normalizeDomain(input) {
|
|
14
|
+
if (typeof input !== 'string') return '';
|
|
15
|
+
return input.trim().toLowerCase().replace(/\.$/, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Validates a normalized domain (with an optional `*.` wildcard prefix).
|
|
19
|
+
export function isValidDomain(domain) {
|
|
20
|
+
return DOMAIN_REGEX.test(normalizeDomain(domain));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Strips an optional `/hostedzone/` prefix (as copied from the AWS
|
|
24
|
+
// console) and validates the `Z...` shape. Returns the canonical zone
|
|
25
|
+
// ID, or null when invalid.
|
|
26
|
+
export function normalizeZoneId(input) {
|
|
27
|
+
if (typeof input !== 'string') return null;
|
|
28
|
+
const match = ZONE_ID_REGEX.exec(input.trim());
|
|
29
|
+
return match ? match[1] : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validates sender-address syntax and that the address belongs to
|
|
33
|
+
// `expectedDomain` (or one of its subdomains), so SES sends never fail
|
|
34
|
+
// at runtime against an unverified identity. Wildcards are rejected on
|
|
35
|
+
// both sides: SES domain identities do not support them.
|
|
36
|
+
export function isValidFromEmail(email, expectedDomain) {
|
|
37
|
+
if (typeof email !== 'string' || typeof expectedDomain !== 'string') return false;
|
|
38
|
+
const address = email.trim();
|
|
39
|
+
if (!FROM_EMAIL_REGEX.test(address)) return false;
|
|
40
|
+
if (address.includes('*')) return false;
|
|
41
|
+
const domain = normalizeDomain(expectedDomain);
|
|
42
|
+
if (!domain || domain.includes('*')) return false;
|
|
43
|
+
const addressDomain = address.slice(address.lastIndexOf('@') + 1).toLowerCase();
|
|
44
|
+
return addressDomain === domain || addressDomain.endsWith(`.${domain}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parses a `terraform/domain.tf` document into `{ domain, mode, zoneId }`
|
|
48
|
+
// (fields are null when absent). Mode comes from the
|
|
49
|
+
// `# grada:domain-mode=<mode>` marker (legacy `# deploy-stack:` files
|
|
50
|
+
// still parse) with structural fallbacks for hand-edited files; the
|
|
51
|
+
// zone ID prefers the explicit `# grada:zone-id=<id>` marker over
|
|
52
|
+
// `zone_id` attributes.
|
|
53
|
+
export function parseDomainTf(content) {
|
|
54
|
+
const text = String(content ?? '');
|
|
55
|
+
if (!text.trim()) return null;
|
|
56
|
+
const modeMatch = text.match(/#\s*(?:grada|deploy-stack):domain-mode=([a-z-]+)/);
|
|
57
|
+
let mode = modeMatch && DOMAIN_MODES.includes(modeMatch[1]) ? modeMatch[1] : null;
|
|
58
|
+
if (!mode) {
|
|
59
|
+
if (text.includes('"cdn_alias_a"')) mode = 'route53';
|
|
60
|
+
else if (text.includes('"aws_acm_certificate_validation"')) mode = 'external-active';
|
|
61
|
+
else if (text.includes('"aws_acm_certificate"')) mode = 'external-pending';
|
|
62
|
+
}
|
|
63
|
+
const domainMatch = text.match(/domain_name\s*=\s*"([^"]+)"/);
|
|
64
|
+
const domain = domainMatch ? normalizeDomain(domainMatch[1]) : null;
|
|
65
|
+
let zoneId = null;
|
|
66
|
+
const zoneMarker = text.match(/#\s*(?:grada|deploy-stack):zone-id=([A-Za-z0-9/_-]+)/);
|
|
67
|
+
if (zoneMarker) zoneId = normalizeZoneId(zoneMarker[1]);
|
|
68
|
+
if (!zoneId) {
|
|
69
|
+
const zoneAttr = text.match(/zone_id\s*=\s*"([^"]+)"/);
|
|
70
|
+
if (zoneAttr) zoneId = normalizeZoneId(zoneAttr[1]);
|
|
71
|
+
}
|
|
72
|
+
return { domain: domain || null, mode, zoneId };
|
|
73
|
+
}
|