thevoidforge 21.0.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/dist/scripts/vault-read.d.ts +11 -0
- package/dist/scripts/vault-read.js +89 -0
- package/dist/scripts/voidforge.d.ts +20 -0
- package/dist/scripts/voidforge.js +404 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/wizard/api/auth.d.ts +5 -0
- package/dist/wizard/api/auth.js +133 -0
- package/dist/wizard/api/blueprint.d.ts +45 -0
- package/dist/wizard/api/blueprint.js +184 -0
- package/dist/wizard/api/cloud-providers.d.ts +16 -0
- package/dist/wizard/api/cloud-providers.js +363 -0
- package/dist/wizard/api/credentials.d.ts +1 -0
- package/dist/wizard/api/credentials.js +258 -0
- package/dist/wizard/api/danger-room.d.ts +18 -0
- package/dist/wizard/api/danger-room.js +401 -0
- package/dist/wizard/api/deploy.d.ts +4 -0
- package/dist/wizard/api/deploy.js +164 -0
- package/dist/wizard/api/prd.d.ts +1 -0
- package/dist/wizard/api/prd.js +363 -0
- package/dist/wizard/api/project.d.ts +1 -0
- package/dist/wizard/api/project.js +239 -0
- package/dist/wizard/api/projects.d.ts +6 -0
- package/dist/wizard/api/projects.js +648 -0
- package/dist/wizard/api/provision.d.ts +4 -0
- package/dist/wizard/api/provision.js +535 -0
- package/dist/wizard/api/terminal.d.ts +25 -0
- package/dist/wizard/api/terminal.js +241 -0
- package/dist/wizard/api/users.d.ts +6 -0
- package/dist/wizard/api/users.js +244 -0
- package/dist/wizard/api/war-room.d.ts +14 -0
- package/dist/wizard/api/war-room.js +45 -0
- package/dist/wizard/lib/ad-platform-core.d.ts +6 -0
- package/dist/wizard/lib/ad-platform-core.js +1 -0
- package/dist/wizard/lib/adapters/index.d.ts +52 -0
- package/dist/wizard/lib/adapters/index.js +38 -0
- package/dist/wizard/lib/adapters/sandbox-bank.d.ts +17 -0
- package/dist/wizard/lib/adapters/sandbox-bank.js +77 -0
- package/dist/wizard/lib/adapters/sandbox.d.ts +39 -0
- package/dist/wizard/lib/adapters/sandbox.js +174 -0
- package/dist/wizard/lib/adapters/stripe.d.ts +19 -0
- package/dist/wizard/lib/adapters/stripe.js +143 -0
- package/dist/wizard/lib/adapters/types.d.ts +9 -0
- package/dist/wizard/lib/adapters/types.js +10 -0
- package/dist/wizard/lib/agent-memory.d.ts +36 -0
- package/dist/wizard/lib/agent-memory.js +114 -0
- package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
- package/dist/wizard/lib/anomaly-detection.js +122 -0
- package/dist/wizard/lib/anthropic.d.ts +21 -0
- package/dist/wizard/lib/anthropic.js +105 -0
- package/dist/wizard/lib/asset-scanner.d.ts +23 -0
- package/dist/wizard/lib/asset-scanner.js +107 -0
- package/dist/wizard/lib/audit-log.d.ts +23 -0
- package/dist/wizard/lib/audit-log.js +70 -0
- package/dist/wizard/lib/autonomy-controller.d.ts +76 -0
- package/dist/wizard/lib/autonomy-controller.js +183 -0
- package/dist/wizard/lib/body-parser.d.ts +2 -0
- package/dist/wizard/lib/body-parser.js +36 -0
- package/dist/wizard/lib/build-analytics.d.ts +39 -0
- package/dist/wizard/lib/build-analytics.js +91 -0
- package/dist/wizard/lib/build-step.d.ts +21 -0
- package/dist/wizard/lib/build-step.js +104 -0
- package/dist/wizard/lib/campaign-proposer.d.ts +39 -0
- package/dist/wizard/lib/campaign-proposer.js +180 -0
- package/dist/wizard/lib/campaign-state-machine.d.ts +63 -0
- package/dist/wizard/lib/campaign-state-machine.js +114 -0
- package/dist/wizard/lib/ci-generator.d.ts +14 -0
- package/dist/wizard/lib/ci-generator.js +187 -0
- package/dist/wizard/lib/claude-merge.d.ts +38 -0
- package/dist/wizard/lib/claude-merge.js +115 -0
- package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/erd-gen.js +98 -0
- package/dist/wizard/lib/codegen/integrations.d.ts +18 -0
- package/dist/wizard/lib/codegen/integrations.js +189 -0
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
- package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
- package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
- package/dist/wizard/lib/codegen/prisma-types.js +44 -0
- package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/seed-gen.js +128 -0
- package/dist/wizard/lib/compliance.d.ts +51 -0
- package/dist/wizard/lib/compliance.js +112 -0
- package/dist/wizard/lib/correlation-engine.d.ts +59 -0
- package/dist/wizard/lib/correlation-engine.js +151 -0
- package/dist/wizard/lib/cost-estimator.d.ts +22 -0
- package/dist/wizard/lib/cost-estimator.js +72 -0
- package/dist/wizard/lib/cost-tracker.d.ts +27 -0
- package/dist/wizard/lib/cost-tracker.js +37 -0
- package/dist/wizard/lib/daemon-aggregator.d.ts +71 -0
- package/dist/wizard/lib/daemon-aggregator.js +204 -0
- package/dist/wizard/lib/daemon-core.d.ts +6 -0
- package/dist/wizard/lib/daemon-core.js +5 -0
- package/dist/wizard/lib/dashboard-data.d.ts +132 -0
- package/dist/wizard/lib/dashboard-data.js +336 -0
- package/dist/wizard/lib/dashboard-ws.d.ts +25 -0
- package/dist/wizard/lib/dashboard-ws.js +91 -0
- package/dist/wizard/lib/deep-current.d.ts +77 -0
- package/dist/wizard/lib/deep-current.js +234 -0
- package/dist/wizard/lib/deploy-coordinator.d.ts +40 -0
- package/dist/wizard/lib/deploy-coordinator.js +86 -0
- package/dist/wizard/lib/deploy-log.d.ts +28 -0
- package/dist/wizard/lib/deploy-log.js +52 -0
- package/dist/wizard/lib/desktop-notify.d.ts +27 -0
- package/dist/wizard/lib/desktop-notify.js +98 -0
- package/dist/wizard/lib/dns/cloudflare-dns.d.ts +35 -0
- package/dist/wizard/lib/dns/cloudflare-dns.js +216 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.d.ts +31 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.js +148 -0
- package/dist/wizard/lib/dns/types.d.ts +22 -0
- package/dist/wizard/lib/dns/types.js +4 -0
- package/dist/wizard/lib/document-discovery.d.ts +33 -0
- package/dist/wizard/lib/document-discovery.js +145 -0
- package/dist/wizard/lib/env-validator.d.ts +14 -0
- package/dist/wizard/lib/env-validator.js +205 -0
- package/dist/wizard/lib/env-writer.d.ts +13 -0
- package/dist/wizard/lib/env-writer.js +26 -0
- package/dist/wizard/lib/exec.d.ts +30 -0
- package/dist/wizard/lib/exec.js +52 -0
- package/dist/wizard/lib/experiment.d.ts +70 -0
- package/dist/wizard/lib/experiment.js +169 -0
- package/dist/wizard/lib/extensions.d.ts +20 -0
- package/dist/wizard/lib/extensions.js +183 -0
- package/dist/wizard/lib/financial/adapter-factory.d.ts +47 -0
- package/dist/wizard/lib/financial/adapter-factory.js +225 -0
- package/dist/wizard/lib/financial/billing/base.d.ts +6 -0
- package/dist/wizard/lib/financial/billing/base.js +1 -0
- package/dist/wizard/lib/financial/billing/google-billing.d.ts +56 -0
- package/dist/wizard/lib/financial/billing/google-billing.js +298 -0
- package/dist/wizard/lib/financial/billing/meta-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/meta-billing.js +243 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.js +260 -0
- package/dist/wizard/lib/financial/campaign/base.d.ts +13 -0
- package/dist/wizard/lib/financial/campaign/base.js +1 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.d.ts +42 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.js +388 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.d.ts +41 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.js +311 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.d.ts +45 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.js +261 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.d.ts +40 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.js +350 -0
- package/dist/wizard/lib/financial/funding-auto.d.ts +44 -0
- package/dist/wizard/lib/financial/funding-auto.js +52 -0
- package/dist/wizard/lib/financial/funding-policy.d.ts +60 -0
- package/dist/wizard/lib/financial/funding-policy.js +179 -0
- package/dist/wizard/lib/financial/platform-planner.d.ts +47 -0
- package/dist/wizard/lib/financial/platform-planner.js +134 -0
- package/dist/wizard/lib/financial/reconciliation-engine.d.ts +78 -0
- package/dist/wizard/lib/financial/reconciliation-engine.js +193 -0
- package/dist/wizard/lib/financial/registry.d.ts +22 -0
- package/dist/wizard/lib/financial/registry.js +26 -0
- package/dist/wizard/lib/financial/reporting.d.ts +96 -0
- package/dist/wizard/lib/financial/reporting.js +198 -0
- package/dist/wizard/lib/financial/stablecoin/base.d.ts +6 -0
- package/dist/wizard/lib/financial/stablecoin/base.js +1 -0
- package/dist/wizard/lib/financial/stablecoin/circle.d.ts +54 -0
- package/dist/wizard/lib/financial/stablecoin/circle.js +367 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.d.ts +24 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.js +171 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.d.ts +47 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.js +202 -0
- package/dist/wizard/lib/financial/treasury-planner.d.ts +52 -0
- package/dist/wizard/lib/financial/treasury-planner.js +128 -0
- package/dist/wizard/lib/financial-core.d.ts +6 -0
- package/dist/wizard/lib/financial-core.js +5 -0
- package/dist/wizard/lib/financial-vault.d.ts +34 -0
- package/dist/wizard/lib/financial-vault.js +199 -0
- package/dist/wizard/lib/frontmatter.d.ts +30 -0
- package/dist/wizard/lib/frontmatter.js +96 -0
- package/dist/wizard/lib/gap-analysis.d.ts +37 -0
- package/dist/wizard/lib/gap-analysis.js +218 -0
- package/dist/wizard/lib/github.d.ts +22 -0
- package/dist/wizard/lib/github.js +261 -0
- package/dist/wizard/lib/headless-deploy.d.ts +14 -0
- package/dist/wizard/lib/headless-deploy.js +452 -0
- package/dist/wizard/lib/health-monitor.d.ts +15 -0
- package/dist/wizard/lib/health-monitor.js +91 -0
- package/dist/wizard/lib/health-poller.d.ts +9 -0
- package/dist/wizard/lib/health-poller.js +123 -0
- package/dist/wizard/lib/heartbeat.d.ts +15 -0
- package/dist/wizard/lib/heartbeat.js +827 -0
- package/dist/wizard/lib/http-helpers.d.ts +9 -0
- package/dist/wizard/lib/http-helpers.js +24 -0
- package/dist/wizard/lib/image-gen.d.ts +56 -0
- package/dist/wizard/lib/image-gen.js +159 -0
- package/dist/wizard/lib/instance-sizing.d.ts +26 -0
- package/dist/wizard/lib/instance-sizing.js +51 -0
- package/dist/wizard/lib/kongo/analytics.d.ts +29 -0
- package/dist/wizard/lib/kongo/analytics.js +179 -0
- package/dist/wizard/lib/kongo/campaigns.d.ts +52 -0
- package/dist/wizard/lib/kongo/campaigns.js +91 -0
- package/dist/wizard/lib/kongo/client.d.ts +58 -0
- package/dist/wizard/lib/kongo/client.js +221 -0
- package/dist/wizard/lib/kongo/jobs.d.ts +57 -0
- package/dist/wizard/lib/kongo/jobs.js +122 -0
- package/dist/wizard/lib/kongo/pages.d.ts +60 -0
- package/dist/wizard/lib/kongo/pages.js +150 -0
- package/dist/wizard/lib/kongo/provisioner.d.ts +64 -0
- package/dist/wizard/lib/kongo/provisioner.js +116 -0
- package/dist/wizard/lib/kongo/seed.d.ts +49 -0
- package/dist/wizard/lib/kongo/seed.js +237 -0
- package/dist/wizard/lib/kongo/types.d.ts +323 -0
- package/dist/wizard/lib/kongo/types.js +11 -0
- package/dist/wizard/lib/kongo/variants.d.ts +57 -0
- package/dist/wizard/lib/kongo/variants.js +88 -0
- package/dist/wizard/lib/kongo/webhooks.d.ts +41 -0
- package/dist/wizard/lib/kongo/webhooks.js +112 -0
- package/dist/wizard/lib/marker.d.ts +28 -0
- package/dist/wizard/lib/marker.js +79 -0
- package/dist/wizard/lib/migrator.d.ts +35 -0
- package/dist/wizard/lib/migrator.js +190 -0
- package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
- package/dist/wizard/lib/natural-language-deploy.js +186 -0
- package/dist/wizard/lib/network.d.ts +22 -0
- package/dist/wizard/lib/network.js +72 -0
- package/dist/wizard/lib/oauth-core.d.ts +6 -0
- package/dist/wizard/lib/oauth-core.js +5 -0
- package/dist/wizard/lib/open-browser.d.ts +1 -0
- package/dist/wizard/lib/open-browser.js +26 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.d.ts +209 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.js +269 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.d.ts +200 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.js +212 -0
- package/dist/wizard/lib/patterns/daemon-process.d.ts +88 -0
- package/dist/wizard/lib/patterns/daemon-process.js +271 -0
- package/dist/wizard/lib/patterns/financial-transaction.d.ts +161 -0
- package/dist/wizard/lib/patterns/financial-transaction.js +132 -0
- package/dist/wizard/lib/patterns/funding-plan.d.ts +136 -0
- package/dist/wizard/lib/patterns/funding-plan.js +200 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +94 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +139 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.d.ts +67 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.js +216 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.d.ts +96 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.js +182 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.d.ts +218 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.js +264 -0
- package/dist/wizard/lib/prd-validator.d.ts +39 -0
- package/dist/wizard/lib/prd-validator.js +137 -0
- package/dist/wizard/lib/project-init.d.ts +24 -0
- package/dist/wizard/lib/project-init.js +193 -0
- package/dist/wizard/lib/project-registry.d.ts +86 -0
- package/dist/wizard/lib/project-registry.js +359 -0
- package/dist/wizard/lib/provision-manifest.d.ts +44 -0
- package/dist/wizard/lib/provision-manifest.js +164 -0
- package/dist/wizard/lib/provisioner-registry.d.ts +15 -0
- package/dist/wizard/lib/provisioner-registry.js +34 -0
- package/dist/wizard/lib/provisioners/aws-vps.d.ts +6 -0
- package/dist/wizard/lib/provisioners/aws-vps.js +643 -0
- package/dist/wizard/lib/provisioners/cloudflare.d.ts +6 -0
- package/dist/wizard/lib/provisioners/cloudflare.js +300 -0
- package/dist/wizard/lib/provisioners/docker.d.ts +6 -0
- package/dist/wizard/lib/provisioners/docker.js +75 -0
- package/dist/wizard/lib/provisioners/http-client.d.ts +20 -0
- package/dist/wizard/lib/provisioners/http-client.js +79 -0
- package/dist/wizard/lib/provisioners/railway.d.ts +6 -0
- package/dist/wizard/lib/provisioners/railway.js +413 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.js +54 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.js +112 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.d.ts +11 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.js +91 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.d.ts +5 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.js +185 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.js +36 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.d.ts +14 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.js +202 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.js +67 -0
- package/dist/wizard/lib/provisioners/self-deploy.d.ts +41 -0
- package/dist/wizard/lib/provisioners/self-deploy.js +185 -0
- package/dist/wizard/lib/provisioners/static-s3.d.ts +6 -0
- package/dist/wizard/lib/provisioners/static-s3.js +235 -0
- package/dist/wizard/lib/provisioners/types.d.ts +40 -0
- package/dist/wizard/lib/provisioners/types.js +4 -0
- package/dist/wizard/lib/provisioners/vercel.d.ts +6 -0
- package/dist/wizard/lib/provisioners/vercel.js +287 -0
- package/dist/wizard/lib/pty-manager.d.ts +42 -0
- package/dist/wizard/lib/pty-manager.js +231 -0
- package/dist/wizard/lib/rate-limiter-core.d.ts +5 -0
- package/dist/wizard/lib/rate-limiter-core.js +5 -0
- package/dist/wizard/lib/reconciliation.d.ts +43 -0
- package/dist/wizard/lib/reconciliation.js +173 -0
- package/dist/wizard/lib/revenue-types.d.ts +5 -0
- package/dist/wizard/lib/revenue-types.js +1 -0
- package/dist/wizard/lib/route-optimizer.d.ts +28 -0
- package/dist/wizard/lib/route-optimizer.js +93 -0
- package/dist/wizard/lib/s3-deploy.d.ts +19 -0
- package/dist/wizard/lib/s3-deploy.js +156 -0
- package/dist/wizard/lib/safety-tiers.d.ts +76 -0
- package/dist/wizard/lib/safety-tiers.js +134 -0
- package/dist/wizard/lib/sentry-generator.d.ts +15 -0
- package/dist/wizard/lib/sentry-generator.js +116 -0
- package/dist/wizard/lib/server-config.d.ts +13 -0
- package/dist/wizard/lib/server-config.js +23 -0
- package/dist/wizard/lib/service-install.d.ts +18 -0
- package/dist/wizard/lib/service-install.js +182 -0
- package/dist/wizard/lib/site-scanner.d.ts +80 -0
- package/dist/wizard/lib/site-scanner.js +262 -0
- package/dist/wizard/lib/ssh-deploy.d.ts +25 -0
- package/dist/wizard/lib/ssh-deploy.js +225 -0
- package/dist/wizard/lib/templates.d.ts +24 -0
- package/dist/wizard/lib/templates.js +219 -0
- package/dist/wizard/lib/totp.d.ts +35 -0
- package/dist/wizard/lib/totp.js +276 -0
- package/dist/wizard/lib/tower-auth.d.ts +43 -0
- package/dist/wizard/lib/tower-auth.js +352 -0
- package/dist/wizard/lib/tower-rate-limit.d.ts +14 -0
- package/dist/wizard/lib/tower-rate-limit.js +61 -0
- package/dist/wizard/lib/tower-session.d.ts +28 -0
- package/dist/wizard/lib/tower-session.js +119 -0
- package/dist/wizard/lib/treasury-backup.d.ts +23 -0
- package/dist/wizard/lib/treasury-backup.js +126 -0
- package/dist/wizard/lib/treasury-heartbeat.d.ts +82 -0
- package/dist/wizard/lib/treasury-heartbeat.js +1104 -0
- package/dist/wizard/lib/updater.d.ts +29 -0
- package/dist/wizard/lib/updater.js +190 -0
- package/dist/wizard/lib/user-manager.d.ts +39 -0
- package/dist/wizard/lib/user-manager.js +182 -0
- package/dist/wizard/lib/vault.d.ts +26 -0
- package/dist/wizard/lib/vault.js +161 -0
- package/dist/wizard/router.d.ts +5 -0
- package/dist/wizard/router.js +15 -0
- package/dist/wizard/server.d.ts +18 -0
- package/dist/wizard/server.js +436 -0
- package/package.json +59 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Railway provisioner — creates a real Railway project via GraphQL API + generates railway.toml.
|
|
3
|
+
* v3.8.0: Creates service with GitHub source, sets env vars, polls deploy (ADR-015).
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { httpsPost, safeJsonParse } from './http-client.js';
|
|
8
|
+
import { recordResourcePending, recordResourceCreated } from '../provision-manifest.js';
|
|
9
|
+
import { appendEnvSection } from '../env-writer.js';
|
|
10
|
+
const DEPLOY_POLL_INTERVAL_MS = 5000;
|
|
11
|
+
const DEPLOY_POLL_TIMEOUT_MS = 300_000;
|
|
12
|
+
function gql(token, query, variables) {
|
|
13
|
+
const body = JSON.stringify({ query, variables });
|
|
14
|
+
return httpsPost('backboard.railway.com', '/graphql/v2', {
|
|
15
|
+
'Authorization': `Bearer ${token}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
}, body);
|
|
18
|
+
}
|
|
19
|
+
export const railwayProvisioner = {
|
|
20
|
+
async validate(ctx) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (!ctx.projectDir)
|
|
23
|
+
errors.push('Project directory is required');
|
|
24
|
+
if (!ctx.credentials['railway-token'])
|
|
25
|
+
errors.push('Railway API token is required');
|
|
26
|
+
return errors;
|
|
27
|
+
},
|
|
28
|
+
async provision(ctx, emit) {
|
|
29
|
+
const files = [];
|
|
30
|
+
const resources = [];
|
|
31
|
+
const outputs = {};
|
|
32
|
+
const token = ctx.credentials['railway-token'];
|
|
33
|
+
const framework = ctx.framework || 'express';
|
|
34
|
+
// Step 1: Create Railway project
|
|
35
|
+
emit({ step: 'railway-project', status: 'started', message: 'Creating Railway project' });
|
|
36
|
+
let projectId = '';
|
|
37
|
+
try {
|
|
38
|
+
await recordResourcePending(ctx.runId, 'railway-project', ctx.projectName, 'global');
|
|
39
|
+
const res = await gql(token, `
|
|
40
|
+
mutation($name: String!) {
|
|
41
|
+
projectCreate(input: { name: $name }) {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, { name: ctx.projectName });
|
|
47
|
+
if (res.status !== 200) {
|
|
48
|
+
throw new Error(`Railway API returned ${res.status}`);
|
|
49
|
+
}
|
|
50
|
+
const data = safeJsonParse(res.body);
|
|
51
|
+
if (data.errors && data.errors.length > 0) {
|
|
52
|
+
throw new Error(data.errors[0].message);
|
|
53
|
+
}
|
|
54
|
+
projectId = data.data?.projectCreate?.id ?? '';
|
|
55
|
+
if (!projectId)
|
|
56
|
+
throw new Error('No project ID returned');
|
|
57
|
+
resources.push({ type: 'railway-project', id: projectId, region: 'global' });
|
|
58
|
+
await recordResourceCreated(ctx.runId, 'railway-project', projectId, 'global');
|
|
59
|
+
outputs['RAILWAY_PROJECT_ID'] = projectId;
|
|
60
|
+
outputs['RAILWAY_PROJECT_NAME'] = data.data?.projectCreate?.name ?? ctx.projectName;
|
|
61
|
+
emit({ step: 'railway-project', status: 'done', message: `Project "${outputs['RAILWAY_PROJECT_NAME']}" created on Railway` });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
emit({ step: 'railway-project', status: 'error', message: 'Failed to create Railway project', detail: err.message });
|
|
65
|
+
return { success: false, resources, outputs, files, error: err.message };
|
|
66
|
+
}
|
|
67
|
+
// Fetch the default environment ID once — shared by all subsequent steps
|
|
68
|
+
let environmentId = '';
|
|
69
|
+
try {
|
|
70
|
+
const envRes = await gql(token, `
|
|
71
|
+
query($projectId: String!) {
|
|
72
|
+
project(id: $projectId) {
|
|
73
|
+
environments { edges { node { id name } } }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`, { projectId });
|
|
77
|
+
if (envRes.status === 200) {
|
|
78
|
+
const envData = safeJsonParse(envRes.body);
|
|
79
|
+
const edges = envData?.data?.project?.environments?.edges ?? [];
|
|
80
|
+
const prodEnv = edges.find(e => e.node.name === 'production') || edges[0];
|
|
81
|
+
environmentId = prodEnv?.node.id ?? '';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
emit({ step: 'railway-env', status: 'error', message: 'Could not fetch project environment — database/redis services will use fallback creation', detail: 'Environment query failed' });
|
|
86
|
+
}
|
|
87
|
+
// Helper: deploy a database template or fall back to serviceCreate
|
|
88
|
+
async function deployTemplate(templateName, resourceLabel, displayName) {
|
|
89
|
+
await recordResourcePending(ctx.runId, 'railway-service', `${projectId}-${resourceLabel}`, 'global');
|
|
90
|
+
// Only attempt templateDeploy if we have a valid environment ID
|
|
91
|
+
if (environmentId) {
|
|
92
|
+
try {
|
|
93
|
+
const res = await gql(token, `
|
|
94
|
+
mutation($projectId: String!, $environmentId: String!, $template: String!) {
|
|
95
|
+
templateDeploy(input: {
|
|
96
|
+
projectId: $projectId,
|
|
97
|
+
environmentId: $environmentId,
|
|
98
|
+
services: [{ template: $template, hasDomain: false }]
|
|
99
|
+
}) {
|
|
100
|
+
projectId
|
|
101
|
+
workflowId
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`, { projectId, environmentId, template: templateName });
|
|
105
|
+
if (res.status === 200) {
|
|
106
|
+
const data = safeJsonParse(res.body);
|
|
107
|
+
if (!data?.errors || data.errors.length === 0) {
|
|
108
|
+
// Template deploy succeeded — resource is tracked at project level
|
|
109
|
+
// (individual service IDs are not returned by templateDeploy)
|
|
110
|
+
resources.push({ type: 'railway-service', id: `${projectId}-${resourceLabel}`, region: 'global' });
|
|
111
|
+
await recordResourceCreated(ctx.runId, 'railway-service', `${projectId}-${resourceLabel}`, 'global');
|
|
112
|
+
emit({ step: `railway-${resourceLabel}`, status: 'done', message: `${displayName} deployed via template — connection string available in Railway dashboard` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Fall through to serviceCreate fallback
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Fallback: create a bare service (user configures the database image in dashboard)
|
|
122
|
+
const svcRes = await gql(token, `
|
|
123
|
+
mutation($projectId: String!, $name: String!) {
|
|
124
|
+
serviceCreate(input: { projectId: $projectId, name: $name }) {
|
|
125
|
+
id
|
|
126
|
+
name
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`, { projectId, name: `${ctx.projectName}-${templateName}` });
|
|
130
|
+
if (svcRes.status === 200) {
|
|
131
|
+
const svcData = safeJsonParse(svcRes.body);
|
|
132
|
+
const svcId = svcData?.data?.serviceCreate?.id;
|
|
133
|
+
if (svcId) {
|
|
134
|
+
resources.push({ type: 'railway-service', id: svcId, region: 'global' });
|
|
135
|
+
await recordResourceCreated(ctx.runId, 'railway-service', svcId, 'global');
|
|
136
|
+
emit({ step: `railway-${resourceLabel}`, status: 'done', message: `${displayName} service created — configure database image in Railway dashboard` });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
emit({ step: `railway-${resourceLabel}`, status: 'error', message: `${displayName} service creation returned no ID`, detail: 'Create the database manually in the Railway dashboard' });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
emit({ step: `railway-${resourceLabel}`, status: 'error', message: `Failed to create ${displayName} service (API returned ${svcRes.status})`, detail: 'Create the database manually in the Railway dashboard' });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Step 2: Add database service if requested (ADR-019: template services, not plugins)
|
|
147
|
+
if (ctx.database === 'postgres' || ctx.database === 'mysql') {
|
|
148
|
+
const dbType = ctx.database === 'postgres' ? 'Postgres' : 'MySQL';
|
|
149
|
+
const templateName = ctx.database === 'postgres' ? 'postgres' : 'mysql';
|
|
150
|
+
emit({ step: 'railway-db', status: 'started', message: `Adding ${dbType} service to Railway project` });
|
|
151
|
+
try {
|
|
152
|
+
await deployTemplate(templateName, 'db', dbType);
|
|
153
|
+
outputs['RAILWAY_DB_TYPE'] = dbType;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
emit({ step: 'railway-db', status: 'error', message: `Failed to add ${dbType} service`, detail: err.message });
|
|
157
|
+
// Non-fatal
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
emit({ step: 'railway-db', status: 'skipped', message: ctx.database === 'sqlite' ? 'SQLite — no remote database service needed' : 'No database requested' });
|
|
162
|
+
}
|
|
163
|
+
// Step 3: Add Redis service if cache requested (ADR-019: template services)
|
|
164
|
+
if (ctx.cache === 'redis') {
|
|
165
|
+
emit({ step: 'railway-redis', status: 'started', message: 'Adding Redis service to Railway project' });
|
|
166
|
+
try {
|
|
167
|
+
await deployTemplate('redis', 'redis', 'Redis');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
emit({ step: 'railway-redis', status: 'error', message: 'Failed to add Redis service', detail: err.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
emit({ step: 'railway-redis', status: 'skipped', message: 'No cache requested' });
|
|
175
|
+
}
|
|
176
|
+
// Step 4: Create service with GitHub source (ADR-015)
|
|
177
|
+
// Must happen BEFORE custom domain so the domain can attach to this service
|
|
178
|
+
const ghOwner = ctx.credentials['_github-owner'];
|
|
179
|
+
const ghRepo = ctx.credentials['_github-repo-name'];
|
|
180
|
+
let serviceId = '';
|
|
181
|
+
if (projectId && ghOwner && ghRepo) {
|
|
182
|
+
emit({ step: 'railway-service', status: 'started', message: `Creating service linked to ${ghOwner}/${ghRepo}` });
|
|
183
|
+
try {
|
|
184
|
+
// Create service with GitHub repo source
|
|
185
|
+
const svcRes = await gql(token, `
|
|
186
|
+
mutation($projectId: String!, $repo: String!) {
|
|
187
|
+
serviceCreate(input: {
|
|
188
|
+
projectId: $projectId,
|
|
189
|
+
source: { repo: $repo }
|
|
190
|
+
}) {
|
|
191
|
+
id
|
|
192
|
+
name
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
`, { projectId, repo: `${ghOwner}/${ghRepo}` });
|
|
196
|
+
if (svcRes.status === 200) {
|
|
197
|
+
const svcData = safeJsonParse(svcRes.body);
|
|
198
|
+
if (svcData?.errors?.length) {
|
|
199
|
+
emit({ step: 'railway-service', status: 'error', message: 'Failed to create service', detail: svcData.errors[0].message });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
serviceId = svcData?.data?.serviceCreate?.id ?? '';
|
|
203
|
+
if (serviceId) {
|
|
204
|
+
resources.push({ type: 'railway-service', id: serviceId, region: 'global' });
|
|
205
|
+
await recordResourceCreated(ctx.runId, 'railway-service', serviceId, 'global');
|
|
206
|
+
}
|
|
207
|
+
emit({ step: 'railway-service', status: 'done', message: `Service created — linked to GitHub repo` });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
emit({ step: 'railway-service', status: 'error', message: 'Failed to create service', detail: err.message });
|
|
213
|
+
// Non-fatal — project exists, user can link manually
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Step 5: Add custom domain if hostname provided (after service creation so it can attach)
|
|
217
|
+
if (ctx.hostname && projectId && serviceId && environmentId) {
|
|
218
|
+
emit({ step: 'railway-domain', status: 'started', message: `Adding domain ${ctx.hostname} to Railway service` });
|
|
219
|
+
try {
|
|
220
|
+
const domRes = await gql(token, `
|
|
221
|
+
mutation($projectId: String!, $environmentId: String!, $serviceId: String!, $domain: String!) {
|
|
222
|
+
customDomainCreate(input: { projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId, domain: $domain }) {
|
|
223
|
+
id
|
|
224
|
+
domain
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
`, { projectId, environmentId, serviceId, domain: ctx.hostname });
|
|
228
|
+
if (domRes.status !== 200)
|
|
229
|
+
throw new Error(`Railway API returned ${domRes.status}`);
|
|
230
|
+
const domData = safeJsonParse(domRes.body);
|
|
231
|
+
if (domData?.errors && domData.errors.length > 0) {
|
|
232
|
+
throw new Error(domData.errors[0].message);
|
|
233
|
+
}
|
|
234
|
+
const domain = domData?.data?.customDomainCreate?.domain ?? ctx.hostname;
|
|
235
|
+
outputs['RAILWAY_CUSTOM_DOMAIN'] = domain;
|
|
236
|
+
emit({ step: 'railway-domain', status: 'done', message: `Domain "${domain}" added to Railway service` });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
emit({ step: 'railway-domain', status: 'error', message: 'Failed to add domain to Railway', detail: err.message });
|
|
240
|
+
// Non-fatal
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (ctx.hostname && projectId && (!serviceId || !environmentId)) {
|
|
244
|
+
emit({ step: 'railway-domain', status: 'skipped', message: 'Cannot add domain — service or environment not available. Add domain manually in Railway dashboard.' });
|
|
245
|
+
}
|
|
246
|
+
// Step 6: Set environment variables on the service
|
|
247
|
+
if (serviceId && environmentId) {
|
|
248
|
+
emit({ step: 'railway-envvars', status: 'started', message: 'Setting environment variables' });
|
|
249
|
+
try {
|
|
250
|
+
// Railway uses ${{service.VAR}} syntax for service variable references (ADR-019)
|
|
251
|
+
const variables = {};
|
|
252
|
+
if (ctx.database === 'postgres' || ctx.database === 'mysql') {
|
|
253
|
+
variables['DATABASE_URL'] = ctx.database === 'postgres'
|
|
254
|
+
? '${{Postgres.DATABASE_URL}}'
|
|
255
|
+
: '${{MySQL.DATABASE_URL}}';
|
|
256
|
+
}
|
|
257
|
+
if (ctx.cache === 'redis') {
|
|
258
|
+
variables['REDIS_URL'] = '${{Redis.REDIS_URL}}';
|
|
259
|
+
}
|
|
260
|
+
if (Object.keys(variables).length > 0) {
|
|
261
|
+
await gql(token, `
|
|
262
|
+
mutation($input: VariableCollectionUpsertInput!) {
|
|
263
|
+
variableCollectionUpsert(input: $input)
|
|
264
|
+
}
|
|
265
|
+
`, {
|
|
266
|
+
input: {
|
|
267
|
+
projectId,
|
|
268
|
+
serviceId,
|
|
269
|
+
environmentId,
|
|
270
|
+
variables,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
emit({ step: 'railway-envvars', status: 'done', message: `Set ${Object.keys(variables).length} environment variables` });
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
emit({ step: 'railway-envvars', status: 'done', message: 'No environment variables to set' });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
emit({ step: 'railway-envvars', status: 'error', message: 'Failed to set env vars', detail: err.message });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Step 7: Poll for deployment
|
|
284
|
+
if (serviceId && environmentId) {
|
|
285
|
+
emit({ step: 'railway-deploy', status: 'started', message: 'Waiting for Railway deployment...' });
|
|
286
|
+
try {
|
|
287
|
+
const start = Date.now();
|
|
288
|
+
let deployUrl = '';
|
|
289
|
+
while (Date.now() - start < DEPLOY_POLL_TIMEOUT_MS) {
|
|
290
|
+
await new Promise(r => setTimeout(r, DEPLOY_POLL_INTERVAL_MS));
|
|
291
|
+
if (ctx.abortSignal?.aborted)
|
|
292
|
+
break;
|
|
293
|
+
// Query service by ID to avoid inspecting the wrong service (e.g., a DB service)
|
|
294
|
+
const depRes = await gql(token, `
|
|
295
|
+
query($serviceId: String!) {
|
|
296
|
+
service(id: $serviceId) {
|
|
297
|
+
serviceInstances { edges { node {
|
|
298
|
+
domains { serviceDomains { domain } }
|
|
299
|
+
latestDeployment { status }
|
|
300
|
+
} } }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
`, { serviceId });
|
|
304
|
+
if (depRes.status === 200) {
|
|
305
|
+
const depData = safeJsonParse(depRes.body);
|
|
306
|
+
const instance = depData?.data?.service?.serviceInstances?.edges?.[0]?.node;
|
|
307
|
+
const deployStatus = instance?.latestDeployment?.status;
|
|
308
|
+
const domain = instance?.domains?.serviceDomains?.[0]?.domain;
|
|
309
|
+
if (deployStatus === 'SUCCESS' && domain) {
|
|
310
|
+
deployUrl = `https://${domain}`;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (deployStatus === 'FAILED' || deployStatus === 'CRASHED') {
|
|
314
|
+
emit({ step: 'railway-deploy', status: 'error', message: `Deployment ${deployStatus.toLowerCase()} — check Railway dashboard` });
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
319
|
+
if (elapsed % 15 === 0) {
|
|
320
|
+
emit({ step: 'railway-deploy', status: 'started', message: `Waiting for deployment... (${elapsed}s)` });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (deployUrl) {
|
|
324
|
+
outputs['DEPLOY_URL'] = deployUrl;
|
|
325
|
+
// Only set RAILWAY_DOMAIN if no custom domain was added (don't overwrite user's domain)
|
|
326
|
+
if (!outputs['RAILWAY_CUSTOM_DOMAIN']) {
|
|
327
|
+
outputs['RAILWAY_DOMAIN'] = deployUrl.replace('https://', '');
|
|
328
|
+
}
|
|
329
|
+
emit({ step: 'railway-deploy', status: 'done', message: `Live at ${deployUrl}` });
|
|
330
|
+
}
|
|
331
|
+
else if (!ctx.abortSignal?.aborted) {
|
|
332
|
+
emit({ step: 'railway-deploy', status: 'error', message: 'Deployment polling timed out — check Railway dashboard' });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
emit({ step: 'railway-deploy', status: 'error', message: 'Failed to poll deployment', detail: err.message });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (!ghOwner || !ghRepo) {
|
|
340
|
+
emit({ step: 'railway-deploy', status: 'skipped', message: 'No GitHub repo linked — deploy manually with: railway link && railway up' });
|
|
341
|
+
}
|
|
342
|
+
// Step 8: Generate railway.toml
|
|
343
|
+
emit({ step: 'railway-config', status: 'started', message: 'Generating railway.toml' });
|
|
344
|
+
try {
|
|
345
|
+
const startCommand = framework === 'next.js'
|
|
346
|
+
? 'npm run start'
|
|
347
|
+
: framework === 'django'
|
|
348
|
+
? 'gunicorn config.wsgi:application --bind 0.0.0.0:$PORT'
|
|
349
|
+
: 'node dist/index.js';
|
|
350
|
+
const buildCommand = framework === 'django'
|
|
351
|
+
? 'pip install -r requirements.txt && python manage.py collectstatic --noinput'
|
|
352
|
+
: 'npm ci && npm run build';
|
|
353
|
+
const config = `# railway.toml — Railway deployment configuration
|
|
354
|
+
# Generated by VoidForge
|
|
355
|
+
# Deploy with: railway link ${projectId} && railway up
|
|
356
|
+
|
|
357
|
+
[build]
|
|
358
|
+
builder = "nixpacks"
|
|
359
|
+
buildCommand = "${buildCommand}"
|
|
360
|
+
|
|
361
|
+
[deploy]
|
|
362
|
+
startCommand = "${startCommand}"
|
|
363
|
+
healthcheckPath = "/"
|
|
364
|
+
restartPolicyType = "on_failure"
|
|
365
|
+
restartPolicyMaxRetries = 3
|
|
366
|
+
`;
|
|
367
|
+
await writeFile(join(ctx.projectDir, 'railway.toml'), config, 'utf-8');
|
|
368
|
+
files.push('railway.toml');
|
|
369
|
+
emit({ step: 'railway-config', status: 'done', message: 'Generated railway.toml' });
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
emit({ step: 'railway-config', status: 'error', message: 'Failed to write railway.toml', detail: err.message });
|
|
373
|
+
}
|
|
374
|
+
// Step 9: Write .env
|
|
375
|
+
emit({ step: 'railway-env', status: 'started', message: 'Writing Railway config to .env' });
|
|
376
|
+
try {
|
|
377
|
+
const envLines = [
|
|
378
|
+
`# VoidForge Railway — generated ${new Date().toISOString()}`,
|
|
379
|
+
`RAILWAY_PROJECT_ID=${projectId}`,
|
|
380
|
+
`RAILWAY_PROJECT_NAME=${outputs['RAILWAY_PROJECT_NAME'] || ctx.projectName}`,
|
|
381
|
+
];
|
|
382
|
+
if (outputs['DEPLOY_URL'])
|
|
383
|
+
envLines.push(`DEPLOY_URL=${outputs['DEPLOY_URL']}`);
|
|
384
|
+
envLines.push(ghOwner ? '# Auto-deploys on push to main' : `# Deploy with: railway link ${projectId} && railway up`);
|
|
385
|
+
await appendEnvSection(ctx.projectDir, envLines);
|
|
386
|
+
emit({ step: 'railway-env', status: 'done', message: 'Railway config written to .env' });
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
emit({ step: 'railway-env', status: 'error', message: 'Failed to write .env', detail: err.message });
|
|
390
|
+
}
|
|
391
|
+
return { success: true, resources, outputs, files };
|
|
392
|
+
},
|
|
393
|
+
async cleanup(resources, credentials) {
|
|
394
|
+
const token = credentials['railway-token'];
|
|
395
|
+
if (!token)
|
|
396
|
+
return;
|
|
397
|
+
for (const resource of resources) {
|
|
398
|
+
if (resource.type === 'railway-project') {
|
|
399
|
+
try {
|
|
400
|
+
await gql(token, `
|
|
401
|
+
mutation($id: String!) {
|
|
402
|
+
projectDelete(id: $id)
|
|
403
|
+
}
|
|
404
|
+
`, { id: resource.id });
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
console.error(`Failed to delete Railway project ${resource.id}:`, err.message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Plugins are deleted with the project — no need to delete separately
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: Caddyfile — reverse proxy + automatic HTTPS + security headers.
|
|
3
|
+
* Written to projectDir/infra/Caddyfile
|
|
4
|
+
*/
|
|
5
|
+
interface CaddyfileOptions {
|
|
6
|
+
framework: string;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function generateCaddyfile(opts: CaddyfileOptions): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: Caddyfile — reverse proxy + automatic HTTPS + security headers.
|
|
3
|
+
* Written to projectDir/infra/Caddyfile
|
|
4
|
+
*/
|
|
5
|
+
export function generateCaddyfile(opts) {
|
|
6
|
+
const port = opts.framework === 'django' ? '8000' : '3000';
|
|
7
|
+
const siteAddress = opts.hostname || ':80';
|
|
8
|
+
const comment = opts.hostname
|
|
9
|
+
? `# Caddyfile — Reverse proxy for ${opts.hostname}
|
|
10
|
+
# Generated by VoidForge
|
|
11
|
+
#
|
|
12
|
+
# Caddy will auto-provision HTTPS via Let's Encrypt.
|
|
13
|
+
# If using Cloudflare proxy (orange cloud), set SSL mode to "Full" in Cloudflare dashboard.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# 1. Copy to server: scp Caddyfile user@host:/etc/caddy/Caddyfile
|
|
17
|
+
# 2. Reload: sudo systemctl reload caddy`
|
|
18
|
+
: `# Caddyfile — Reverse proxy with automatic HTTPS
|
|
19
|
+
# Generated by VoidForge
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# 1. Replace :80 with your domain (e.g. example.com)
|
|
23
|
+
# 2. Copy to server: scp Caddyfile user@host:/etc/caddy/Caddyfile
|
|
24
|
+
# 3. Reload: sudo systemctl reload caddy
|
|
25
|
+
#
|
|
26
|
+
# Caddy auto-provisions HTTPS certificates via Let's Encrypt
|
|
27
|
+
# when you replace :80 with a real domain name.`;
|
|
28
|
+
return `${comment}
|
|
29
|
+
|
|
30
|
+
${siteAddress} {
|
|
31
|
+
reverse_proxy localhost:${port}
|
|
32
|
+
|
|
33
|
+
# Security headers
|
|
34
|
+
header {
|
|
35
|
+
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
|
36
|
+
X-Content-Type-Options nosniff
|
|
37
|
+
X-Frame-Options DENY
|
|
38
|
+
Referrer-Policy strict-origin-when-cross-origin
|
|
39
|
+
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
|
40
|
+
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"
|
|
41
|
+
-Server
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Gzip compression
|
|
45
|
+
encode gzip
|
|
46
|
+
|
|
47
|
+
# Access logs
|
|
48
|
+
log {
|
|
49
|
+
output file /var/log/caddy/access.log
|
|
50
|
+
format json
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: deploy.sh — release-directory deploy with health check + auto-rollback.
|
|
3
|
+
* Uses timestamped release dirs with a "current" symlink for atomic deploys.
|
|
4
|
+
* Written to projectDir/infra/deploy.sh
|
|
5
|
+
*/
|
|
6
|
+
interface DeployScriptOptions {
|
|
7
|
+
framework: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function generateDeployScript(opts: DeployScriptOptions): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: deploy.sh — release-directory deploy with health check + auto-rollback.
|
|
3
|
+
* Uses timestamped release dirs with a "current" symlink for atomic deploys.
|
|
4
|
+
* Written to projectDir/infra/deploy.sh
|
|
5
|
+
*/
|
|
6
|
+
export function generateDeployScript(opts) {
|
|
7
|
+
const isNode = ['next.js', 'express'].includes(opts.framework) || !opts.framework;
|
|
8
|
+
const isDjango = opts.framework === 'django';
|
|
9
|
+
let installCommands;
|
|
10
|
+
let restartCommands;
|
|
11
|
+
let healthPath = '/';
|
|
12
|
+
if (isNode) {
|
|
13
|
+
installCommands = 'npm ci --production --ignore-scripts';
|
|
14
|
+
restartCommands = 'pm2 startOrRestart /opt/app/current/ecosystem.config.js --env production && pm2 save';
|
|
15
|
+
}
|
|
16
|
+
else if (isDjango) {
|
|
17
|
+
installCommands = 'pip3.12 install -r requirements.txt';
|
|
18
|
+
restartCommands = 'python3.12 manage.py migrate --noinput && python3.12 manage.py collectstatic --noinput && supervisorctl restart app';
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
installCommands = 'bundle install --deployment --without development test';
|
|
22
|
+
restartCommands = 'RAILS_ENV=production bundle exec rails db:migrate && touch tmp/restart.txt';
|
|
23
|
+
}
|
|
24
|
+
return `#!/usr/bin/env bash
|
|
25
|
+
# deploy.sh — Release-directory deploy with health check + auto-rollback
|
|
26
|
+
# Generated by VoidForge. Reads config from .env
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
# Load environment
|
|
30
|
+
if [ -f .env ]; then
|
|
31
|
+
set -a
|
|
32
|
+
source .env
|
|
33
|
+
set +a
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
SSH_HOST="\${SSH_HOST:?Set SSH_HOST in .env}"
|
|
37
|
+
SSH_USER="\${SSH_USER:-ec2-user}"
|
|
38
|
+
SSH_KEY="\${SSH_KEY_PATH:-.ssh/deploy-key.pem}"
|
|
39
|
+
RELEASES_DIR="/opt/app/releases"
|
|
40
|
+
CURRENT_LINK="/opt/app/current"
|
|
41
|
+
HEALTH_URL="http://localhost:3000${healthPath}"
|
|
42
|
+
HEALTH_RETRIES=5
|
|
43
|
+
HEALTH_DELAY=3
|
|
44
|
+
|
|
45
|
+
SSH_OPTS="-i $SSH_KEY -o StrictHostKeyChecking=accept-new"
|
|
46
|
+
|
|
47
|
+
RELEASE="$(date +%Y%m%d%H%M%S)"
|
|
48
|
+
RELEASE_DIR="$RELEASES_DIR/$RELEASE"
|
|
49
|
+
|
|
50
|
+
echo "=== Deploying release $RELEASE to $SSH_HOST ==="
|
|
51
|
+
|
|
52
|
+
# Create release directory
|
|
53
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "mkdir -p $RELEASE_DIR"
|
|
54
|
+
|
|
55
|
+
# Sync files to new release directory
|
|
56
|
+
rsync -avz --delete \\
|
|
57
|
+
--exclude node_modules \\
|
|
58
|
+
--exclude .git \\
|
|
59
|
+
--exclude .env \\
|
|
60
|
+
--exclude logs \\
|
|
61
|
+
--exclude .ssh \\
|
|
62
|
+
--exclude infra \\
|
|
63
|
+
--exclude coverage \\
|
|
64
|
+
-e "ssh $SSH_OPTS" \\
|
|
65
|
+
./ "$SSH_USER@$SSH_HOST:$RELEASE_DIR/"
|
|
66
|
+
|
|
67
|
+
# Install dependencies in release directory
|
|
68
|
+
echo "Installing dependencies..."
|
|
69
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "cd $RELEASE_DIR && ${installCommands}"
|
|
70
|
+
|
|
71
|
+
# Swap symlink atomically (previous release preserved for rollback)
|
|
72
|
+
PREVIOUS="$(ssh $SSH_OPTS $SSH_USER@$SSH_HOST "readlink $CURRENT_LINK 2>/dev/null || echo ''")"
|
|
73
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "ln -sfn $RELEASE_DIR $CURRENT_LINK"
|
|
74
|
+
|
|
75
|
+
# Restart application
|
|
76
|
+
echo "Restarting application..."
|
|
77
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "cd $CURRENT_LINK && ${restartCommands}"
|
|
78
|
+
|
|
79
|
+
# Health check with retries
|
|
80
|
+
echo "Running health check..."
|
|
81
|
+
HEALTHY=false
|
|
82
|
+
for i in $(seq 1 $HEALTH_RETRIES); do
|
|
83
|
+
sleep $HEALTH_DELAY
|
|
84
|
+
if ssh $SSH_OPTS $SSH_USER@$SSH_HOST "curl -sf $HEALTH_URL > /dev/null 2>&1"; then
|
|
85
|
+
HEALTHY=true
|
|
86
|
+
echo "Health check passed (attempt $i/$HEALTH_RETRIES)"
|
|
87
|
+
break
|
|
88
|
+
fi
|
|
89
|
+
echo "Health check attempt $i/$HEALTH_RETRIES failed, retrying..."
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
if [ "$HEALTHY" = false ]; then
|
|
93
|
+
echo "=== HEALTH CHECK FAILED — rolling back ==="
|
|
94
|
+
if [ -n "$PREVIOUS" ]; then
|
|
95
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "ln -sfn $PREVIOUS $CURRENT_LINK"
|
|
96
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "cd $CURRENT_LINK && ${restartCommands}"
|
|
97
|
+
echo "Rolled back to previous release: $PREVIOUS"
|
|
98
|
+
else
|
|
99
|
+
echo "No previous release to roll back to!"
|
|
100
|
+
fi
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Clean up old releases (keep last 5)
|
|
105
|
+
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "cd $RELEASES_DIR && ls -1d */ | head -n -5 | xargs -r rm -rf"
|
|
106
|
+
|
|
107
|
+
echo ""
|
|
108
|
+
echo "=== Deploy complete ==="
|
|
109
|
+
echo "Release: $RELEASE"
|
|
110
|
+
echo "App running at http://$SSH_HOST"
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docker-compose.yml template generator.
|
|
3
|
+
*/
|
|
4
|
+
interface ComposeOptions {
|
|
5
|
+
projectName: string;
|
|
6
|
+
framework: string;
|
|
7
|
+
database: string;
|
|
8
|
+
cache: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function generateDockerCompose(opts: ComposeOptions): string;
|
|
11
|
+
export {};
|