@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,115 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { intro, outro, spinner, log } from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
import { getAiAssistants } from '../utils/prompts.js';
|
|
6
|
+
import { getBaseRules, getCursorRules, injectManagedBlock } from '../utils/ai-rules.js';
|
|
7
|
+
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
8
|
+
import { failCommand } from '../utils/command.js';
|
|
9
|
+
|
|
10
|
+
function getProjectContext(cwd) {
|
|
11
|
+
const context = { region: '', port: '' };
|
|
12
|
+
const mainTfPath = path.join(cwd, 'terraform', 'main.tf');
|
|
13
|
+
|
|
14
|
+
if (fsSync.existsSync(mainTfPath)) {
|
|
15
|
+
const tfContent = fsSync.readFileSync(mainTfPath, 'utf8');
|
|
16
|
+
|
|
17
|
+
// Extract Region
|
|
18
|
+
const regionMatch = tfContent.match(/region\s*=\s*"([^"]+)"/);
|
|
19
|
+
if (regionMatch) context.region = regionMatch[1];
|
|
20
|
+
|
|
21
|
+
// Extract Port
|
|
22
|
+
const portMatch = tfContent.match(/containerPort\s*=\s*(\d+)/);
|
|
23
|
+
if (portMatch) context.port = portMatch[1];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function syncAi() {
|
|
30
|
+
intro(color.bgCyan(color.black(' grada sync-ai 🤖 ')));
|
|
31
|
+
|
|
32
|
+
const assistants = await getAiAssistants();
|
|
33
|
+
|
|
34
|
+
if (!assistants || assistants.length === 0) {
|
|
35
|
+
log.warn('No AI assistants selected. Skipping synchronization.');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const s = spinner();
|
|
40
|
+
s.start('Writing AI context rules...');
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
|
|
43
|
+
const context = getProjectContext(cwd);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// --- Agentic IDEs ---
|
|
47
|
+
if (assistants.includes('cursor')) {
|
|
48
|
+
const cursorDir = path.join(cwd, '.cursor', 'rules');
|
|
49
|
+
if (!fsSync.existsSync(cursorDir)) fsSync.mkdirSync(cursorDir, { recursive: true });
|
|
50
|
+
fsSync.writeFileSync(path.join(cursorDir, 'grada.mdc'), getCursorRules(context));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (assistants.includes('roo')) {
|
|
54
|
+
const rooDir = path.join(cwd, '.roo', 'rules');
|
|
55
|
+
if (!fsSync.existsSync(rooDir)) fsSync.mkdirSync(rooDir, { recursive: true });
|
|
56
|
+
fsSync.writeFileSync(path.join(rooDir, 'grada.md'), getBaseRules(context));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (assistants.includes('trae')) {
|
|
60
|
+
const traeDir = path.join(cwd, '.trae', 'rules');
|
|
61
|
+
if (!fsSync.existsSync(traeDir)) fsSync.mkdirSync(traeDir, { recursive: true });
|
|
62
|
+
injectManagedBlock(path.join(traeDir, 'project_rules.md'), getBaseRules(context), true);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (assistants.includes('continue')) {
|
|
66
|
+
const promptsDir = path.join(cwd, '.prompts');
|
|
67
|
+
if (!fsSync.existsSync(promptsDir)) fsSync.mkdirSync(promptsDir, { recursive: true });
|
|
68
|
+
fsSync.writeFileSync(path.join(promptsDir, 'grada.prompt'), getBaseRules(context));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (assistants.includes('windsurf')) {
|
|
72
|
+
injectManagedBlock(path.join(cwd, '.windsurfrules'), getBaseRules(context), false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (assistants.includes('copilot')) {
|
|
76
|
+
const githubDir = path.join(cwd, '.github');
|
|
77
|
+
if (!fsSync.existsSync(githubDir)) fsSync.mkdirSync(githubDir, { recursive: true });
|
|
78
|
+
injectManagedBlock(path.join(githubDir, 'copilot-instructions.md'), getBaseRules(context), true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Terminal Agents ---
|
|
82
|
+
if (assistants.includes('claude')) {
|
|
83
|
+
injectManagedBlock(path.join(cwd, 'CLAUDE.md'), getBaseRules(context), true);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (assistants.includes('goose')) {
|
|
87
|
+
injectManagedBlock(path.join(cwd, '.goosehints'), getBaseRules(context), true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (assistants.includes('aider')) {
|
|
91
|
+
injectManagedBlock(path.join(cwd, '.aider.conf.yml'), getBaseRules(context), false); // Uses # comments
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
s.stop('AI context synchronized successfully!');
|
|
95
|
+
outro(`${color.green('✅ AI Assistant Rules generated!')} Your AI tools now know exactly how to deploy your app without hallucinating Terraform.`);
|
|
96
|
+
|
|
97
|
+
trackEvent('sync_ai_executed', {
|
|
98
|
+
assistants_selected: assistants,
|
|
99
|
+
has_cursor: assistants.includes('cursor'),
|
|
100
|
+
has_roo: assistants.includes('roo'),
|
|
101
|
+
has_trae: assistants.includes('trae'),
|
|
102
|
+
has_continue: assistants.includes('continue'),
|
|
103
|
+
has_windsurf: assistants.includes('windsurf'),
|
|
104
|
+
has_copilot: assistants.includes('copilot'),
|
|
105
|
+
has_claude: assistants.includes('claude'),
|
|
106
|
+
has_goose: assistants.includes('goose'),
|
|
107
|
+
has_aider: assistants.includes('aider')
|
|
108
|
+
});
|
|
109
|
+
await flushTelemetry();
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
s.stop('❌ Failed to write AI context files.');
|
|
113
|
+
return failCommand({ message: error.message, useErrorStream: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { ECSClient, DescribeServicesCommand, UpdateServiceCommand } from '@aws-sdk/client-ecs';
|
|
3
|
+
import { RDSClient, DescribeDBInstancesCommand, DescribeDBClustersCommand, StartDBInstanceCommand, StartDBClusterCommand } from '@aws-sdk/client-rds';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
import { intro, outro, spinner } from '@clack/prompts';
|
|
6
|
+
import { trackSuccess, trackFailure } from '../core/telemetry.js';
|
|
7
|
+
import { failCommand, failProjectNotInitialized } from '../utils/command.js';
|
|
8
|
+
import { parseFlags, normalizeOptions, normalizeArgv } from '../utils/args.js';
|
|
9
|
+
import { handleAuthErrorBranch, resolveClient } from '../utils/aws.js';
|
|
10
|
+
import { resolveRegion, resolveProjectName, resolveCwd, readTerraformComputeTarget } from '../utils/resolvers.js';
|
|
11
|
+
import { findDbTarget } from '../utils/rds.js';
|
|
12
|
+
import { fetchActiveService } from '../utils/ecs.js';
|
|
13
|
+
import { pollUntil } from '../utils/system.js';
|
|
14
|
+
import {
|
|
15
|
+
resolveSleepTarget,
|
|
16
|
+
readSleepState,
|
|
17
|
+
removeSleepStateEntry,
|
|
18
|
+
} from '../utils/sleep-state.js';
|
|
19
|
+
import {
|
|
20
|
+
readCronScheduleName,
|
|
21
|
+
hasCronAddon,
|
|
22
|
+
hasSqsWorkerScaling,
|
|
23
|
+
setSchedulerState,
|
|
24
|
+
setWorkerScalingSuspended,
|
|
25
|
+
} from '../utils/sleep-targets.js';
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_WAKE_TIMEOUT_MS = 600000;
|
|
28
|
+
export const DEFAULT_WAKE_POLL_INTERVAL_MS = 5000;
|
|
29
|
+
|
|
30
|
+
export function parseWakeArgs(argv = []) {
|
|
31
|
+
const args = normalizeArgv(argv);
|
|
32
|
+
if (args[0] === 'wake') args.shift();
|
|
33
|
+
const { options, rest } = parseFlags(args, {
|
|
34
|
+
string: ['project-name', 'cluster', 'service', 'db-identifier', 'region', 'workspace'],
|
|
35
|
+
boolean: ['skip-db', 'wait', 'no-wait', 'yes', 'force', 'headless'],
|
|
36
|
+
});
|
|
37
|
+
const positionals = rest.filter((arg) => typeof arg === 'string' && !arg.startsWith('-'));
|
|
38
|
+
if (positionals.length > 0) options.env = positionals[0];
|
|
39
|
+
if (positionals.length > 1) options.unexpectedPositionals = positionals.slice(1);
|
|
40
|
+
return options;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runWake(input = {}) {
|
|
44
|
+
const options = normalizeOptions(input);
|
|
45
|
+
let cwd;
|
|
46
|
+
let region;
|
|
47
|
+
let projectName;
|
|
48
|
+
try {
|
|
49
|
+
cwd = resolveCwd(options);
|
|
50
|
+
region = resolveRegion(options, cwd);
|
|
51
|
+
projectName = resolveProjectName(options, cwd);
|
|
52
|
+
} catch {
|
|
53
|
+
return failProjectNotInitialized({ event: 'wake_run' });
|
|
54
|
+
}
|
|
55
|
+
const target = resolveSleepTarget(options, cwd);
|
|
56
|
+
const isLambda = readTerraformComputeTarget(cwd) === 'lambda';
|
|
57
|
+
const skipDb = options.skipDb === true || options.skipDb === 'true';
|
|
58
|
+
const noWait = options.noWait === true || options.noWait === 'true'
|
|
59
|
+
|| options.wait === false || options.wait === 'false';
|
|
60
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
|
|
61
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_WAKE_POLL_INTERVAL_MS;
|
|
62
|
+
|
|
63
|
+
intro(color.bgCyan(color.black(' grada wake ☀️ ')));
|
|
64
|
+
|
|
65
|
+
if (Array.isArray(options.unexpectedPositionals) && options.unexpectedPositionals.length > 0) {
|
|
66
|
+
return failCommand({
|
|
67
|
+
message: `\n✖ Unexpected argument "${options.unexpectedPositionals[0]}". Pass a single environment: wake [env].\n`,
|
|
68
|
+
event: 'wake_run',
|
|
69
|
+
telemetry: { projectName },
|
|
70
|
+
errorCode: 'UNEXPECTED_POSITIONAL_ARGS',
|
|
71
|
+
reason: 'unexpected-positional-args',
|
|
72
|
+
resultExtra: { cluster: target.cluster, region },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entry = readSleepState(cwd)[target.envKey] || null;
|
|
77
|
+
if (entry?.autoRestartAt && Date.now() > Date.parse(entry.autoRestartAt)) {
|
|
78
|
+
console.log(color.yellow('\n⚠ The 7-day sleep window has elapsed — AWS may have auto-restarted the database already.'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ecsClient = resolveClient(options.ecsClient, ECSClient, { region });
|
|
82
|
+
const rdsClient = resolveClient(options.rdsClient, RDSClient, { region });
|
|
83
|
+
const runSync = options.spawnSyncImpl || spawnSync;
|
|
84
|
+
|
|
85
|
+
const s = spinner();
|
|
86
|
+
s.start(`Waking ${target.envKey}...`);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// 1. Start the database first so booting containers never
|
|
90
|
+
// crash-loop on an unreachable database.
|
|
91
|
+
let dbTarget = null;
|
|
92
|
+
let dbStarted = false;
|
|
93
|
+
if (!skipDb) {
|
|
94
|
+
dbTarget = await findDbTarget(rdsClient, {
|
|
95
|
+
dbIdentifier: target.dbIdentifier,
|
|
96
|
+
dbClusterIdentifier: target.dbClusterIdentifier,
|
|
97
|
+
});
|
|
98
|
+
if (dbTarget && dbTarget.status === 'stopped') {
|
|
99
|
+
if (dbTarget.kind === 'cluster') {
|
|
100
|
+
await rdsClient.send(new StartDBClusterCommand({ DBClusterIdentifier: dbTarget.id }));
|
|
101
|
+
} else {
|
|
102
|
+
await rdsClient.send(new StartDBInstanceCommand({ DBInstanceIdentifier: dbTarget.id }));
|
|
103
|
+
}
|
|
104
|
+
dbStarted = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!noWait && dbTarget && dbStarted) {
|
|
109
|
+
const isCluster = dbTarget.kind === 'cluster';
|
|
110
|
+
s.message(`Waiting for ${dbTarget.id} to become available...`);
|
|
111
|
+
const outcome = await pollUntil({
|
|
112
|
+
intervalMs: pollIntervalMs,
|
|
113
|
+
timeoutMs,
|
|
114
|
+
...(options.sleepFn ? { sleepFn: options.sleepFn } : {}),
|
|
115
|
+
...(options.nowFn ? { nowFn: options.nowFn } : {}),
|
|
116
|
+
onTick: async ({ elapsedMs }) => {
|
|
117
|
+
const resp = isCluster
|
|
118
|
+
? await rdsClient.send(new DescribeDBClustersCommand({ DBClusterIdentifier: dbTarget.id }))
|
|
119
|
+
: await rdsClient.send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: dbTarget.id }));
|
|
120
|
+
const current = isCluster
|
|
121
|
+
? (resp.DBClusters || [])[0] || null
|
|
122
|
+
: (resp.DBInstances || [])[0] || null;
|
|
123
|
+
if (current && current.Status === 'available') return { done: true, value: current };
|
|
124
|
+
s.message(`Waiting for ${dbTarget.id}... [${Math.floor(elapsedMs / 1000)}s]`);
|
|
125
|
+
return { done: false };
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (outcome.timedOut) {
|
|
129
|
+
s.stop(color.yellow('Database still starting.'));
|
|
130
|
+
return failCommand({
|
|
131
|
+
print: () => {
|
|
132
|
+
console.log(color.yellow(`\n⚠ ${dbTarget.id} did not become available in time.`));
|
|
133
|
+
console.log(isLambda
|
|
134
|
+
? ` The environment is still asleep — re-run ${color.green(`npx grada-run wake${target.envKey === 'default' ? '' : ` ${target.envKey}`}`)} once the database is available.\n`
|
|
135
|
+
: ` ECS services were left asleep — re-run ${color.green(`npx grada-run wake${target.envKey === 'default' ? '' : ` ${target.envKey}`}`)} once the database is available.\n`);
|
|
136
|
+
},
|
|
137
|
+
event: 'wake_run',
|
|
138
|
+
telemetry: { projectName, env_kind: target.envKind },
|
|
139
|
+
errorCode: 'RDS_WAKE_TIMEOUT',
|
|
140
|
+
reason: 'rds-wake-timeout',
|
|
141
|
+
resultExtra: { cluster: target.cluster, region },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Restore ECS desired counts (ledger first, then 1/1 defaults).
|
|
147
|
+
// Lambda ledgers store 0/0: there is nothing to restore.
|
|
148
|
+
const desiredApp = isLambda ? (entry?.services?.app ?? 0) : Math.max(1, entry?.services?.app ?? 1);
|
|
149
|
+
const desiredWorker = isLambda ? (entry?.services?.worker ?? 0) : Math.max(1, entry?.services?.worker ?? 1);
|
|
150
|
+
const restored = [];
|
|
151
|
+
for (const [serviceName, desired] of [
|
|
152
|
+
[target.appService, desiredApp],
|
|
153
|
+
[target.workerService, desiredWorker],
|
|
154
|
+
]) {
|
|
155
|
+
if (desired <= 0) continue;
|
|
156
|
+
const service = await fetchActiveService(ecsClient, target.cluster, serviceName);
|
|
157
|
+
if (!service) continue;
|
|
158
|
+
await ecsClient.send(new UpdateServiceCommand({
|
|
159
|
+
cluster: target.cluster,
|
|
160
|
+
service: serviceName,
|
|
161
|
+
desiredCount: desired,
|
|
162
|
+
}));
|
|
163
|
+
restored.push({ name: serviceName, desired });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (restored.length === 0 && !dbTarget) {
|
|
167
|
+
s.stop(color.yellow('Nothing to wake.'));
|
|
168
|
+
return failCommand({
|
|
169
|
+
print: () => {
|
|
170
|
+
if (isLambda) {
|
|
171
|
+
console.log(`\n No databases found for ${color.cyan(target.appPrefix)} — and Lambda compute needs no wake-up.`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`\n No ECS services or databases found for ${color.cyan(target.appPrefix)}.`);
|
|
174
|
+
}
|
|
175
|
+
console.log(` Run ${color.green('npx grada-run apply')} to provision your infrastructure.\n`);
|
|
176
|
+
},
|
|
177
|
+
event: 'wake_run',
|
|
178
|
+
telemetry: { projectName, env_kind: target.envKind, error_code: 'NOTHING_TO_WAKE' },
|
|
179
|
+
reason: 'nothing-to-wake',
|
|
180
|
+
resultExtra: { cluster: target.cluster, region },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Resume the scheduled work sleep paused. A resume failure warns
|
|
185
|
+
// loudly — a schedule left disabled would silently stop cron — but
|
|
186
|
+
// the environment itself is already awake, so wake still succeeds.
|
|
187
|
+
const cronConfigured = hasCronAddon(cwd);
|
|
188
|
+
let cronName = null;
|
|
189
|
+
let cronResumed = false;
|
|
190
|
+
let cronNote = null;
|
|
191
|
+
if (cronConfigured) {
|
|
192
|
+
cronName = readCronScheduleName(cwd, target.appPrefix);
|
|
193
|
+
if (!cronName) {
|
|
194
|
+
cronNote = 'schedule name unreadable, skipped';
|
|
195
|
+
} else {
|
|
196
|
+
const resumed = setSchedulerState({ name: cronName, enabled: true, region, run: runSync });
|
|
197
|
+
if (resumed.ok) {
|
|
198
|
+
cronResumed = true;
|
|
199
|
+
if (resumed.skipped) cronNote = 'already running';
|
|
200
|
+
} else if (resumed.reason === 'not-deployed') {
|
|
201
|
+
cronNote = 'not deployed yet, skipped';
|
|
202
|
+
} else if (resumed.reason === 'cli-missing') {
|
|
203
|
+
cronNote = 'skipped (AWS CLI not found)';
|
|
204
|
+
} else {
|
|
205
|
+
cronNote = `could not resume (${resumed.detail || resumed.reason})`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const scalingConfigured = hasSqsWorkerScaling(cwd);
|
|
210
|
+
let scalingResumed = false;
|
|
211
|
+
let scalingNote = null;
|
|
212
|
+
if (scalingConfigured) {
|
|
213
|
+
const resumed = setWorkerScalingSuspended({
|
|
214
|
+
cluster: target.cluster, service: target.workerService, suspended: false, region, run: runSync,
|
|
215
|
+
});
|
|
216
|
+
if (resumed.ok) {
|
|
217
|
+
scalingResumed = true;
|
|
218
|
+
if (resumed.skipped) scalingNote = 'already active';
|
|
219
|
+
} else if (resumed.reason === 'not-registered') {
|
|
220
|
+
scalingNote = 'not registered, skipped';
|
|
221
|
+
} else if (resumed.reason === 'cli-missing') {
|
|
222
|
+
scalingNote = 'skipped (AWS CLI not found)';
|
|
223
|
+
} else {
|
|
224
|
+
scalingNote = `could not resume (${resumed.detail || resumed.reason})`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!noWait && restored.length > 0) {
|
|
229
|
+
s.message('Waiting for ECS tasks to start...');
|
|
230
|
+
const outcome = await pollUntil({
|
|
231
|
+
intervalMs: pollIntervalMs,
|
|
232
|
+
timeoutMs,
|
|
233
|
+
...(options.sleepFn ? { sleepFn: options.sleepFn } : {}),
|
|
234
|
+
...(options.nowFn ? { nowFn: options.nowFn } : {}),
|
|
235
|
+
onTick: async ({ elapsedMs }) => {
|
|
236
|
+
const resp = await ecsClient.send(new DescribeServicesCommand({
|
|
237
|
+
cluster: target.cluster,
|
|
238
|
+
services: restored.map((r) => r.name),
|
|
239
|
+
}));
|
|
240
|
+
const services = resp.services || [];
|
|
241
|
+
const ready = restored.every(({ name, desired }) => {
|
|
242
|
+
const svc = services.find((candidate) => candidate.serviceName === name || candidate.serviceArn?.endsWith(`/${name}`));
|
|
243
|
+
return svc && (svc.runningCount ?? 0) >= desired;
|
|
244
|
+
});
|
|
245
|
+
if (ready) return { done: true, value: true };
|
|
246
|
+
s.message(`Waiting for ECS tasks... [${Math.floor(elapsedMs / 1000)}s]`);
|
|
247
|
+
return { done: false };
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
if (outcome.timedOut) {
|
|
251
|
+
s.stop(color.yellow('ECS tasks still starting.'));
|
|
252
|
+
return failCommand({
|
|
253
|
+
print: () => {
|
|
254
|
+
console.log(color.yellow('\n⚠ ECS tasks did not reach the desired count in time.'));
|
|
255
|
+
console.log(` The restore was applied — check progress with ${color.green('npx grada-run status')}.\n`);
|
|
256
|
+
},
|
|
257
|
+
event: 'wake_run',
|
|
258
|
+
telemetry: { projectName, env_kind: target.envKind },
|
|
259
|
+
errorCode: 'ECS_WAKE_TIMEOUT',
|
|
260
|
+
reason: 'ecs-wake-timeout',
|
|
261
|
+
resultExtra: { cluster: target.cluster, region },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
removeSleepStateEntry(cwd, target.envKey);
|
|
267
|
+
|
|
268
|
+
s.stop(color.green('Environment awake. ☀️'));
|
|
269
|
+
console.log('');
|
|
270
|
+
if (isLambda) {
|
|
271
|
+
console.log(` ${color.dim('lambda:')} scale-to-zero compute — nothing to restore`);
|
|
272
|
+
}
|
|
273
|
+
if (skipDb) {
|
|
274
|
+
console.log(` ${color.dim('rds:')} skipped (--skip-db)`);
|
|
275
|
+
} else if (dbStarted) {
|
|
276
|
+
console.log(` ${color.dim('rds:')} ${color.cyan(dbTarget.id)} started`);
|
|
277
|
+
} else if (dbTarget) {
|
|
278
|
+
console.log(` ${color.dim('rds:')} ${color.cyan(dbTarget.id)} already ${dbTarget.status}`);
|
|
279
|
+
} else {
|
|
280
|
+
console.log(` ${color.dim('rds:')} no database provisioned`);
|
|
281
|
+
}
|
|
282
|
+
for (const { name, desired } of restored) {
|
|
283
|
+
console.log(` ${color.dim('ecs:')} ${color.cyan(name)} restored to ${desired}`);
|
|
284
|
+
}
|
|
285
|
+
if (cronConfigured) {
|
|
286
|
+
const label = cronName ? `${color.cyan(cronName)} ` : '';
|
|
287
|
+
console.log(` ${color.dim('cron:')} ${label}${cronResumed && !cronNote ? 'resumed' : cronNote}`);
|
|
288
|
+
}
|
|
289
|
+
if (scalingConfigured) {
|
|
290
|
+
console.log(` ${color.dim('scaling:')} ${scalingResumed && !scalingNote ? 'worker auto-scaling resumed' : scalingNote}`);
|
|
291
|
+
}
|
|
292
|
+
console.log('');
|
|
293
|
+
|
|
294
|
+
await trackSuccess('wake_run', {
|
|
295
|
+
projectName,
|
|
296
|
+
env_kind: target.envKind,
|
|
297
|
+
ecs_restored: restored.length,
|
|
298
|
+
db_started: dbStarted,
|
|
299
|
+
waited: !noWait,
|
|
300
|
+
cron_resumed: cronResumed,
|
|
301
|
+
scaling_resumed: scalingResumed,
|
|
302
|
+
});
|
|
303
|
+
outro(color.green('Done.'));
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
env: target.envKey,
|
|
307
|
+
cluster: target.cluster,
|
|
308
|
+
region,
|
|
309
|
+
ecsRestored: restored.length,
|
|
310
|
+
dbStarted,
|
|
311
|
+
waited: !noWait,
|
|
312
|
+
cronResumed,
|
|
313
|
+
scalingResumed,
|
|
314
|
+
};
|
|
315
|
+
} catch (error) {
|
|
316
|
+
await trackFailure('wake_run', {
|
|
317
|
+
projectName,
|
|
318
|
+
env_kind: target.envKind,
|
|
319
|
+
error_code: error?.name || 'UNKNOWN',
|
|
320
|
+
error_message: error?.message,
|
|
321
|
+
});
|
|
322
|
+
if (handleAuthErrorBranch(error, s, options)) {
|
|
323
|
+
return { ok: false, reason: 'error', cluster: target.cluster, region };
|
|
324
|
+
}
|
|
325
|
+
try { s.stop(color.red('❌ Wake failed.')); } catch { /* spinner already stopped */ }
|
|
326
|
+
return failCommand({
|
|
327
|
+
message: `✖ ${error?.message || error}`,
|
|
328
|
+
hint: 'Check your AWS credentials and region, then try again.',
|
|
329
|
+
reason: 'error',
|
|
330
|
+
resultExtra: { cluster: target.cluster, region },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const wakeCommand = runWake;
|
|
336
|
+
|
|
337
|
+
export default runWake;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { normalizeArgv } from '../utils/args.js';
|
|
2
|
+
|
|
3
|
+
export function parseCliArgs(processArgs) {
|
|
4
|
+
const argv = normalizeArgv(processArgs);
|
|
5
|
+
// 1. Extract telemetry flag safely
|
|
6
|
+
const hasNoTelemetry = argv.some(arg => arg === '--no-telemetry' || (typeof arg === 'string' && arg.startsWith('--no-telemetry=')));
|
|
7
|
+
|
|
8
|
+
// 2. Filter out telemetry flag
|
|
9
|
+
const args = argv.filter(arg => arg !== '--no-telemetry' && !(typeof arg === 'string' && arg.startsWith('--no-telemetry=')));
|
|
10
|
+
|
|
11
|
+
// 3. Flag readers. Every value flag supports both `--flag=value` and
|
|
12
|
+
// `--flag value` spellings; space-form values are recorded in
|
|
13
|
+
// `consumed` so section 5 can keep them out of the positional
|
|
14
|
+
// command string. Boolean flags never consume the next token.
|
|
15
|
+
const consumed = new Set();
|
|
16
|
+
|
|
17
|
+
const getValueFlag = (flagName) => {
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (typeof arg !== 'string') continue;
|
|
21
|
+
if (arg.startsWith(`--${flagName}=`)) {
|
|
22
|
+
const value = arg.slice(flagName.length + 3);
|
|
23
|
+
return value === '' ? null : value;
|
|
24
|
+
}
|
|
25
|
+
if (arg === `--${flagName}`) {
|
|
26
|
+
const next = args[i + 1];
|
|
27
|
+
if (typeof next === 'string' && next !== '' && !next.startsWith('--')) {
|
|
28
|
+
consumed.add(i + 1);
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getBoolFlag = (flagName) => {
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (typeof arg !== 'string') continue;
|
|
41
|
+
if (arg.startsWith(`--${flagName}=`)) {
|
|
42
|
+
return arg.slice(flagName.length + 3) === 'true';
|
|
43
|
+
}
|
|
44
|
+
if (arg === `--${flagName}`) return true;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 4. Parse execution flags
|
|
50
|
+
const isHeadless = args.includes('--headless');
|
|
51
|
+
const isDryRun = args.includes('--dry-run');
|
|
52
|
+
const isPreconfigured = args.includes('--preconfigured');
|
|
53
|
+
|
|
54
|
+
// Headless values are always parsed (so consumption marking is
|
|
55
|
+
// mode-independent) but only exposed in headless mode.
|
|
56
|
+
const headlessValues = {
|
|
57
|
+
dir: getValueFlag('dir'),
|
|
58
|
+
framework: getValueFlag('framework'),
|
|
59
|
+
region: getValueFlag('region'),
|
|
60
|
+
port: getValueFlag('port'),
|
|
61
|
+
size: getValueFlag('size'),
|
|
62
|
+
healthCheckPath: getValueFlag('healthCheckPath'),
|
|
63
|
+
desiredCount: getValueFlag('desiredCount'),
|
|
64
|
+
branch: getValueFlag('branch'),
|
|
65
|
+
needsDatabase: getBoolFlag('needsDatabase'),
|
|
66
|
+
enablePrPreviews: getBoolFlag('enablePrPreviews')
|
|
67
|
+
};
|
|
68
|
+
const headlessOptions = isHeadless ? headlessValues : {};
|
|
69
|
+
|
|
70
|
+
const parseWithFlag = () => {
|
|
71
|
+
const values = [];
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const arg = args[i];
|
|
74
|
+
if (typeof arg !== 'string') continue;
|
|
75
|
+
if (arg === '--with') {
|
|
76
|
+
const next = args[i + 1];
|
|
77
|
+
if (typeof next === 'string' && next !== '' && !next.startsWith('--')) {
|
|
78
|
+
consumed.add(i + 1);
|
|
79
|
+
values.push(...next.split(','));
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg.startsWith('--with=')) {
|
|
84
|
+
values.push(...arg.slice('--with='.length).split(','));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
const deduped = [];
|
|
89
|
+
for (const raw of values) {
|
|
90
|
+
const value = String(raw).trim();
|
|
91
|
+
if (!value || seen.has(value)) continue;
|
|
92
|
+
seen.add(value);
|
|
93
|
+
deduped.push(value);
|
|
94
|
+
}
|
|
95
|
+
return deduped;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const initOptions = {
|
|
99
|
+
with: parseWithFlag(),
|
|
100
|
+
model: getValueFlag('model'),
|
|
101
|
+
domain: getValueFlag('domain'),
|
|
102
|
+
zoneId: getValueFlag('zone-id'),
|
|
103
|
+
fromEmail: getValueFlag('from-email'),
|
|
104
|
+
dbEngine: getValueFlag('db-engine'),
|
|
105
|
+
target: getValueFlag('target'),
|
|
106
|
+
setupCiMigrate: args.includes('--setup-ci-migrate'),
|
|
107
|
+
setupCiDrift: args.includes('--setup-ci-drift'),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// 5. Isolate positional commands (non-string elements are positionals;
|
|
111
|
+
// values consumed by space-form flags are excluded).
|
|
112
|
+
const isFlag = (arg) => typeof arg === 'string' && arg.startsWith('--');
|
|
113
|
+
const positionalArgs = args.filter((arg, idx) => !isFlag(arg) && !consumed.has(idx));
|
|
114
|
+
const baseCommand = positionalArgs.length > 0 ? positionalArgs.slice(0, 2).join(' ') : 'init';
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
hasNoTelemetry,
|
|
118
|
+
positionalArgs,
|
|
119
|
+
baseCommand,
|
|
120
|
+
isHeadless,
|
|
121
|
+
isDryRun,
|
|
122
|
+
isPreconfigured,
|
|
123
|
+
autoApprove: getBoolFlag('auto-approve') === true,
|
|
124
|
+
yes: getBoolFlag('yes') === true,
|
|
125
|
+
headlessOptions,
|
|
126
|
+
initOptions
|
|
127
|
+
};
|
|
128
|
+
}
|