@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,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
|
+
}
|