@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,438 @@
1
+ /**
2
+ * Railway API Client
3
+ * Fetches estimated usage and deployment data from Railway's GraphQL API.
4
+ */
5
+
6
+ import { createLogger } from "@techstream/quark-core";
7
+ import { AppError } from "@techstream/quark-core/errors";
8
+
9
+ const log = createLogger("lib:railway");
10
+
11
+ const RAILWAY_GRAPHQL_URL = "https://backboard.railway.com/graphql/v2";
12
+
13
+ /**
14
+ * Execute a GraphQL query against Railway's API.
15
+ */
16
+ async function graphql(query, variables = {}) {
17
+ const token = process.env.RAILWAY_API_TOKEN;
18
+ if (!token) {
19
+ throw new AppError(
20
+ "RAILWAY_API_TOKEN is not configured",
21
+ 500,
22
+ "RAILWAY_API_TOKEN_MISSING",
23
+ );
24
+ }
25
+
26
+ const response = await fetch(RAILWAY_GRAPHQL_URL, {
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ Authorization: `Bearer ${token}`,
31
+ },
32
+ body: JSON.stringify({ query, variables }),
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new AppError(
37
+ `Railway API error: ${response.status} ${response.statusText}`,
38
+ response.status,
39
+ "RAILWAY_API_ERROR",
40
+ );
41
+ }
42
+
43
+ const json = await response.json();
44
+
45
+ if (json.errors) {
46
+ throw new AppError(
47
+ `Railway GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`,
48
+ 502,
49
+ "RAILWAY_GRAPHQL_ERROR",
50
+ );
51
+ }
52
+
53
+ return json.data;
54
+ }
55
+
56
+ /**
57
+ * Fetch deployments for a specific Railway project/environment.
58
+ *
59
+ * @param {string} railwayProjectId - Railway project ID
60
+ * @param {string} [environmentId] - Optional environment ID
61
+ * @returns {Promise<Array>} Deployment records
62
+ */
63
+ export async function fetchProjectDeployments(railwayProjectId, environmentId) {
64
+ log.info("Fetching Railway deployments", { railwayProjectId, environmentId });
65
+
66
+ const query = `
67
+ query GetProjectDeployments($input: DeploymentListInput!) {
68
+ deployments(input: $input) {
69
+ edges {
70
+ node {
71
+ id
72
+ status
73
+ createdAt
74
+ meta
75
+ serviceId
76
+ service {
77
+ id
78
+ name
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ `;
85
+
86
+ try {
87
+ const data = await graphql(query, {
88
+ input: {
89
+ projectId: railwayProjectId,
90
+ ...(environmentId ? { environmentId } : {}),
91
+ },
92
+ });
93
+ const edges = data?.deployments?.edges || [];
94
+ return edges.map((e) => e.node);
95
+ } catch (error) {
96
+ log.error("Failed to fetch Railway deployments", {
97
+ railwayProjectId,
98
+ error: error.message,
99
+ });
100
+ return [];
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Fetch workspace-level billing data.
106
+ * Returns the total dollar cost for the current billing period.
107
+ *
108
+ * @param {string} workspaceId - Railway workspace ID
109
+ * @returns {Promise<object|null>} Billing data with currentUsage, billingPeriod, state
110
+ */
111
+ export async function fetchWorkspaceBilling(workspaceId) {
112
+ log.info("Fetching workspace billing", { workspaceId });
113
+
114
+ const query = `
115
+ query GetWorkspaceBilling($workspaceId: String!) {
116
+ workspace(workspaceId: $workspaceId) {
117
+ id
118
+ name
119
+ plan
120
+ customer {
121
+ currentUsage
122
+ creditBalance
123
+ appliedCredits
124
+ billingPeriod {
125
+ start
126
+ end
127
+ }
128
+ state
129
+ isTrialing
130
+ isUsageSubscriber
131
+ }
132
+ }
133
+ }
134
+ `;
135
+
136
+ try {
137
+ const data = await graphql(query, { workspaceId });
138
+ const workspace = data?.workspace;
139
+ if (!workspace?.customer) {
140
+ log.warn("No customer billing data found", { workspaceId });
141
+ return null;
142
+ }
143
+ return {
144
+ workspaceId: workspace.id,
145
+ workspaceName: workspace.name,
146
+ plan: workspace.plan,
147
+ currentUsage: workspace.customer.currentUsage,
148
+ creditBalance: workspace.customer.creditBalance,
149
+ appliedCredits: workspace.customer.appliedCredits,
150
+ billingPeriodStart: workspace.customer.billingPeriod.start,
151
+ billingPeriodEnd: workspace.customer.billingPeriod.end,
152
+ state: workspace.customer.state,
153
+ isTrialing: workspace.customer.isTrialing,
154
+ isUsageSubscriber: workspace.customer.isUsageSubscriber,
155
+ };
156
+ } catch (error) {
157
+ log.error("Failed to fetch workspace billing", {
158
+ workspaceId,
159
+ error: error.message,
160
+ });
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Normalize a Railway deployment record to our Deployment format.
167
+ *
168
+ * @param {object} deploy - Raw deployment node from Railway API
169
+ * @param {string} projectId - Our managed project ID
170
+ * @returns {object} Normalized deployment data
171
+ */
172
+ export function normalizeDeployment(deploy, projectId) {
173
+ const statusMap = {
174
+ SUCCESS: "SUCCESS",
175
+ FAILED: "FAILED",
176
+ BUILDING: "BUILDING",
177
+ DEPLOYING: "DEPLOYING",
178
+ CANCELLED: "CANCELLED",
179
+ REMOVED: "CANCELLED",
180
+ SKIPPED: "CANCELLED",
181
+ };
182
+
183
+ const meta = deploy.meta || {};
184
+
185
+ return {
186
+ projectId,
187
+ service: deploy.service?.name || deploy.serviceId || "unknown",
188
+ railwayId: deploy.id,
189
+ status: statusMap[deploy.status] || "SUCCESS",
190
+ commitMessage: meta.commitMessage || null,
191
+ commitSha: meta.commitHash || null,
192
+ branch: meta.branch || null,
193
+ source: "railway",
194
+ deployedAt: new Date(deploy.createdAt),
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Fetch services for a Railway project with their latest deployment state.
200
+ * Railway's API uses Relay-style connections.
201
+ *
202
+ * @param {string} railwayProjectId - Railway project ID
203
+ * @returns {Promise<Array>} Services with their latest deployment info
204
+ */
205
+ export async function fetchProjectServices(railwayProjectId) {
206
+ log.info("Fetching project services", { railwayProjectId });
207
+
208
+ const query = `
209
+ query GetProjectServices($projectId: String!) {
210
+ project(id: $projectId) {
211
+ services {
212
+ edges {
213
+ node {
214
+ id
215
+ name
216
+ deployments(first: 1) {
217
+ edges {
218
+ node {
219
+ id
220
+ status
221
+ createdAt
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ `;
231
+
232
+ try {
233
+ const data = await graphql(query, {
234
+ projectId: railwayProjectId,
235
+ });
236
+ const edges = data?.project?.services?.edges || [];
237
+ return edges.map((e) => {
238
+ const service = e.node;
239
+ const deployEdges = service.deployments?.edges || [];
240
+ const latestDeployment = deployEdges[0]?.node || null;
241
+ return {
242
+ id: service.id,
243
+ name: service.name,
244
+ currentDeployment: latestDeployment,
245
+ };
246
+ });
247
+ } catch (error) {
248
+ log.error("Failed to fetch project services", {
249
+ railwayProjectId,
250
+ error: error.message,
251
+ });
252
+ return [];
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Fetch the most recent deployments for a specific service within a project.
258
+ *
259
+ * @param {string} railwayProjectId - Railway project ID
260
+ * @param {object} options
261
+ * @param {string} options.serviceId - The service ID to filter deployments by
262
+ * @param {number} [options.limit=5] - Maximum number of deployments to return
263
+ * @returns {Promise<Array>} Array of deployment nodes
264
+ */
265
+ export async function fetchLatestDeployments(
266
+ railwayProjectId,
267
+ { serviceId, limit = 5 },
268
+ ) {
269
+ log.info("Fetching latest Railway deployments", {
270
+ railwayProjectId,
271
+ serviceId,
272
+ limit,
273
+ });
274
+
275
+ const query = `
276
+ query GetLatestDeployments($input: DeploymentListInput!) {
277
+ deployments(input: $input) {
278
+ edges {
279
+ node {
280
+ id
281
+ status
282
+ createdAt
283
+ meta
284
+ serviceId
285
+ service {
286
+ id
287
+ name
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ `;
294
+
295
+ try {
296
+ const data = await graphql(query, {
297
+ input: {
298
+ projectId: railwayProjectId,
299
+ serviceId,
300
+ first: limit,
301
+ },
302
+ });
303
+ const edges = data?.deployments?.edges || [];
304
+ return edges.map((e) => e.node);
305
+ } catch (error) {
306
+ log.error("Failed to fetch latest Railway deployments", {
307
+ railwayProjectId,
308
+ serviceId,
309
+ error: error.message,
310
+ });
311
+ return [];
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Railway usage pricing rates (from Railway dashboard).
317
+ * Rates are in USD per unit per minute unless otherwise noted.
318
+ */
319
+ const USAGE_RATES = {
320
+ CPU: 0.000463, // $ per vCPU / minute
321
+ MEMORY: 0.000231, // $ per GB / minute
322
+ EGRESS: 0.05, // $ per GB (total, not per minute)
323
+ VOLUME: 0.00000347, // $ per GB / minute
324
+ BACKUP: 0.00000347, // $ per GB / minute
325
+ };
326
+
327
+ /**
328
+ * Fetch actual resource usage for a Railway project.
329
+ * Returns metered usage data (not estimated) that matches Railway's dashboard.
330
+ * The values are in minutely-accumulated units (vCPU-minutes, GB-minutes).
331
+ *
332
+ * @param {string} railwayProjectId - Railway project ID
333
+ * @param {object} [options]
334
+ * @param {string} [options.startDate] - ISO date string for period start
335
+ * @param {string} [options.endDate] - ISO date string for period end
336
+ * @returns {Promise<{metrics: object, costs: object, totalCost: number}>}
337
+ */
338
+ export async function fetchProjectUsage(railwayProjectId, options = {}) {
339
+ log.info("Fetching project usage", { railwayProjectId, ...options });
340
+
341
+ const measurements = [
342
+ "CPU_USAGE",
343
+ "MEMORY_USAGE_GB",
344
+ "NETWORK_TX_GB",
345
+ "NETWORK_RX_GB",
346
+ "DISK_USAGE_GB",
347
+ "BACKUP_USAGE_GB",
348
+ ];
349
+
350
+ const query = `
351
+ query GetProjectUsage($projectId: String!, $measurements: [MetricMeasurement!]!) {
352
+ usage(projectId: $projectId, measurements: $measurements) {
353
+ measurement
354
+ value
355
+ }
356
+ }
357
+ `;
358
+
359
+ const variables = { projectId: railwayProjectId, measurements };
360
+
361
+ // Add date range if provided
362
+ let finalQuery = query;
363
+ let finalVars = variables;
364
+ if (options.startDate && options.endDate) {
365
+ finalQuery = `
366
+ query GetProjectUsage($projectId: String!, $measurements: [MetricMeasurement!]!, $startDate: DateTime!, $endDate: DateTime!) {
367
+ usage(projectId: $projectId, measurements: $measurements, startDate: $startDate, endDate: $endDate) {
368
+ measurement
369
+ value
370
+ }
371
+ }
372
+ `;
373
+ finalVars = {
374
+ ...variables,
375
+ startDate: options.startDate,
376
+ endDate: options.endDate,
377
+ };
378
+ }
379
+
380
+ try {
381
+ const data = await graphql(finalQuery, finalVars);
382
+ const usage = data?.usage || [];
383
+
384
+ // Build metrics map
385
+ const metrics = {};
386
+ for (const u of usage) {
387
+ metrics[u.measurement] = u.value;
388
+ }
389
+
390
+ // Calculate costs
391
+ const cpuValue = metrics.CPU_USAGE || 0;
392
+ const memValue = metrics.MEMORY_USAGE_GB || 0;
393
+ const netTx = metrics.NETWORK_TX_GB || 0;
394
+ const netRx = metrics.NETWORK_RX_GB || 0;
395
+ const diskValue = metrics.DISK_USAGE_GB || 0;
396
+ const backupValue = metrics.BACKUP_USAGE_GB || 0;
397
+
398
+ const costs = {
399
+ cpu: cpuValue * USAGE_RATES.CPU,
400
+ memory: memValue * USAGE_RATES.MEMORY,
401
+ egress: netTx * USAGE_RATES.EGRESS,
402
+ volume: diskValue * USAGE_RATES.VOLUME,
403
+ backup: backupValue * USAGE_RATES.BACKUP,
404
+ };
405
+
406
+ const totalCost = Object.values(costs).reduce((sum, c) => sum + c, 0);
407
+
408
+ return {
409
+ metrics: {
410
+ cpu: cpuValue,
411
+ memory: memValue,
412
+ networkTx: netTx,
413
+ networkRx: netRx,
414
+ disk: diskValue,
415
+ backup: backupValue,
416
+ },
417
+ costs,
418
+ totalCost,
419
+ };
420
+ } catch (error) {
421
+ log.error("Failed to fetch project usage", {
422
+ railwayProjectId,
423
+ error: error.message,
424
+ });
425
+ return {
426
+ metrics: {
427
+ cpu: 0,
428
+ memory: 0,
429
+ networkTx: 0,
430
+ networkRx: 0,
431
+ disk: 0,
432
+ backup: 0,
433
+ },
434
+ costs: { cpu: 0, memory: 0, egress: 0, volume: 0, backup: 0 },
435
+ totalCost: 0,
436
+ };
437
+ }
438
+ }
@@ -0,0 +1,81 @@
1
+ import { createLogger } from "@techstream/quark-core";
2
+
3
+ const _logger = createLogger("worker:tokens");
4
+
5
+ // Model configurations
6
+ const MODELS = {
7
+ "deepseek/deepseek-v4-flash": { contextWindow: 1_000_000 },
8
+ "deepseek/deepseek-chat": { contextWindow: 64_000 },
9
+ "anthropic/claude-3.5-sonnet": { contextWindow: 200_000 },
10
+ "openai/gpt-4o": { contextWindow: 128_000 },
11
+ };
12
+
13
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
14
+ /**
15
+ * Characters-per-token ratio used for estimation.
16
+ *
17
+ * 4 chars/token is a reasonable heuristic for English prose but has known
18
+ * limitations: code and JSON payloads are denser (overcounted), while
19
+ * non-Latin scripts (CJK, Arabic) are less dense (undercounted).
20
+ *
21
+ * A production-grade tokenizer (tiktoken, @anthropic-ai/tokenizer) would be
22
+ * more accurate but adds weight. The 4:1 heuristic keeps the dependency
23
+ * footprint at zero while being conservative enough for budget enforcement
24
+ * (overestimating is safer than underestimating for truncation decisions).
25
+ */
26
+ const CHARS_PER_TOKEN = 4;
27
+ const OVERHEAD_PER_MESSAGE = 14; // ~4 for role framing + ~10 for tool call structure
28
+
29
+ /**
30
+ * Estimate token count for a string
31
+ * @param {string} text
32
+ * @returns {number}
33
+ */
34
+ export function estimateTokens(text) {
35
+ if (!text) return 0;
36
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
37
+ }
38
+
39
+ /**
40
+ * Estimate total tokens for an array of messages
41
+ * @param {Array<{role: string, content?: string, tool_calls?: Array, tool_results?: Array}>} messages
42
+ * @returns {number}
43
+ */
44
+ export function estimateMessageTokens(messages) {
45
+ if (!messages || messages.length === 0) return 0;
46
+
47
+ let total = 0;
48
+ for (const msg of messages) {
49
+ total += OVERHEAD_PER_MESSAGE;
50
+ if (msg.content) total += estimateTokens(msg.content);
51
+ if (msg.tool_calls) {
52
+ for (const tc of msg.tool_calls) {
53
+ total += estimateTokens(JSON.stringify(tc));
54
+ }
55
+ }
56
+ if (msg.tool_results) {
57
+ for (const tr of msg.tool_results) {
58
+ total += estimateTokens(JSON.stringify(tr));
59
+ }
60
+ }
61
+ }
62
+ return total;
63
+ }
64
+
65
+ /**
66
+ * Get usable token budget for a model
67
+ * @param {string} model
68
+ * @param {object} options
69
+ * @returns {number}
70
+ */
71
+ export function getModelBudget(model, options = {}) {
72
+ const {
73
+ responseReserved = 8_000,
74
+ toolsReserved = 2_500,
75
+ systemReserved = 500,
76
+ } = options;
77
+ const config = MODELS[model] || { contextWindow: DEFAULT_CONTEXT_WINDOW };
78
+ const usable =
79
+ config.contextWindow - responseReserved - toolsReserved - systemReserved;
80
+ return Math.floor(usable * 0.8); // 80% safety margin
81
+ }