@usequark/quark-create-app 1.23.11
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/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import { sleep } from "../../utils.js";
|
|
6
|
+
|
|
7
|
+
export const RAILWAY = "railway";
|
|
8
|
+
|
|
9
|
+
export const DIAGNOSTIC_CODES = Object.freeze({
|
|
10
|
+
RAILWAY_CLI_NOT_FOUND: "railway_cli_not_found",
|
|
11
|
+
RAILWAY_NOT_LOGGED_IN: "railway_not_logged_in",
|
|
12
|
+
RAILWAY_LINK_FAILED: "railway_link_failed",
|
|
13
|
+
RAILWAY_DEPLOY_FAILED: "railway_deploy_failed",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export class RailwayError extends Error {
|
|
17
|
+
constructor(message, code, meta) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "RailwayError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.meta = meta;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function checkRailwayCLI() {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execa(RAILWAY, ["--version"], { timeout: 10_000 });
|
|
28
|
+
return stdout.trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function checkRailwayLogin() {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execa(RAILWAY, ["whoami"], { timeout: 10_000 });
|
|
37
|
+
const emailMatch = stdout.trim().match(/\(([^)]+@[^)]+)\)/);
|
|
38
|
+
return emailMatch ? emailMatch[1] : stdout.trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function isProjectLinked(cwd) {
|
|
45
|
+
const railwayDir = path.join(cwd, ".railway");
|
|
46
|
+
try {
|
|
47
|
+
const entries = await fs.readdir(railwayDir);
|
|
48
|
+
return entries.some((e) => e.endsWith(".json"));
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function tryLinkProject(projectId, { cwd } = {}) {
|
|
55
|
+
try {
|
|
56
|
+
await execa(RAILWAY, ["link", "--project", projectId], {
|
|
57
|
+
cwd,
|
|
58
|
+
timeout: 30_000,
|
|
59
|
+
});
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listProjects({ cwd } = {}) {
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execa(RAILWAY, ["list", "--json"], {
|
|
69
|
+
cwd,
|
|
70
|
+
timeout: 15_000,
|
|
71
|
+
});
|
|
72
|
+
const all = JSON.parse(stdout);
|
|
73
|
+
return Array.isArray(all) ? all.filter((p) => !p.deletedAt) : [];
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function ensureRailwayProject({
|
|
80
|
+
projectName,
|
|
81
|
+
projectId,
|
|
82
|
+
cwd,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const alreadyLinked = await isProjectLinked(cwd);
|
|
85
|
+
|
|
86
|
+
if (alreadyLinked) {
|
|
87
|
+
try {
|
|
88
|
+
await execa(RAILWAY, ["status"], { cwd, timeout: 15_000 });
|
|
89
|
+
return { created: false, linked: true, projectName: null };
|
|
90
|
+
} catch {
|
|
91
|
+
// Stale link - clean up so we don't create an orphan project
|
|
92
|
+
const railwayDir = path.join(cwd, ".railway");
|
|
93
|
+
await fs.rm(railwayDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (projectId) {
|
|
98
|
+
await execa(RAILWAY, ["link", "--project", projectId], {
|
|
99
|
+
cwd,
|
|
100
|
+
timeout: 30_000,
|
|
101
|
+
});
|
|
102
|
+
return { created: false, linked: true, projectName: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!projectName) {
|
|
106
|
+
projectName = path.basename(path.resolve(cwd));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if a project with this name already exists on Railway
|
|
110
|
+
// before calling init (which would create "quark-1", "quark-2", etc.)
|
|
111
|
+
const existing = await listProjects({ cwd });
|
|
112
|
+
const match = existing.find((p) => p.name === projectName);
|
|
113
|
+
if (match) {
|
|
114
|
+
await execa(RAILWAY, ["link", "--project", match.id], {
|
|
115
|
+
cwd,
|
|
116
|
+
timeout: 30_000,
|
|
117
|
+
});
|
|
118
|
+
return { created: false, linked: true, projectName: match.name };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create new project with a single retry for transient API failures
|
|
122
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
await execa(RAILWAY, ["init", "--name", projectName, "--json"], {
|
|
125
|
+
cwd,
|
|
126
|
+
timeout: 60_000,
|
|
127
|
+
});
|
|
128
|
+
return { created: true, linked: true, projectName };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (attempt === 2) {
|
|
131
|
+
// Check if the project was actually created despite the error
|
|
132
|
+
const updated = await listProjects({ cwd });
|
|
133
|
+
const created = updated.find(
|
|
134
|
+
(p) => p.name === projectName || p.name === `${projectName}-1`,
|
|
135
|
+
);
|
|
136
|
+
if (created) {
|
|
137
|
+
await execa(RAILWAY, ["link", "--project", created.id], {
|
|
138
|
+
cwd,
|
|
139
|
+
timeout: 30_000,
|
|
140
|
+
});
|
|
141
|
+
return { created: true, linked: true, projectName: created.name };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const stderr = error.stderr || "";
|
|
145
|
+
if (stderr.includes("already exists")) {
|
|
146
|
+
const retryMatch = updated.find((p) => p.name === projectName);
|
|
147
|
+
if (retryMatch) {
|
|
148
|
+
await execa(RAILWAY, ["link", "--project", retryMatch.id], {
|
|
149
|
+
cwd,
|
|
150
|
+
timeout: 30_000,
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
created: false,
|
|
154
|
+
linked: true,
|
|
155
|
+
projectName: retryMatch.name,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function ensurePlugin(pluginName, { cwd } = {}) {
|
|
167
|
+
try {
|
|
168
|
+
const { stdout } = await execa(
|
|
169
|
+
RAILWAY,
|
|
170
|
+
["add", "--database", pluginName, "--json"],
|
|
171
|
+
{
|
|
172
|
+
cwd,
|
|
173
|
+
timeout: 60_000,
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
// --json output: [{ name, id, ... }] or { name, id }
|
|
177
|
+
const parsed = JSON.parse(stdout);
|
|
178
|
+
const serviceInfo = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
179
|
+
const rawName = serviceInfo?.name || pluginName;
|
|
180
|
+
const serviceName = rawName.charAt(0).toUpperCase() + rawName.slice(1);
|
|
181
|
+
return {
|
|
182
|
+
added: true,
|
|
183
|
+
serviceName,
|
|
184
|
+
serviceId: serviceInfo?.id || null,
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const stderr = error.stderr || "";
|
|
188
|
+
if (stderr.includes("already exists") || stderr.includes("already")) {
|
|
189
|
+
return { added: false, exists: true };
|
|
190
|
+
}
|
|
191
|
+
throw new RailwayError(
|
|
192
|
+
`Failed to provision ${pluginName}: ${error.message}`,
|
|
193
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
194
|
+
{ pluginName, stderr },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function ensureService(serviceName, { cwd } = {}) {
|
|
200
|
+
try {
|
|
201
|
+
await execa(RAILWAY, ["add", "--service", serviceName, "--json"], {
|
|
202
|
+
cwd,
|
|
203
|
+
timeout: 30_000,
|
|
204
|
+
});
|
|
205
|
+
return { created: true };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const stderr = error.stderr || "";
|
|
208
|
+
if (stderr.includes("already exists") || stderr.includes("already")) {
|
|
209
|
+
return { created: false, exists: true };
|
|
210
|
+
}
|
|
211
|
+
return { created: false, exists: false };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function deployService(serviceName, { cwd, environment } = {}) {
|
|
216
|
+
const args = ["up", "--service", serviceName, "--detach", "--json"];
|
|
217
|
+
if (environment) {
|
|
218
|
+
args.push("--environment", environment);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let deploymentId;
|
|
222
|
+
try {
|
|
223
|
+
const { stdout } = await execa(RAILWAY, args, {
|
|
224
|
+
cwd,
|
|
225
|
+
timeout: 120_000,
|
|
226
|
+
});
|
|
227
|
+
const parsed = JSON.parse(stdout);
|
|
228
|
+
deploymentId = parsed?.deploymentId || parsed?.id;
|
|
229
|
+
if (!deploymentId) {
|
|
230
|
+
throw new RailwayError(
|
|
231
|
+
`No deployment ID returned for "${serviceName}"`,
|
|
232
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof RailwayError) throw error;
|
|
237
|
+
throw new RailwayError(
|
|
238
|
+
`Failed to start deployment for "${serviceName}": ${error.message}`,
|
|
239
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
240
|
+
{ serviceName, stderr: error.stderr },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Poll for deployment completion via deployment list (status field)
|
|
245
|
+
const pollArgs = ["deployment", "list", "--json", "--service", serviceName];
|
|
246
|
+
if (environment) {
|
|
247
|
+
pollArgs.push("--environment", environment);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
251
|
+
let spinIdx = 0;
|
|
252
|
+
const spinner = setInterval(() => {
|
|
253
|
+
spinIdx = (spinIdx + 1) % spinnerChars.length;
|
|
254
|
+
process.stdout.write(
|
|
255
|
+
`\r ${chalk.cyan(spinnerChars[spinIdx])} Deploying ${serviceName}...`,
|
|
256
|
+
);
|
|
257
|
+
}, 100);
|
|
258
|
+
|
|
259
|
+
const startTime = Date.now();
|
|
260
|
+
const maxWait = 600_000; // 10 minute overall timeout
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
while (Date.now() - startTime < maxWait) {
|
|
264
|
+
await sleep(5_000);
|
|
265
|
+
let deployments;
|
|
266
|
+
try {
|
|
267
|
+
const { stdout } = await execa(RAILWAY, pollArgs, {
|
|
268
|
+
cwd,
|
|
269
|
+
timeout: 15_000,
|
|
270
|
+
});
|
|
271
|
+
deployments = JSON.parse(stdout);
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const deploy = Array.isArray(deployments)
|
|
277
|
+
? deployments.find((d) => d.id === deploymentId)
|
|
278
|
+
: null;
|
|
279
|
+
if (!deploy) continue;
|
|
280
|
+
|
|
281
|
+
const status = (deploy.status || "").toUpperCase();
|
|
282
|
+
|
|
283
|
+
if (status === "SUCCESS" || status === "HEALTHY") {
|
|
284
|
+
clearInterval(spinner);
|
|
285
|
+
process.stdout.write(
|
|
286
|
+
`\r ${chalk.green("✔")} Deploying ${serviceName}... done\n`,
|
|
287
|
+
);
|
|
288
|
+
return { serviceName, success: true, deploymentId };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (status === "CRASHED" || status === "FAILED") {
|
|
292
|
+
clearInterval(spinner);
|
|
293
|
+
process.stdout.write(
|
|
294
|
+
`\r ${chalk.red("✖")} Deploying ${serviceName}... failed\n`,
|
|
295
|
+
);
|
|
296
|
+
throw new RailwayError(
|
|
297
|
+
`Deployment failed for "${serviceName}" (status: ${status})`,
|
|
298
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
299
|
+
{ serviceName, deploymentId, status },
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
clearInterval(spinner);
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
`\r ${chalk.red("✖")} Deploying ${serviceName}... timed out\n`,
|
|
307
|
+
);
|
|
308
|
+
throw new RailwayError(
|
|
309
|
+
`Deployment timed out for "${serviceName}" after 10 minutes`,
|
|
310
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
311
|
+
{ serviceName, deploymentId },
|
|
312
|
+
);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
clearInterval(spinner);
|
|
315
|
+
if (error instanceof RailwayError) throw error;
|
|
316
|
+
throw new RailwayError(
|
|
317
|
+
`Failed to check deployment status for "${serviceName}": ${error.message}`,
|
|
318
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
319
|
+
{ serviceName, deploymentId },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function deleteService(serviceName, { cwd, environment } = {}) {
|
|
325
|
+
const args = [
|
|
326
|
+
"service",
|
|
327
|
+
"delete",
|
|
328
|
+
"--service",
|
|
329
|
+
serviceName,
|
|
330
|
+
"--yes",
|
|
331
|
+
"--json",
|
|
332
|
+
];
|
|
333
|
+
if (environment) {
|
|
334
|
+
args.push("--environment", environment);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await execa(RAILWAY, args, { cwd, timeout: 30_000 });
|
|
339
|
+
return true;
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function getExistingVariable(
|
|
346
|
+
key,
|
|
347
|
+
{ cwd, environment, serviceName } = {},
|
|
348
|
+
) {
|
|
349
|
+
const args = ["variable", "list", "--json"];
|
|
350
|
+
if (serviceName) {
|
|
351
|
+
args.push("--service", serviceName);
|
|
352
|
+
}
|
|
353
|
+
if (environment) {
|
|
354
|
+
args.push("--environment", environment);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const { stdout } = await execa(RAILWAY, args, { cwd, timeout: 10_000 });
|
|
359
|
+
const vars = JSON.parse(stdout);
|
|
360
|
+
if (Array.isArray(vars)) {
|
|
361
|
+
const found = vars.find((v) => v.name === key || v.key === key);
|
|
362
|
+
return found?.value || found?.rawValue || null;
|
|
363
|
+
}
|
|
364
|
+
if (vars && typeof vars === "object") {
|
|
365
|
+
return vars[key] || null;
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function getServiceUrl(serviceName, { cwd } = {}) {
|
|
374
|
+
try {
|
|
375
|
+
const { stdout } = await execa(
|
|
376
|
+
RAILWAY,
|
|
377
|
+
["domain", "--service", serviceName, "--json"],
|
|
378
|
+
{
|
|
379
|
+
cwd,
|
|
380
|
+
timeout: 15_000,
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
const parsed = JSON.parse(stdout);
|
|
384
|
+
return parsed?.[0]?.domain || parsed?.domain || null;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function setProjectVariable(
|
|
391
|
+
key,
|
|
392
|
+
value,
|
|
393
|
+
{ cwd, environment, serviceName } = {},
|
|
394
|
+
) {
|
|
395
|
+
const args = [
|
|
396
|
+
"variable",
|
|
397
|
+
"set",
|
|
398
|
+
`${key}=${value}`,
|
|
399
|
+
"--json",
|
|
400
|
+
"--skip-deploys",
|
|
401
|
+
];
|
|
402
|
+
if (serviceName) {
|
|
403
|
+
args.push("--service", serviceName);
|
|
404
|
+
}
|
|
405
|
+
if (environment) {
|
|
406
|
+
args.push("--environment", environment);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
await execa(RAILWAY, args, { cwd, timeout: 60_000 });
|
|
411
|
+
return true;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const errMsg = error.stderr?.trim() || error.shortMessage || error.message;
|
|
414
|
+
throw new RailwayError(
|
|
415
|
+
`Failed to set variable "${key}" for service "${serviceName || "(default)"}": ${errMsg}`,
|
|
416
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
417
|
+
{ key, value, serviceName, environment },
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function setProjectVariables(
|
|
423
|
+
vars,
|
|
424
|
+
{ cwd, environment, serviceName } = {},
|
|
425
|
+
) {
|
|
426
|
+
const args = [
|
|
427
|
+
"variable",
|
|
428
|
+
"set",
|
|
429
|
+
...vars.map(({ key, value }) => `${key}=${value}`),
|
|
430
|
+
"--json",
|
|
431
|
+
"--skip-deploys",
|
|
432
|
+
];
|
|
433
|
+
if (serviceName) {
|
|
434
|
+
args.push("--service", serviceName);
|
|
435
|
+
}
|
|
436
|
+
if (environment) {
|
|
437
|
+
args.push("--environment", environment);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const maxWait = 120_000;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
await execa(RAILWAY, args, { cwd, timeout: maxWait });
|
|
444
|
+
return true;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const errMsg = error.stderr?.trim() || error.shortMessage || error.message;
|
|
447
|
+
|
|
448
|
+
// Check if variables were actually set despite the timeout
|
|
449
|
+
// by verifying the first and last keys.
|
|
450
|
+
const sampleKeys = [];
|
|
451
|
+
if (vars.length > 0) {
|
|
452
|
+
sampleKeys.push(vars[0].key);
|
|
453
|
+
if (vars.length > 1) {
|
|
454
|
+
sampleKeys.push(vars[vars.length - 1].key);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
let allSet = sampleKeys.length > 0;
|
|
458
|
+
for (const key of sampleKeys) {
|
|
459
|
+
try {
|
|
460
|
+
const existing = await getExistingVariable(key, {
|
|
461
|
+
cwd,
|
|
462
|
+
environment,
|
|
463
|
+
serviceName,
|
|
464
|
+
});
|
|
465
|
+
if (!existing) {
|
|
466
|
+
allSet = false;
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
allSet = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (allSet) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
throw new RailwayError(
|
|
477
|
+
`Failed to set variables for service "${serviceName || "(default)"}": ${errMsg}`,
|
|
478
|
+
DIAGNOSTIC_CODES.RAILWAY_DEPLOY_FAILED,
|
|
479
|
+
{ vars, serviceName, environment },
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function removeServiceDomain(serviceName, { cwd } = {}) {
|
|
485
|
+
try {
|
|
486
|
+
const url = await getServiceUrl(serviceName, { cwd });
|
|
487
|
+
if (!url) return false;
|
|
488
|
+
await execa(RAILWAY, ["domain", "remove", "--service", serviceName, url], {
|
|
489
|
+
cwd,
|
|
490
|
+
timeout: 15_000,
|
|
491
|
+
});
|
|
492
|
+
return true;
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export async function getProjectDomains({ cwd } = {}) {
|
|
499
|
+
try {
|
|
500
|
+
const { stdout } = await execa(RAILWAY, ["domain", "--json"], {
|
|
501
|
+
cwd,
|
|
502
|
+
timeout: 15_000,
|
|
503
|
+
});
|
|
504
|
+
return JSON.parse(stdout);
|
|
505
|
+
} catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|