@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.
Files changed (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,430 @@
1
+ import path from "node:path";
2
+ import chalk from "chalk";
3
+ import { generateSecret, sleep } from "../utils.js";
4
+ import {
5
+ applyIacConfig,
6
+ checkRailwayCLI,
7
+ checkRailwayLogin,
8
+ ensurePlugin,
9
+ ensureRailwayProject,
10
+ escapeTsString,
11
+ generateIacFile,
12
+ getDeploymentStatus,
13
+ getExistingVariable,
14
+ getServiceUrl,
15
+ installRailwaySdk,
16
+ removeServiceDomain,
17
+ setProjectVariables,
18
+ } from "./adapters/index.js";
19
+ import { resolveQuarkDeployProject } from "./discovery.js";
20
+
21
+ async function healthCheckService(url) {
22
+ const healthUrl = `${url.replace(/\/$/, "")}/api/health`;
23
+ const maxWait = 60_000;
24
+ const startTime = Date.now();
25
+ let lastError;
26
+
27
+ while (Date.now() - startTime < maxWait) {
28
+ try {
29
+ const response = await fetch(healthUrl, {
30
+ signal: AbortSignal.timeout(5_000),
31
+ });
32
+ if (response.ok) {
33
+ console.log(
34
+ chalk.green(` ✔ Health check passed (${response.status})`),
35
+ );
36
+ return true;
37
+ }
38
+ lastError = `HTTP ${response.status}`;
39
+ } catch (error) {
40
+ lastError = error.message || "connection refused";
41
+ }
42
+ await sleep(3_000);
43
+ }
44
+
45
+ console.log(
46
+ chalk.yellow(` ⚠ Health check timed out: ${lastError} - verify manually`),
47
+ );
48
+ return false;
49
+ }
50
+
51
+ export async function deployToRailway(options = {}) {
52
+ const {
53
+ cwd = process.cwd(),
54
+ projectName,
55
+ projectId,
56
+ environment,
57
+ provision = true,
58
+ } = options;
59
+
60
+ console.log(chalk.blue.bold("\n⚡ Deploying to Railway\n"));
61
+
62
+ // --- Step 1: Check Railway CLI ---
63
+ console.log(chalk.cyan(" Checking Railway CLI..."));
64
+ const version = await checkRailwayCLI();
65
+ if (!version) {
66
+ console.error(chalk.red(" ✖ Railway CLI not found."));
67
+ console.error(chalk.yellow(" Install: npm install -g @railway/cli"));
68
+ console.error(
69
+ chalk.yellow(" Or: brew install railwayhq/brew/railway"),
70
+ );
71
+ throw new Error("Railway CLI is required but was not found");
72
+ }
73
+ console.log(chalk.green(` ✔ Railway CLI detected (${version})`));
74
+
75
+ // --- Step 2: Check login ---
76
+ console.log(chalk.cyan(" Checking Railway login..."));
77
+ const user = await checkRailwayLogin();
78
+ if (!user) {
79
+ console.error(chalk.red(" ✖ Not logged into Railway."));
80
+ console.error(chalk.yellow(" Run: railway login"));
81
+ throw new Error("Not logged into Railway");
82
+ }
83
+ console.log(chalk.green(` ✔ Logged in as ${user}`));
84
+
85
+ // --- Step 3: Discover project ---
86
+ console.log(chalk.cyan(" Discovering Quark project..."));
87
+ let discovery;
88
+ try {
89
+ discovery = await resolveQuarkDeployProject(cwd);
90
+ } catch (error) {
91
+ console.error(chalk.red(` ✖ ${error.message}`));
92
+ throw new Error(`Project discovery failed: ${error.message}`);
93
+ }
94
+ console.log(
95
+ chalk.green(
96
+ ` ✔ Discovered ${discovery.services.length} service(s): ${discovery.services.map((s) => s.name).join(", ")}`,
97
+ ),
98
+ );
99
+
100
+ // --- Step 4: Ensure Railway project ---
101
+ console.log(chalk.cyan(" Ensuring Railway project..."));
102
+ try {
103
+ const project = await ensureRailwayProject({ projectName, projectId, cwd });
104
+ if (project.created) {
105
+ console.log(
106
+ chalk.green(
107
+ ` ✔ Created Railway project "${project.projectName || projectName || ""}"`,
108
+ ),
109
+ );
110
+ } else {
111
+ console.log(chalk.green(" ✔ Railway project linked"));
112
+ }
113
+ } catch (error) {
114
+ console.error(
115
+ chalk.red(` ✖ Failed to link Railway project: ${error.message}`),
116
+ );
117
+ throw new Error(`Project linking failed: ${error.message}`);
118
+ }
119
+
120
+ // --- Step 5: Provision plugins ---
121
+ let pgServiceName = "Postgres";
122
+ let redisServiceName = "Redis";
123
+ if (provision) {
124
+ console.log(chalk.cyan(" Provisioning PostgreSQL..."));
125
+ const pg = await ensurePlugin("postgres", { cwd });
126
+ if (pg.added) {
127
+ console.log(chalk.green(" ✔ PostgreSQL provisioned"));
128
+ if (pg.serviceName) pgServiceName = pg.serviceName;
129
+ } else {
130
+ console.log(chalk.dim(" · PostgreSQL already present or skipped"));
131
+ }
132
+
133
+ console.log(chalk.cyan(" Provisioning Redis..."));
134
+ const redis = await ensurePlugin("redis", { cwd });
135
+ if (redis.added) {
136
+ console.log(chalk.green(" ✔ Redis provisioned"));
137
+ if (redis.serviceName) redisServiceName = redis.serviceName;
138
+ } else {
139
+ console.log(chalk.dim(" · Redis already present or skipped"));
140
+ }
141
+ }
142
+
143
+ // --- Step 6: Preserve existing secrets across deploys ---
144
+ const projectLabel = path.basename(cwd);
145
+ let authSecret;
146
+ let nextAuthSecret;
147
+
148
+ if (discovery.services.length > 0) {
149
+ const firstSvc = discovery.services[0];
150
+
151
+ const existingAuthSecret = await getExistingVariable("AUTH_SECRET", {
152
+ cwd,
153
+ environment,
154
+ serviceName: firstSvc.name,
155
+ });
156
+ authSecret = existingAuthSecret || generateSecret();
157
+
158
+ const existingNextAuthSecret = await getExistingVariable(
159
+ "NEXTAUTH_SECRET",
160
+ {
161
+ cwd,
162
+ environment,
163
+ serviceName: firstSvc.name,
164
+ },
165
+ );
166
+ nextAuthSecret = existingNextAuthSecret || generateSecret();
167
+ }
168
+
169
+ // --- Step 7: Generate IaC file (.railway/railway.ts) ---
170
+ console.log(chalk.cyan(" Generating IaC configuration..."));
171
+ const iacPath = `${cwd}/.railway/railway.ts`;
172
+
173
+ await generateIacFile({
174
+ iacPath,
175
+ services: discovery.services,
176
+ // Pass raw name — generateIacFile handles escaping internally
177
+ projectName: projectName || projectLabel,
178
+ variableRefs: {
179
+ DATABASE_URL: `\${{${pgServiceName}.DATABASE_URL}}`,
180
+ REDIS_URL: `\${{${redisServiceName}.REDIS_URL}}`,
181
+ NODE_ENV: '"production"',
182
+ APP_NAME: `"${escapeTsString(projectLabel)}"`,
183
+ APP_DESCRIPTION: `"${escapeTsString(`${projectLabel} - Quark application`)}"`,
184
+ },
185
+ secrets: {
186
+ AUTH_SECRET: "preserve()",
187
+ NEXTAUTH_SECRET: "preserve()",
188
+ },
189
+ serviceVars: {
190
+ worker: {
191
+ WORKER_CONCURRENCY: '"5"',
192
+ },
193
+ web: {
194
+ AUTH_ALLOW_SIGNUP: '"false"',
195
+ HOSTNAME: '"0.0.0.0"',
196
+ STORAGE_PROVIDER: '"local"',
197
+ },
198
+ },
199
+ });
200
+ console.log(chalk.green(` ✔ IaC file generated at .railway/railway.ts`));
201
+
202
+ // --- Step 8: Install Railway SDK (required for IaC evaluation) ---
203
+ console.log(chalk.cyan(" Installing Railway SDK..."));
204
+ try {
205
+ await installRailwaySdk({ cwd });
206
+ console.log(chalk.green(" ✔ Railway SDK installed"));
207
+ } catch (error) {
208
+ // SDK is mandatory — railway config apply cannot evaluate .railway/railway.ts without it
209
+ console.error(
210
+ chalk.red(` ✖ Failed to install Railway SDK: ${error.message}`),
211
+ );
212
+ throw new Error(
213
+ `Railway SDK installation failed. The generated IaC file requires the "railway" package.\n` +
214
+ `Install it manually: pnpm add -D railway\n` +
215
+ `Original error: ${error.message}`,
216
+ );
217
+ }
218
+
219
+ // --- Step 9: Set dynamic variable references (infrastructure wiring) ---
220
+ // These values cannot be represented in IaC: Railway reference strings
221
+ // like ${{Postgres.DATABASE_URL}} resolve at runtime, and secrets use
222
+ // preserve() to keep existing Railway-managed values.
223
+ //
224
+ // Variables are set BEFORE IaC apply because:
225
+ // - setProjectVariables uses --skip-deploys (no redundant redeploy)
226
+ // - IaC apply then triggers a single deployment with all variables in place
227
+ // - Status check after apply sees the correct deployment
228
+ let anyVarFailed = false;
229
+
230
+ for (const service of discovery.services) {
231
+ const sharedVars = [
232
+ { key: "DATABASE_URL", value: `\${{${pgServiceName}.DATABASE_URL}}` },
233
+ { key: "REDIS_URL", value: `\${{${redisServiceName}.REDIS_URL}}` },
234
+ { key: "APP_NAME", value: projectLabel },
235
+ {
236
+ key: "APP_DESCRIPTION",
237
+ value: `${projectLabel} - Quark application`,
238
+ },
239
+ { key: "NODE_ENV", value: "production" },
240
+ { key: "AUTH_SECRET", value: authSecret },
241
+ { key: "NEXTAUTH_SECRET", value: nextAuthSecret },
242
+ { key: "STORAGE_PROVIDER", value: "local" },
243
+ ];
244
+
245
+ const webVars = [
246
+ ...sharedVars,
247
+ { key: "AUTH_ALLOW_SIGNUP", value: "false" },
248
+ { key: "HOSTNAME", value: "0.0.0.0" },
249
+ ];
250
+
251
+ const workerVars = [
252
+ ...sharedVars,
253
+ { key: "WORKER_CONCURRENCY", value: "5" },
254
+ ];
255
+
256
+ const serviceVars = service.kind === "worker" ? workerVars : webVars;
257
+
258
+ try {
259
+ const ok = await setProjectVariables(serviceVars, {
260
+ cwd,
261
+ environment,
262
+ serviceName: service.name,
263
+ });
264
+ if (!ok) {
265
+ anyVarFailed = true;
266
+ }
267
+ } catch (error) {
268
+ console.error(
269
+ chalk.yellow(
270
+ ` ⚠ Could not set variables for "${service.name}": ${error.message}`,
271
+ ),
272
+ );
273
+ anyVarFailed = true;
274
+ }
275
+ }
276
+
277
+ if (!anyVarFailed) {
278
+ console.log(chalk.green(" ✔ Required variables set"));
279
+ }
280
+
281
+ // --- Step 10: Apply IaC configuration ---
282
+ // Apply AFTER variables are set so the resulting deployment uses all
283
+ // configured values. The status check below sees this deployment.
284
+ console.log(chalk.cyan(" Applying IaC configuration..."));
285
+ const applyResult = await applyIacConfig({ cwd });
286
+
287
+ if (!applyResult.success) {
288
+ console.error(chalk.red(` ✖ IaC apply failed: ${applyResult.output}`));
289
+ throw new Error(`IaC apply failed: ${applyResult.output}`);
290
+ }
291
+ console.log(chalk.green(" ✔ IaC configuration applied"));
292
+
293
+ // --- Step 11: Verify deployment status after IaC apply ---
294
+ // railway config apply triggers builds but may not wait for them to complete.
295
+ // Poll deployment status to verify the apply actually resulted in a healthy deploy.
296
+ const results = [];
297
+
298
+ for (const service of discovery.services) {
299
+ if (service.kind === "web") {
300
+ // Check deployment status
301
+ const { status } = await getDeploymentStatus(service.name, {
302
+ cwd,
303
+ environment,
304
+ });
305
+
306
+ if (status === "CRASHED" || status === "FAILED") {
307
+ console.error(
308
+ chalk.red(
309
+ ` ✖ ${service.name} deployment failed (status: ${status})`,
310
+ ),
311
+ );
312
+ results.push({
313
+ kind: service.kind,
314
+ name: service.name,
315
+ success: false,
316
+ healthy: false,
317
+ url: null,
318
+ });
319
+ continue;
320
+ }
321
+
322
+ // Deployment is building/deploying or succeeded — check health
323
+ const url = await getServiceUrl(service.name, { cwd });
324
+ if (url) {
325
+ console.log(chalk.cyan(` Checking ${service.name} health...`));
326
+ const healthy = await healthCheckService(url);
327
+ results.push({
328
+ kind: service.kind,
329
+ name: service.name,
330
+ success: true,
331
+ healthy,
332
+ url,
333
+ });
334
+ } else {
335
+ results.push({
336
+ kind: service.kind,
337
+ name: service.name,
338
+ success: status === "SUCCESS" || status === "HEALTHY",
339
+ healthy: false,
340
+ url: null,
341
+ });
342
+ }
343
+ } else {
344
+ // Worker: verify deployment didn't crash
345
+ const { status } = await getDeploymentStatus(service.name, {
346
+ cwd,
347
+ environment,
348
+ });
349
+
350
+ const deployed = status !== "CRASHED" && status !== "FAILED";
351
+
352
+ // Remove default Railway domain for worker (unused)
353
+ if (deployed) {
354
+ const removed = await removeServiceDomain(service.name, { cwd });
355
+ if (removed) {
356
+ console.log(chalk.dim(` · Worker domain removed`));
357
+ }
358
+ }
359
+
360
+ results.push({
361
+ kind: service.kind,
362
+ name: service.name,
363
+ success: deployed,
364
+ healthy: deployed,
365
+ url: null,
366
+ });
367
+ }
368
+ }
369
+
370
+ // --- Step 12: Summary ---
371
+ const allDeployed = results.every((r) => r.success);
372
+ const allHealthy = results.every((r) => r.healthy !== false);
373
+
374
+ console.log(chalk.blue.bold("\n📋 Deploy Summary\n"));
375
+ for (const result of results) {
376
+ const icon = result.success ? chalk.green("✔") : chalk.red("✖");
377
+ const healthIcon =
378
+ result.healthy === false ? chalk.yellow(" ⚠ unhealthy") : "";
379
+ const url = result.url ? chalk.white(` → ${result.url}`) : "";
380
+ console.log(` ${icon} ${result.name}${url}${healthIcon}`);
381
+ }
382
+
383
+ if (allDeployed && allHealthy) {
384
+ console.log(chalk.green.bold("\n✅ Deploy complete!\n"));
385
+ } else if (allDeployed && !allHealthy) {
386
+ console.log(
387
+ chalk.yellow.bold("\n⚠ Deploy complete but health check failed!\n"),
388
+ );
389
+ console.log(
390
+ chalk.yellow(
391
+ " The service was deployed but did not become healthy within the timeout.",
392
+ ),
393
+ );
394
+ console.log(
395
+ chalk.yellow(" Check Railway logs: `railway logs --service web`"),
396
+ );
397
+ console.log("");
398
+ } else {
399
+ console.log(chalk.red.bold("\n❌ Deploy failed - see errors above\n"));
400
+ }
401
+
402
+ if (anyVarFailed) {
403
+ console.log(
404
+ chalk.yellow(" ⚠ Some environment variables could not be set."),
405
+ );
406
+ console.log(
407
+ chalk.yellow(" Your services may not work correctly without them."),
408
+ );
409
+ console.log(
410
+ chalk.yellow(
411
+ " Run `railway variable set KEY=VALUE --service <name>` to fix.\n",
412
+ ),
413
+ );
414
+ }
415
+
416
+ if (allDeployed) {
417
+ console.log(chalk.cyan("Next steps:"));
418
+ console.log(
419
+ chalk.white(" 1. Configure custom domains in your Railway dashboard"),
420
+ );
421
+ console.log(chalk.white(""));
422
+ console.log(
423
+ chalk.white(
424
+ " 2. Run `quark deploy status` to check deployment status\n",
425
+ ),
426
+ );
427
+ }
428
+
429
+ return { success: allDeployed, healthy: allHealthy, services: results };
430
+ }
@@ -0,0 +1,105 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { after, test } from "node:test";
6
+
7
+ const temporaryDirectories = [];
8
+
9
+ after(async () => {
10
+ await Promise.all(
11
+ temporaryDirectories.map((dir) =>
12
+ fs.rm(dir, { recursive: true, force: true }),
13
+ ),
14
+ );
15
+ });
16
+
17
+ async function _makeTempDir() {
18
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "deploy-test-"));
19
+ temporaryDirectories.push(dir);
20
+ return dir;
21
+ }
22
+
23
+ async function _writeJson(filePath, value) {
24
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
25
+ await fs.writeFile(filePath, JSON.stringify(value, null, "\t"));
26
+ }
27
+
28
+ test("deployToRailway validates CLI prerequisites", async () => {
29
+ const { deployToRailway } = await import("./deploy.js");
30
+ assert.equal(typeof deployToRailway, "function");
31
+ });
32
+
33
+ test("inspectProject module loads correctly", async () => {
34
+ const { inspectProject } = await import("./inspect.js");
35
+ assert.equal(typeof inspectProject, "function");
36
+ });
37
+
38
+ test("adapters index exports all expected functions", async () => {
39
+ const mod = await import("./adapters/index.js");
40
+ const expectedExports = [
41
+ "checkRailwayCLI",
42
+ "checkRailwayLogin",
43
+ "deleteService",
44
+ "ensureRailwayProject",
45
+ "ensurePlugin",
46
+ "ensureService",
47
+ "deployService",
48
+ "getExistingVariable",
49
+ "getServiceUrl",
50
+ "setProjectVariables",
51
+ "getProjectDomains",
52
+ "isProjectLinked",
53
+ "listProjects",
54
+ "tryLinkProject",
55
+ "RailwayError",
56
+ "RAILWAY_DIAGNOSTIC_CODES",
57
+ "generateIacFile",
58
+ "applyIacConfig",
59
+ "installRailwaySdk",
60
+ "escapeTsString",
61
+ "getDeploymentStatus",
62
+ ];
63
+ for (const name of expectedExports) {
64
+ assert.ok(name in mod, `Expected export "${name}" to exist`);
65
+ }
66
+ });
67
+
68
+ test("deploy index exports all expected functions", async () => {
69
+ const mod = await import("./index.js");
70
+ const expectedExports = [
71
+ "QUARK_DEPLOY_PROJECT_KIND",
72
+ "QUARK_SERVICE_CONTRACTS",
73
+ "QUARK_SERVICE_KINDS",
74
+ "SUPPORTED_QUARK_SERVICE_KINDS",
75
+ "discoverQuarkDeployProject",
76
+ "formatQuarkDeployDiagnostics",
77
+ "QUARK_DEPLOY_DIAGNOSTIC_CODES",
78
+ "resolveQuarkDeployProject",
79
+ "deployToRailway",
80
+ "inspectProject",
81
+ "checkRailwayCLI",
82
+ "checkRailwayLogin",
83
+ "deleteService",
84
+ "ensureRailwayProject",
85
+ "ensurePlugin",
86
+ "ensureService",
87
+ "deployService",
88
+ "getExistingVariable",
89
+ "getServiceUrl",
90
+ "setProjectVariables",
91
+ "isProjectLinked",
92
+ "listProjects",
93
+ "tryLinkProject",
94
+ "RailwayError",
95
+ "RAILWAY_DIAGNOSTIC_CODES",
96
+ "generateIacFile",
97
+ "applyIacConfig",
98
+ "installRailwaySdk",
99
+ "escapeTsString",
100
+ "getDeploymentStatus",
101
+ ];
102
+ for (const name of expectedExports) {
103
+ assert.ok(name in mod, `Expected export "${name}" to exist`);
104
+ }
105
+ });
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ QUARK_DEPLOY_PROJECT_KIND,
5
+ QUARK_SERVICE_CONTRACTS,
6
+ SUPPORTED_QUARK_SERVICE_KINDS,
7
+ } from "./contract.js";
8
+
9
+ export const QUARK_DEPLOY_DIAGNOSTIC_CODES = Object.freeze({
10
+ MISSING_REQUIRED_SERVICE: "missing_required_service",
11
+ });
12
+
13
+ function resolveProjectPath(projectDir, relativePath) {
14
+ return path.join(projectDir, ...relativePath.split("/"));
15
+ }
16
+
17
+ async function readJsonIfExists(filePath) {
18
+ try {
19
+ const contents = await fs.readFile(filePath, "utf8");
20
+ return JSON.parse(contents);
21
+ } catch (error) {
22
+ if (error && typeof error === "object" && error.code === "ENOENT") {
23
+ return null;
24
+ }
25
+
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ function createMissingRequiredServiceDiagnostic(serviceContract) {
31
+ return {
32
+ code: QUARK_DEPLOY_DIAGNOSTIC_CODES.MISSING_REQUIRED_SERVICE,
33
+ serviceKind: serviceContract.kind,
34
+ required: true,
35
+ expectedPath: serviceContract.relativePackageJsonPath,
36
+ message: `Missing required Quark ${serviceContract.name} service at ${serviceContract.relativePackageJsonPath}.`,
37
+ };
38
+ }
39
+
40
+ function createDiscoveredService(projectDir, serviceContract, packageJson) {
41
+ return {
42
+ kind: serviceContract.kind,
43
+ name: serviceContract.name,
44
+ required: serviceContract.required,
45
+ rootDir: resolveProjectPath(projectDir, serviceContract.relativeRootDir),
46
+ relativeRootDir: serviceContract.relativeRootDir,
47
+ packageJsonPath: resolveProjectPath(
48
+ projectDir,
49
+ serviceContract.relativePackageJsonPath,
50
+ ),
51
+ relativePackageJsonPath: serviceContract.relativePackageJsonPath,
52
+ packageName:
53
+ typeof packageJson?.name === "string" ? packageJson.name : null,
54
+ runtime: {
55
+ ...serviceContract.runtime,
56
+ entrypointPath: resolveProjectPath(
57
+ projectDir,
58
+ serviceContract.runtime.relativeEntrypoint,
59
+ ),
60
+ },
61
+ };
62
+ }
63
+
64
+ export function formatQuarkDeployDiagnostics(diagnostics) {
65
+ return diagnostics.map((diagnostic) => diagnostic.message).join(". ");
66
+ }
67
+
68
+ export async function discoverQuarkDeployProject(projectDir) {
69
+ const services = [];
70
+ const diagnostics = [];
71
+
72
+ for (const serviceKind of SUPPORTED_QUARK_SERVICE_KINDS) {
73
+ const serviceContract = QUARK_SERVICE_CONTRACTS[serviceKind];
74
+ const packageJson = await readJsonIfExists(
75
+ resolveProjectPath(projectDir, serviceContract.relativePackageJsonPath),
76
+ );
77
+
78
+ if (!packageJson) {
79
+ if (serviceContract.required) {
80
+ diagnostics.push(
81
+ createMissingRequiredServiceDiagnostic(serviceContract),
82
+ );
83
+ }
84
+
85
+ continue;
86
+ }
87
+
88
+ services.push(
89
+ createDiscoveredService(projectDir, serviceContract, packageJson),
90
+ );
91
+ }
92
+
93
+ return {
94
+ kind: QUARK_DEPLOY_PROJECT_KIND,
95
+ rootDir: projectDir,
96
+ services,
97
+ diagnostics,
98
+ };
99
+ }
100
+
101
+ export async function resolveQuarkDeployProject(projectDir) {
102
+ const discovery = await discoverQuarkDeployProject(projectDir);
103
+
104
+ if (discovery.diagnostics.length > 0) {
105
+ const error = new Error(
106
+ formatQuarkDeployDiagnostics(discovery.diagnostics),
107
+ );
108
+ error.code = "QUARK_DEPLOY_DISCOVERY_FAILED";
109
+ error.diagnostics = discovery.diagnostics;
110
+ error.discovery = discovery;
111
+ throw error;
112
+ }
113
+
114
+ return discovery;
115
+ }