@treeseed/sdk 0.5.3 → 0.6.1

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 (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +449 -136
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2116 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. package/package.json +7 -1
@@ -0,0 +1,882 @@
1
+ const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
2
+ const DEFAULT_RAILWAY_WORKSPACE = "knowledge-coop";
3
+ function normalizeRailwayEnvironmentName(value) {
4
+ const normalized = typeof value === "string" ? value.trim() : "";
5
+ if (!normalized) {
6
+ return "";
7
+ }
8
+ return normalized === "prod" ? "production" : normalized;
9
+ }
10
+ function configuredEnvValue(env, name) {
11
+ const value = env?.[name];
12
+ return typeof value === "string" && value.trim() ? value.trim() : "";
13
+ }
14
+ function isUsableRailwayToken(value) {
15
+ return typeof value === "string" && value.trim().length >= 8;
16
+ }
17
+ function resolveRailwayApiToken(env = process.env) {
18
+ const token = configuredEnvValue(env, "RAILWAY_API_TOKEN");
19
+ return isUsableRailwayToken(token) ? token : "";
20
+ }
21
+ function resolveRailwayApiUrl(env = process.env) {
22
+ return configuredEnvValue(env, "TREESEED_RAILWAY_API_URL") || DEFAULT_RAILWAY_API_URL;
23
+ }
24
+ function resolveRailwayWorkspace(env = process.env) {
25
+ return configuredEnvValue(env, "TREESEED_RAILWAY_WORKSPACE") || DEFAULT_RAILWAY_WORKSPACE;
26
+ }
27
+ function normalizeRailwayErrorMessage(payload, fallbackStatus) {
28
+ if (payload && typeof payload === "object" && Array.isArray(payload.errors) && payload.errors.length > 0) {
29
+ const first = payload.errors[0];
30
+ if (first && typeof first === "object" && typeof first.message === "string") {
31
+ return first.message;
32
+ }
33
+ }
34
+ return typeof fallbackStatus === "number" ? `Railway API request failed with ${fallbackStatus}.` : "Railway API request failed.";
35
+ }
36
+ function isRetryableRailwayStatus(status) {
37
+ return status === 408 || status === 429 || status >= 500;
38
+ }
39
+ function parseRetryAfterMs(value) {
40
+ if (!value) {
41
+ return null;
42
+ }
43
+ const seconds = Number(value);
44
+ if (Number.isFinite(seconds) && seconds >= 0) {
45
+ return Math.round(seconds * 1e3);
46
+ }
47
+ const absoluteTime = Date.parse(value);
48
+ if (Number.isFinite(absoluteTime)) {
49
+ return Math.max(0, absoluteTime - Date.now());
50
+ }
51
+ return null;
52
+ }
53
+ function markRailwayTransientError(error, options = {}) {
54
+ const tagged = error;
55
+ tagged.treeseedTransient = true;
56
+ if (typeof options.retryAfterMs === "number" && Number.isFinite(options.retryAfterMs) && options.retryAfterMs >= 0) {
57
+ tagged.treeseedRetryAfterMs = options.retryAfterMs;
58
+ }
59
+ return tagged;
60
+ }
61
+ function isTransientRailwayRequestError(error) {
62
+ if (error && typeof error === "object" && error.treeseedTransient === true) {
63
+ return true;
64
+ }
65
+ const message = error instanceof Error ? error.message : String(error ?? "");
66
+ return /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|rate limit|too many requests|429/iu.test(message);
67
+ }
68
+ function railwayConnectionLabel(value) {
69
+ return typeof value === "string" && value.trim() ? value.trim() : "";
70
+ }
71
+ function normalizeConnectionNodes(connection, mapper) {
72
+ if (!connection || typeof connection !== "object" || !Array.isArray(connection.edges)) {
73
+ return [];
74
+ }
75
+ return connection.edges.map((edge) => {
76
+ if (!edge || typeof edge !== "object") {
77
+ return null;
78
+ }
79
+ const node = edge.node;
80
+ return node && typeof node === "object" ? mapper(node) : null;
81
+ }).filter(Boolean);
82
+ }
83
+ function normalizeWorkspace(node) {
84
+ const id = railwayConnectionLabel(node.id);
85
+ const name = railwayConnectionLabel(node.name);
86
+ if (!id || !name) {
87
+ return null;
88
+ }
89
+ return { id, name };
90
+ }
91
+ function normalizeEnvironment(node) {
92
+ const id = railwayConnectionLabel(node.id);
93
+ const name = railwayConnectionLabel(node.name);
94
+ if (!id || !name) {
95
+ return null;
96
+ }
97
+ return { id, name };
98
+ }
99
+ function normalizeService(node) {
100
+ const id = railwayConnectionLabel(node.id);
101
+ const name = railwayConnectionLabel(node.name);
102
+ if (!id || !name) {
103
+ return null;
104
+ }
105
+ return { id, name };
106
+ }
107
+ function normalizeProject(node) {
108
+ const id = railwayConnectionLabel(node.id);
109
+ const name = railwayConnectionLabel(node.name);
110
+ if (!id || !name) {
111
+ return null;
112
+ }
113
+ return {
114
+ id,
115
+ name,
116
+ workspaceId: railwayConnectionLabel(node.workspaceId) || null,
117
+ environments: normalizeConnectionNodes(node.environments, normalizeEnvironment),
118
+ services: normalizeConnectionNodes(node.services, normalizeService)
119
+ };
120
+ }
121
+ function normalizeVariableMap(value) {
122
+ if (!value) {
123
+ return {};
124
+ }
125
+ if (typeof value === "string") {
126
+ try {
127
+ return normalizeVariableMap(JSON.parse(value));
128
+ } catch {
129
+ return {};
130
+ }
131
+ }
132
+ if (typeof value !== "object" || Array.isArray(value)) {
133
+ return {};
134
+ }
135
+ return Object.fromEntries(
136
+ Object.entries(value).map(([key, entryValue]) => {
137
+ if (typeof entryValue === "string") {
138
+ return [key, entryValue];
139
+ }
140
+ if (entryValue && typeof entryValue === "object" && typeof entryValue.value === "string") {
141
+ return [key, entryValue.value];
142
+ }
143
+ return [key, null];
144
+ })
145
+ );
146
+ }
147
+ function normalizeRailwayNumber(value) {
148
+ if (typeof value === "number" && Number.isFinite(value)) {
149
+ return value;
150
+ }
151
+ if (typeof value === "string" && value.trim()) {
152
+ const parsed = Number(value);
153
+ return Number.isFinite(parsed) ? parsed : null;
154
+ }
155
+ return null;
156
+ }
157
+ function normalizeRailwayCustomDomainDnsRecord(node) {
158
+ const fqdn = railwayConnectionLabel(node.fqdn);
159
+ if (!fqdn) {
160
+ return null;
161
+ }
162
+ return {
163
+ fqdn,
164
+ hostlabel: railwayConnectionLabel(node.hostlabel),
165
+ recordType: railwayConnectionLabel(node.recordType),
166
+ requiredValue: railwayConnectionLabel(node.requiredValue),
167
+ currentValue: railwayConnectionLabel(node.currentValue),
168
+ status: railwayConnectionLabel(node.status),
169
+ zone: railwayConnectionLabel(node.zone),
170
+ purpose: railwayConnectionLabel(node.purpose)
171
+ };
172
+ }
173
+ function normalizeRailwayCustomDomain(node) {
174
+ const id = railwayConnectionLabel(node.id);
175
+ const domain = railwayConnectionLabel(node.domain);
176
+ if (!id || !domain) {
177
+ return null;
178
+ }
179
+ const status = node.status && typeof node.status === "object" ? node.status : {};
180
+ const dnsRecords = Array.isArray(status.dnsRecords) ? status.dnsRecords.map((entry) => entry && typeof entry === "object" ? normalizeRailwayCustomDomainDnsRecord(entry) : null).filter(Boolean) : [];
181
+ return {
182
+ id,
183
+ domain,
184
+ environmentId: railwayConnectionLabel(node.environmentId),
185
+ serviceId: railwayConnectionLabel(node.serviceId),
186
+ targetPort: typeof node.targetPort === "number" && Number.isFinite(node.targetPort) ? node.targetPort : null,
187
+ verified: status.verified === true,
188
+ certificateStatus: railwayConnectionLabel(status.certificateStatus) || null,
189
+ verificationDnsHost: railwayConnectionLabel(status.verificationDnsHost) || null,
190
+ verificationToken: railwayConnectionLabel(status.verificationToken) || null,
191
+ dnsRecords
192
+ };
193
+ }
194
+ async function railwayGraphqlRequest({
195
+ query,
196
+ variables,
197
+ env = process.env,
198
+ apiToken,
199
+ apiUrl,
200
+ fetchImpl = fetch,
201
+ timeoutMs = 15e3,
202
+ retries = 5
203
+ }) {
204
+ const token = apiToken || resolveRailwayApiToken(env);
205
+ if (!token) {
206
+ throw new Error("Configure RAILWAY_API_TOKEN before invoking Railway APIs.");
207
+ }
208
+ let attempt = 0;
209
+ for (; ; ) {
210
+ const controller = new AbortController();
211
+ let timer = null;
212
+ try {
213
+ const response = await Promise.race([
214
+ fetchImpl(apiUrl || resolveRailwayApiUrl(env), {
215
+ method: "POST",
216
+ headers: {
217
+ authorization: `Bearer ${token}`,
218
+ "content-type": "application/json"
219
+ },
220
+ body: JSON.stringify({ query, variables }),
221
+ signal: controller.signal
222
+ }),
223
+ new Promise((_, reject) => {
224
+ timer = setTimeout(() => {
225
+ controller.abort();
226
+ reject(markRailwayTransientError(new Error(`Railway API request timed out after ${timeoutMs}ms.`)));
227
+ }, timeoutMs);
228
+ })
229
+ ]);
230
+ const payload = await response.json().catch(() => ({}));
231
+ if (!response.ok || Array.isArray(payload.errors) && payload.errors.length > 0) {
232
+ const message = normalizeRailwayErrorMessage(payload, response.status);
233
+ const hasGraphqlErrors = Array.isArray(payload.errors) && payload.errors.length > 0;
234
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
235
+ const shouldRetry = isRetryableRailwayStatus(response.status) || /rate limit|too many requests/iu.test(message);
236
+ const error = new Error(message);
237
+ if (shouldRetry || hasGraphqlErrors && /rate limit|too many requests/iu.test(message)) {
238
+ throw markRailwayTransientError(error, { retryAfterMs });
239
+ }
240
+ throw error;
241
+ }
242
+ return payload;
243
+ } catch (error) {
244
+ if (attempt >= retries || !isTransientRailwayRequestError(error)) {
245
+ throw error;
246
+ }
247
+ attempt += 1;
248
+ const retryAfterMs = error && typeof error === "object" && typeof error.treeseedRetryAfterMs === "number" ? Math.max(0, Number(error.treeseedRetryAfterMs)) : null;
249
+ const backoffMs = retryAfterMs ?? Math.min(500 * 2 ** (attempt - 1), 4e3);
250
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
251
+ } finally {
252
+ if (timer) {
253
+ clearTimeout(timer);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ async function getRailwayAuthProfile({
259
+ env = process.env,
260
+ fetchImpl = fetch
261
+ }) {
262
+ const payload = await railwayGraphqlRequest({
263
+ query: `
264
+ query TreeseedRailwayAuthProfile {
265
+ me {
266
+ id
267
+ name
268
+ email
269
+ workspaces {
270
+ id
271
+ name
272
+ }
273
+ }
274
+ }
275
+ `.trim(),
276
+ env,
277
+ fetchImpl
278
+ });
279
+ const me = payload.data?.me;
280
+ return {
281
+ id: railwayConnectionLabel(me?.id) || null,
282
+ name: railwayConnectionLabel(me?.name) || null,
283
+ email: railwayConnectionLabel(me?.email) || null,
284
+ workspaces: Array.isArray(me?.workspaces) ? me.workspaces.map((workspace) => workspace && typeof workspace === "object" ? normalizeWorkspace(workspace) : null).filter(Boolean) : []
285
+ };
286
+ }
287
+ async function resolveRailwayWorkspaceContext({
288
+ env = process.env,
289
+ workspace,
290
+ fetchImpl = fetch
291
+ }) {
292
+ const desired = (workspace || resolveRailwayWorkspace(env)).trim();
293
+ const profile = await getRailwayAuthProfile({ env, fetchImpl });
294
+ const match = profile.workspaces.find((candidate) => candidate.id === desired || candidate.name === desired) ?? null;
295
+ if (!match) {
296
+ const available = profile.workspaces.map((candidate) => candidate.name).join(", ") || "(none)";
297
+ throw new Error(`Railway workspace ${desired} is not visible to the current token. Available workspaces: ${available}.`);
298
+ }
299
+ return match;
300
+ }
301
+ async function listRailwayProjects({
302
+ env = process.env,
303
+ workspaceId,
304
+ fetchImpl = fetch
305
+ }) {
306
+ const payload = await railwayGraphqlRequest({
307
+ query: `
308
+ query TreeseedRailwayProjects($workspaceId: String!, $first: Int!) {
309
+ projects(workspaceId: $workspaceId, first: $first) {
310
+ edges {
311
+ node {
312
+ id
313
+ name
314
+ workspaceId
315
+ environments(first: 50) {
316
+ edges {
317
+ node {
318
+ id
319
+ name
320
+ }
321
+ }
322
+ }
323
+ services(first: 50) {
324
+ edges {
325
+ node {
326
+ id
327
+ name
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ `.trim(),
336
+ variables: { workspaceId, first: 100 },
337
+ env,
338
+ fetchImpl
339
+ });
340
+ return normalizeConnectionNodes(payload.data?.projects, normalizeProject);
341
+ }
342
+ async function getRailwayProject({
343
+ projectId,
344
+ env = process.env,
345
+ fetchImpl = fetch
346
+ }) {
347
+ const payload = await railwayGraphqlRequest({
348
+ query: `
349
+ query TreeseedRailwayProject($projectId: String!) {
350
+ project(id: $projectId) {
351
+ id
352
+ name
353
+ workspaceId
354
+ environments(first: 50) {
355
+ edges {
356
+ node {
357
+ id
358
+ name
359
+ }
360
+ }
361
+ }
362
+ services(first: 50) {
363
+ edges {
364
+ node {
365
+ id
366
+ name
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ `.trim(),
373
+ variables: { projectId },
374
+ env,
375
+ fetchImpl
376
+ });
377
+ return payload.data?.project ? normalizeProject(payload.data.project) : null;
378
+ }
379
+ async function ensureRailwayProject({
380
+ projectName,
381
+ projectId,
382
+ defaultEnvironmentName = "staging",
383
+ env = process.env,
384
+ workspace,
385
+ fetchImpl = fetch
386
+ }) {
387
+ const workspaceContext = await resolveRailwayWorkspaceContext({ env, workspace, fetchImpl });
388
+ const projects = await listRailwayProjects({ env, workspaceId: workspaceContext.id, fetchImpl });
389
+ const desiredProjectName = railwayConnectionLabel(projectName);
390
+ const desiredProjectId = railwayConnectionLabel(projectId);
391
+ const existing = projects.find(
392
+ (project2) => desiredProjectId && project2.id === desiredProjectId || desiredProjectName && project2.name === desiredProjectName
393
+ ) ?? null;
394
+ if (existing) {
395
+ return { workspace: workspaceContext, project: existing, created: false };
396
+ }
397
+ if (!desiredProjectName) {
398
+ throw new Error("Railway project creation requires a project name.");
399
+ }
400
+ const created = await railwayGraphqlRequest({
401
+ query: `
402
+ mutation TreeseedRailwayProjectCreate($input: ProjectCreateInput!) {
403
+ projectCreate(input: $input) {
404
+ id
405
+ name
406
+ workspaceId
407
+ environments(first: 50) {
408
+ edges {
409
+ node {
410
+ id
411
+ name
412
+ }
413
+ }
414
+ }
415
+ services(first: 50) {
416
+ edges {
417
+ node {
418
+ id
419
+ name
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
425
+ `.trim(),
426
+ variables: {
427
+ input: {
428
+ name: desiredProjectName,
429
+ workspaceId: workspaceContext.id,
430
+ defaultEnvironmentName
431
+ }
432
+ },
433
+ env,
434
+ fetchImpl
435
+ });
436
+ const project = created.data?.projectCreate ? normalizeProject(created.data.projectCreate) : null;
437
+ if (!project) {
438
+ throw new Error(`Railway project create did not return a usable project for ${desiredProjectName}.`);
439
+ }
440
+ return { workspace: workspaceContext, project, created: true };
441
+ }
442
+ async function ensureRailwayEnvironment({
443
+ projectId,
444
+ environmentName,
445
+ env = process.env,
446
+ fetchImpl = fetch
447
+ }) {
448
+ const environments = await listRailwayEnvironments({ projectId, env, fetchImpl });
449
+ const existing = environments.find((environment2) => environment2.name === environmentName || environment2.id === environmentName) ?? null;
450
+ if (existing) {
451
+ return { environment: existing, created: false };
452
+ }
453
+ const created = await railwayGraphqlRequest({
454
+ query: `
455
+ mutation TreeseedRailwayEnvironmentCreate($input: EnvironmentCreateInput!) {
456
+ environmentCreate(input: $input) {
457
+ id
458
+ name
459
+ }
460
+ }
461
+ `.trim(),
462
+ variables: {
463
+ input: {
464
+ projectId,
465
+ name: environmentName,
466
+ skipInitialDeploys: true
467
+ }
468
+ },
469
+ env,
470
+ fetchImpl
471
+ });
472
+ const environment = created.data?.environmentCreate ? normalizeEnvironment(created.data.environmentCreate) : null;
473
+ if (!environment) {
474
+ throw new Error(`Railway environment create did not return a usable environment for ${environmentName}.`);
475
+ }
476
+ return { environment, created: true };
477
+ }
478
+ async function listRailwayEnvironments({
479
+ projectId,
480
+ env = process.env,
481
+ fetchImpl = fetch
482
+ }) {
483
+ const payload = await railwayGraphqlRequest({
484
+ query: `
485
+ query TreeseedRailwayProjectEnvironments($projectId: String!) {
486
+ project(id: $projectId) {
487
+ id
488
+ environments(first: 50) {
489
+ edges {
490
+ node {
491
+ id
492
+ name
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ `.trim(),
499
+ variables: { projectId },
500
+ env,
501
+ fetchImpl
502
+ });
503
+ return normalizeConnectionNodes(payload.data?.project ? payload.data.project.environments : null, normalizeEnvironment);
504
+ }
505
+ async function ensureRailwayService({
506
+ projectId,
507
+ serviceName,
508
+ serviceId,
509
+ env = process.env,
510
+ fetchImpl = fetch
511
+ }) {
512
+ const services = await listRailwayServices({ projectId, env, fetchImpl });
513
+ const desiredServiceName = railwayConnectionLabel(serviceName);
514
+ const desiredServiceId = railwayConnectionLabel(serviceId);
515
+ const existing = services.find(
516
+ (service2) => desiredServiceId && service2.id === desiredServiceId || desiredServiceName && service2.name === desiredServiceName
517
+ ) ?? null;
518
+ if (existing) {
519
+ return { service: existing, created: false };
520
+ }
521
+ if (!desiredServiceName) {
522
+ throw new Error("Railway service creation requires a service name.");
523
+ }
524
+ const created = await railwayGraphqlRequest({
525
+ query: `
526
+ mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
527
+ serviceCreate(input: $input) {
528
+ id
529
+ name
530
+ }
531
+ }
532
+ `.trim(),
533
+ variables: {
534
+ input: {
535
+ projectId,
536
+ name: desiredServiceName
537
+ }
538
+ },
539
+ env,
540
+ fetchImpl
541
+ });
542
+ const service = created.data?.serviceCreate ? normalizeService(created.data.serviceCreate) : null;
543
+ if (!service) {
544
+ throw new Error(`Railway service create did not return a usable service for ${desiredServiceName}.`);
545
+ }
546
+ return { service, created: true };
547
+ }
548
+ async function listRailwayServices({
549
+ projectId,
550
+ env = process.env,
551
+ fetchImpl = fetch
552
+ }) {
553
+ const payload = await railwayGraphqlRequest({
554
+ query: `
555
+ query TreeseedRailwayProjectServices($projectId: String!) {
556
+ project(id: $projectId) {
557
+ id
558
+ services(first: 50) {
559
+ edges {
560
+ node {
561
+ id
562
+ name
563
+ }
564
+ }
565
+ }
566
+ }
567
+ }
568
+ `.trim(),
569
+ variables: { projectId },
570
+ env,
571
+ fetchImpl
572
+ });
573
+ return normalizeConnectionNodes(payload.data?.project ? payload.data.project.services : null, normalizeService);
574
+ }
575
+ async function getRailwayServiceInstance({
576
+ serviceId,
577
+ environmentId,
578
+ env = process.env,
579
+ fetchImpl = fetch
580
+ }) {
581
+ const legacySummary = {
582
+ id: null,
583
+ buildCommand: null,
584
+ startCommand: null,
585
+ rootDirectory: null,
586
+ healthcheckPath: null,
587
+ healthcheckTimeoutSeconds: null,
588
+ healthcheckIntervalSeconds: null,
589
+ restartPolicy: null,
590
+ runtimeMode: null,
591
+ sleepApplication: null,
592
+ runtimeConfigSupported: false
593
+ };
594
+ try {
595
+ const payload = await railwayGraphqlRequest({
596
+ query: `
597
+ query TreeseedRailwayServiceInstance($serviceId: String!, $environmentId: String!) {
598
+ serviceInstance(serviceId: $serviceId, environmentId: $environmentId) {
599
+ id
600
+ buildCommand
601
+ startCommand
602
+ rootDirectory
603
+ healthcheckPath
604
+ healthcheckTimeout
605
+ sleepApplication
606
+ }
607
+ }
608
+ `.trim(),
609
+ variables: { serviceId, environmentId },
610
+ env,
611
+ fetchImpl
612
+ });
613
+ const instance = payload.data?.serviceInstance;
614
+ return {
615
+ id: railwayConnectionLabel(instance?.id) || null,
616
+ buildCommand: railwayConnectionLabel(instance?.buildCommand) || null,
617
+ startCommand: railwayConnectionLabel(instance?.startCommand) || null,
618
+ rootDirectory: railwayConnectionLabel(instance?.rootDirectory) || null,
619
+ healthcheckPath: railwayConnectionLabel(instance?.healthcheckPath) || null,
620
+ healthcheckTimeoutSeconds: normalizeRailwayNumber(instance?.healthcheckTimeout),
621
+ healthcheckIntervalSeconds: null,
622
+ restartPolicy: null,
623
+ runtimeMode: instance?.sleepApplication === true ? "serverless" : "replicated",
624
+ sleepApplication: typeof instance?.sleepApplication === "boolean" ? instance.sleepApplication : null,
625
+ runtimeConfigSupported: true
626
+ };
627
+ } catch (error) {
628
+ const message = error instanceof Error ? error.message : String(error ?? "");
629
+ if (/Cannot query field .*healthcheckPath|Cannot query field .*healthcheckTimeout|Cannot query field .*sleepApplication/iu.test(message)) {
630
+ const payload = await railwayGraphqlRequest({
631
+ query: `
632
+ query TreeseedRailwayServiceInstanceLegacy($serviceId: String!, $environmentId: String!) {
633
+ serviceInstance(serviceId: $serviceId, environmentId: $environmentId) {
634
+ id
635
+ buildCommand
636
+ startCommand
637
+ rootDirectory
638
+ }
639
+ }
640
+ `.trim(),
641
+ variables: { serviceId, environmentId },
642
+ env,
643
+ fetchImpl
644
+ });
645
+ const instance = payload.data?.serviceInstance;
646
+ return {
647
+ ...legacySummary,
648
+ id: railwayConnectionLabel(instance?.id) || null,
649
+ buildCommand: railwayConnectionLabel(instance?.buildCommand) || null,
650
+ startCommand: railwayConnectionLabel(instance?.startCommand) || null,
651
+ rootDirectory: railwayConnectionLabel(instance?.rootDirectory) || null
652
+ };
653
+ }
654
+ if (!/ServiceInstance not found/iu.test(message)) {
655
+ throw error;
656
+ }
657
+ return legacySummary;
658
+ }
659
+ }
660
+ async function ensureRailwayServiceInstanceConfiguration({
661
+ serviceId,
662
+ environmentId,
663
+ buildCommand,
664
+ startCommand,
665
+ rootDirectory,
666
+ healthcheckPath,
667
+ healthcheckTimeoutSeconds,
668
+ healthcheckIntervalSeconds,
669
+ restartPolicy,
670
+ runtimeMode,
671
+ env = process.env,
672
+ fetchImpl = fetch
673
+ }) {
674
+ const current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
675
+ if (!current.id) {
676
+ return { instance: current, updated: false };
677
+ }
678
+ const desired = {
679
+ buildCommand: railwayConnectionLabel(buildCommand) || null,
680
+ startCommand: railwayConnectionLabel(startCommand) || null,
681
+ rootDirectory: railwayConnectionLabel(rootDirectory) || null,
682
+ healthcheckPath: railwayConnectionLabel(healthcheckPath) || null,
683
+ healthcheckTimeoutSeconds: normalizeRailwayNumber(healthcheckTimeoutSeconds),
684
+ healthcheckIntervalSeconds: normalizeRailwayNumber(healthcheckIntervalSeconds),
685
+ restartPolicy: railwayConnectionLabel(restartPolicy) || null,
686
+ runtimeMode: railwayConnectionLabel(runtimeMode) || null,
687
+ sleepApplication: railwayConnectionLabel(runtimeMode) === "serverless" ? true : railwayConnectionLabel(runtimeMode) === "replicated" ? false : null
688
+ };
689
+ const needsRuntimeConfig = desired.healthcheckPath !== null || desired.healthcheckTimeoutSeconds !== null || desired.runtimeMode !== null;
690
+ if (needsRuntimeConfig && current.runtimeConfigSupported !== true) {
691
+ throw new Error("Railway service instance runtime settings are unsupported by the current Railway API schema.");
692
+ }
693
+ if (desired.healthcheckIntervalSeconds !== null) {
694
+ throw new Error("Railway service instance healthcheck intervals are unsupported by the current Railway API schema.");
695
+ }
696
+ if (desired.restartPolicy !== null) {
697
+ throw new Error("Railway service instance restart policies are unsupported by the current Railway API schema.");
698
+ }
699
+ const drifted = desired.buildCommand !== null && desired.buildCommand !== current.buildCommand || desired.startCommand !== null && desired.startCommand !== current.startCommand || desired.rootDirectory !== null && desired.rootDirectory !== current.rootDirectory || desired.healthcheckPath !== null && desired.healthcheckPath !== current.healthcheckPath || desired.healthcheckTimeoutSeconds !== null && desired.healthcheckTimeoutSeconds !== current.healthcheckTimeoutSeconds || desired.runtimeMode !== null && desired.runtimeMode !== current.runtimeMode;
700
+ if (!drifted) {
701
+ return { instance: current, updated: false };
702
+ }
703
+ const mutationQuery = needsRuntimeConfig ? `
704
+ mutation TreeseedRailwayServiceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
705
+ serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
706
+ }
707
+ `.trim() : `
708
+ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
709
+ serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
710
+ }
711
+ `.trim();
712
+ try {
713
+ await railwayGraphqlRequest({
714
+ query: mutationQuery,
715
+ variables: {
716
+ serviceId,
717
+ environmentId,
718
+ input: {
719
+ ...desired.buildCommand !== null ? { buildCommand: desired.buildCommand } : {},
720
+ ...desired.startCommand !== null ? { startCommand: desired.startCommand } : {},
721
+ ...desired.rootDirectory !== null ? { rootDirectory: desired.rootDirectory } : {},
722
+ ...desired.healthcheckPath !== null ? { healthcheckPath: desired.healthcheckPath } : {},
723
+ ...desired.healthcheckTimeoutSeconds !== null ? { healthcheckTimeout: desired.healthcheckTimeoutSeconds } : {},
724
+ ...desired.sleepApplication !== null ? { sleepApplication: desired.sleepApplication } : {}
725
+ }
726
+ },
727
+ env,
728
+ fetchImpl
729
+ });
730
+ } catch (error) {
731
+ const message = error instanceof Error ? error.message : String(error ?? "");
732
+ if (needsRuntimeConfig && /Field .* is not defined by type .*ServiceInstanceUpdateInput|Unknown argument|Cannot query field/iu.test(message)) {
733
+ throw new Error("Railway service instance runtime settings are unsupported by the current Railway API schema.");
734
+ }
735
+ throw error;
736
+ }
737
+ const instance = await getRailwayServiceInstance({
738
+ serviceId,
739
+ environmentId,
740
+ env,
741
+ fetchImpl
742
+ });
743
+ return {
744
+ instance: {
745
+ id: instance.id || current.id,
746
+ buildCommand: instance.buildCommand ?? desired.buildCommand,
747
+ startCommand: instance.startCommand ?? desired.startCommand,
748
+ rootDirectory: instance.rootDirectory ?? desired.rootDirectory,
749
+ healthcheckPath: instance.healthcheckPath ?? desired.healthcheckPath,
750
+ healthcheckTimeoutSeconds: instance.healthcheckTimeoutSeconds ?? desired.healthcheckTimeoutSeconds,
751
+ healthcheckIntervalSeconds: instance.healthcheckIntervalSeconds ?? desired.healthcheckIntervalSeconds,
752
+ restartPolicy: instance.restartPolicy ?? desired.restartPolicy,
753
+ runtimeMode: instance.runtimeMode ?? desired.runtimeMode,
754
+ sleepApplication: instance.sleepApplication ?? desired.sleepApplication,
755
+ runtimeConfigSupported: instance.runtimeConfigSupported
756
+ },
757
+ updated: true
758
+ };
759
+ }
760
+ async function listRailwayVariables({
761
+ projectId,
762
+ environmentId,
763
+ serviceId,
764
+ env = process.env,
765
+ fetchImpl = fetch
766
+ }) {
767
+ const payload = await railwayGraphqlRequest({
768
+ query: `
769
+ query TreeseedRailwayVariables($projectId: String!, $environmentId: String!, $serviceId: String) {
770
+ variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId, unrendered: true)
771
+ }
772
+ `.trim(),
773
+ variables: {
774
+ projectId,
775
+ environmentId,
776
+ serviceId: serviceId || null
777
+ },
778
+ env,
779
+ fetchImpl
780
+ });
781
+ return normalizeVariableMap(payload.data?.variables);
782
+ }
783
+ async function upsertRailwayVariables({
784
+ projectId,
785
+ environmentId,
786
+ serviceId,
787
+ variables,
788
+ env = process.env,
789
+ fetchImpl = fetch
790
+ }) {
791
+ if (Object.keys(variables).length === 0) {
792
+ return;
793
+ }
794
+ await railwayGraphqlRequest({
795
+ query: `
796
+ mutation TreeseedRailwayVariableCollectionUpsert($input: VariableCollectionUpsertInput!) {
797
+ variableCollectionUpsert(input: $input)
798
+ }
799
+ `.trim(),
800
+ variables: {
801
+ input: {
802
+ projectId,
803
+ environmentId,
804
+ serviceId: serviceId || null,
805
+ variables,
806
+ replace: false,
807
+ skipDeploys: true
808
+ }
809
+ },
810
+ env,
811
+ fetchImpl
812
+ });
813
+ }
814
+ async function listRailwayCustomDomains({
815
+ projectId,
816
+ environmentId,
817
+ serviceId,
818
+ env = process.env,
819
+ fetchImpl = fetch
820
+ }) {
821
+ const payload = await railwayGraphqlRequest({
822
+ query: `
823
+ query TreeseedRailwayCustomDomains($projectId: String!, $environmentId: String!, $serviceId: String!) {
824
+ domains(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) {
825
+ customDomains {
826
+ id
827
+ domain
828
+ environmentId
829
+ serviceId
830
+ targetPort
831
+ status {
832
+ verified
833
+ certificateStatus
834
+ verificationDnsHost
835
+ verificationToken
836
+ dnsRecords {
837
+ fqdn
838
+ hostlabel
839
+ recordType
840
+ requiredValue
841
+ currentValue
842
+ status
843
+ zone
844
+ purpose
845
+ }
846
+ }
847
+ }
848
+ }
849
+ }
850
+ `.trim(),
851
+ variables: {
852
+ projectId,
853
+ environmentId,
854
+ serviceId
855
+ },
856
+ env,
857
+ fetchImpl
858
+ });
859
+ return Array.isArray(payload.data?.domains?.customDomains) ? payload.data.domains.customDomains.map((entry) => entry && typeof entry === "object" ? normalizeRailwayCustomDomain(entry) : null).filter(Boolean) : [];
860
+ }
861
+ export {
862
+ ensureRailwayEnvironment,
863
+ ensureRailwayProject,
864
+ ensureRailwayService,
865
+ ensureRailwayServiceInstanceConfiguration,
866
+ getRailwayAuthProfile,
867
+ getRailwayProject,
868
+ getRailwayServiceInstance,
869
+ isUsableRailwayToken,
870
+ listRailwayCustomDomains,
871
+ listRailwayEnvironments,
872
+ listRailwayProjects,
873
+ listRailwayServices,
874
+ listRailwayVariables,
875
+ normalizeRailwayEnvironmentName,
876
+ railwayGraphqlRequest,
877
+ resolveRailwayApiToken,
878
+ resolveRailwayApiUrl,
879
+ resolveRailwayWorkspace,
880
+ resolveRailwayWorkspaceContext,
881
+ upsertRailwayVariables
882
+ };