@treeseed/sdk 0.6.38 → 0.6.40

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 (46) hide show
  1. package/dist/capacity.d.ts +53 -0
  2. package/dist/capacity.js +100 -0
  3. package/dist/control-plane-client.d.ts +41 -1
  4. package/dist/control-plane-client.js +154 -0
  5. package/dist/control-plane.d.ts +6 -1
  6. package/dist/control-plane.js +39 -2
  7. package/dist/d1-store.d.ts +63 -1
  8. package/dist/d1-store.js +190 -1
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +12 -0
  11. package/dist/operations/services/config-runtime.js +5 -3
  12. package/dist/operations/services/deploy.js +3 -2
  13. package/dist/operations/services/github-api.d.ts +2 -1
  14. package/dist/operations/services/github-api.js +35 -26
  15. package/dist/operations/services/knowledge-coop-launch.js +5 -28
  16. package/dist/operations/services/package-reference-policy.d.ts +68 -0
  17. package/dist/operations/services/package-reference-policy.js +135 -0
  18. package/dist/operations/services/project-platform.d.ts +14 -0
  19. package/dist/operations/services/project-platform.js +3 -2
  20. package/dist/operations/services/railway-api.d.ts +33 -0
  21. package/dist/operations/services/railway-api.js +273 -0
  22. package/dist/operations/services/railway-deploy.d.ts +22 -0
  23. package/dist/operations/services/railway-deploy.js +81 -19
  24. package/dist/operations/services/release-candidate.d.ts +2 -0
  25. package/dist/operations/services/release-candidate.js +28 -0
  26. package/dist/operations/services/runtime-tools.js +1 -1
  27. package/dist/operations-registry.js +1 -0
  28. package/dist/reconcile/bootstrap-systems.js +1 -1
  29. package/dist/reconcile/builtin-adapters.js +5 -9
  30. package/dist/reconcile/contracts.d.ts +1 -1
  31. package/dist/reconcile/desired-state.js +9 -17
  32. package/dist/reconcile/state.js +4 -4
  33. package/dist/reconcile/units.js +4 -8
  34. package/dist/sdk-types.d.ts +566 -3
  35. package/dist/sdk.d.ts +12 -1
  36. package/dist/sdk.js +44 -0
  37. package/dist/stores/operational-store.d.ts +12 -1
  38. package/dist/stores/operational-store.js +283 -5
  39. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +5 -24
  40. package/dist/types/agents.d.ts +27 -0
  41. package/dist/workflow/operations.d.ts +94 -2
  42. package/dist/workflow/operations.js +90 -32
  43. package/dist/workflow-state.js +3 -5
  44. package/dist/workflow.d.ts +8 -1
  45. package/dist/workflow.js +6 -0
  46. package/package.json +5 -1
@@ -20,6 +20,19 @@ function isPrereleaseVersion(version) {
20
20
  function isStableVersion(version) {
21
21
  return /^\d+\.\d+\.\d+$/u.test(String(version).trim());
22
22
  }
23
+ function stableVersionBase(version) {
24
+ const match = String(version).trim().match(/^(\d+\.\d+\.\d+)(?:-|$)/u);
25
+ return match?.[1] ?? null;
26
+ }
27
+ function compareStableVersions(left, right) {
28
+ const leftParts = left.split(".").map((part) => Number(part));
29
+ const rightParts = right.split(".").map((part) => Number(part));
30
+ for (let index = 0; index < 3; index += 1) {
31
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
32
+ if (difference !== 0) return difference;
33
+ }
34
+ return 0;
35
+ }
23
36
  function isGitDependencySpec(spec) {
24
37
  return /^(?:git\+|github:|gitlab:|bitbucket:|ssh:\/\/|https:\/\/|file:)/u.test(String(spec).trim()) && String(spec).includes("#");
25
38
  }
@@ -279,9 +292,68 @@ function gitTagMessage(repoDir, tagName) {
279
292
  return "";
280
293
  }
281
294
  }
295
+ function gitRemoteTagMessage(repoDir, tagName) {
296
+ try {
297
+ run("git", ["fetch", "--no-tags", "origin", `refs/tags/${tagName}`], { cwd: repoDir, capture: true });
298
+ return run("git", ["cat-file", "-p", "FETCH_HEAD"], { cwd: repoDir, capture: true });
299
+ } catch {
300
+ return "";
301
+ }
302
+ }
303
+ function gitDevTagMessage(repoDir, tagName) {
304
+ const local = gitTagMessage(repoDir, tagName);
305
+ if (local.trim()) return local;
306
+ return gitRemoteTagMessage(repoDir, tagName);
307
+ }
282
308
  function tagHasTreeseedDevMetadata(repoDir, tagName) {
283
309
  return gitTagMessage(repoDir, tagName).includes("treeseed-dev-tag: true");
284
310
  }
311
+ function parseTreeseedDevTag(tagName, message) {
312
+ const match = String(tagName).match(/^(\d+\.\d+\.\d+)-dev\.([0-9A-Za-z.-]+)\.(\d{8}T\d{6}Z)$/u);
313
+ if (!match) return null;
314
+ const metadata = Object.fromEntries(
315
+ String(message).split(/\r?\n/u).map((line) => line.match(/^([^:]+):\s*(.*)$/u)).filter((lineMatch) => Boolean(lineMatch)).map((lineMatch) => [String(lineMatch[1]).trim(), String(lineMatch[2] ?? "").trim()])
316
+ );
317
+ if (metadata["treeseed-dev-tag"] !== "true") return null;
318
+ return {
319
+ tagName,
320
+ stableVersion: match[1],
321
+ branchSlug: match[2],
322
+ timestamp: match[3],
323
+ metadata,
324
+ branch: metadata.branch || null,
325
+ packageName: metadata.package || null,
326
+ version: metadata.version || null
327
+ };
328
+ }
329
+ function devTagScope(info) {
330
+ const branch = info.branch ?? info.branchSlug;
331
+ if (branch === "staging" || info.branchSlug === "staging") return "staging";
332
+ if (branch === "main" || branch === "prod" || branch === "production" || info.branchSlug === "main" || info.branchSlug === "prod" || info.branchSlug === "production") return "main";
333
+ return "preview";
334
+ }
335
+ function classifyStaleTreeseedDevTag(input) {
336
+ const tagName = String(input.tagName).trim();
337
+ if (!tagName.includes("-dev.")) return { tagName, action: "skip", reason: "not-dev-tag", info: null };
338
+ if (tagName.startsWith("deprecated/")) return { tagName, action: "skip", reason: "deprecated-tag", info: null };
339
+ const info = parseTreeseedDevTag(tagName, input.message);
340
+ if (!info) return { tagName, action: "skip", reason: input.message.includes("treeseed-dev-tag: true") ? "malformed-dev-tag" : "missing-treeseed-metadata", info: null };
341
+ if (input.expectedPackageName && info.packageName && info.packageName !== input.expectedPackageName) {
342
+ return { tagName, action: "skip", reason: "package-mismatch", info };
343
+ }
344
+ if ((input.activeReferences ?? []).includes(tagName)) return { tagName, action: "skip", reason: "still-referenced", info };
345
+ const scope = devTagScope(info);
346
+ if (scope === "main") return { tagName, action: "skip", reason: "main-or-production-tag", info };
347
+ const branchScope = input.branchScope ?? "all";
348
+ if (branchScope !== "all" && branchScope !== scope) return { tagName, action: "skip", reason: `scope-${scope}`, info };
349
+ const currentStableVersion = stableVersionBase(input.currentVersion);
350
+ if (!currentStableVersion) return { tagName, action: "skip", reason: "invalid-current-version", info };
351
+ const comparison = compareStableVersions(info.stableVersion, currentStableVersion);
352
+ if (comparison >= 0) {
353
+ return { tagName, action: "skip", reason: comparison === 0 ? "current-version" : "newer-version", info };
354
+ }
355
+ return { tagName, action: "delete", reason: "stale-before-current-version", info };
356
+ }
285
357
  function cleanupDevTags(repoDir, tagNames, activeReferences = []) {
286
358
  const active = new Set(activeReferences.filter(Boolean));
287
359
  const cleaned = [];
@@ -309,10 +381,72 @@ function cleanupDevTags(repoDir, tagNames, activeReferences = []) {
309
381
  }
310
382
  return { cleaned, skipped };
311
383
  }
384
+ function localDevTags(repoDir) {
385
+ return run("git", ["tag", "-l", "*-dev.*"], { cwd: repoDir, capture: true }).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
386
+ }
387
+ function remoteDevTags(repoDir) {
388
+ try {
389
+ return run("git", ["ls-remote", "--tags", "origin", "*-dev.*"], { cwd: repoDir, capture: true }).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).filter((line) => !line.endsWith("^{}")).map((line) => line.split(/\s+/u)[1] ?? "").filter((ref) => ref.startsWith("refs/tags/")).map((ref) => ref.slice("refs/tags/".length));
390
+ } catch {
391
+ return [];
392
+ }
393
+ }
394
+ function collectTreeseedDevTagCleanupPlan(input) {
395
+ const tagNames = [.../* @__PURE__ */ new Set([...localDevTags(input.repoDir), ...remoteDevTags(input.repoDir)])].sort();
396
+ const classifications = tagNames.map((tagName) => classifyStaleTreeseedDevTag({
397
+ tagName,
398
+ message: gitDevTagMessage(input.repoDir, tagName),
399
+ currentVersion: input.currentVersion,
400
+ activeReferences: input.activeReferences ?? [],
401
+ branchScope: input.branchScope ?? "all",
402
+ expectedPackageName: input.packageName
403
+ }));
404
+ return {
405
+ packageName: input.packageName,
406
+ currentVersion: input.currentVersion,
407
+ branchScope: input.branchScope ?? "all",
408
+ candidates: classifications.filter((classification) => classification.action === "delete"),
409
+ skipped: classifications.filter((classification) => classification.action === "skip"),
410
+ tags: classifications
411
+ };
412
+ }
413
+ function cleanupStaleTreeseedDevTags(input) {
414
+ const plan = collectTreeseedDevTagCleanupPlan(input);
415
+ const cleaned = [];
416
+ const skipped = [...plan.skipped.map((entry) => ({ tagName: entry.tagName, reason: entry.reason }))];
417
+ for (const candidate of plan.candidates) {
418
+ if (input.dryRun) continue;
419
+ try {
420
+ run("git", ["tag", "-d", candidate.tagName], { cwd: input.repoDir });
421
+ } catch {
422
+ }
423
+ try {
424
+ run("git", ["push", "origin", `:refs/tags/${candidate.tagName}`], { cwd: input.repoDir });
425
+ cleaned.push(candidate.tagName);
426
+ } catch (error) {
427
+ skipped.push({ tagName: candidate.tagName, reason: error instanceof Error ? error.message : String(error) });
428
+ }
429
+ }
430
+ return {
431
+ status: input.dryRun ? "planned" : "completed",
432
+ packageName: input.packageName,
433
+ currentVersion: input.currentVersion,
434
+ branchScope: input.branchScope ?? "all",
435
+ candidates: plan.candidates.map((entry) => ({ tagName: entry.tagName, reason: entry.reason, branch: entry.info?.branch, branchSlug: entry.info?.branchSlug, version: entry.info?.version })),
436
+ candidateCount: plan.candidates.length,
437
+ cleaned,
438
+ cleanedCount: cleaned.length,
439
+ skipped,
440
+ skippedCount: skipped.length
441
+ };
442
+ }
312
443
  export {
313
444
  assertNoInternalDevReferences,
445
+ classifyStaleTreeseedDevTag,
314
446
  cleanupDevTags,
447
+ cleanupStaleTreeseedDevTags,
315
448
  collectInternalDevReferenceIssues,
449
+ collectTreeseedDevTagCleanupPlan,
316
450
  createDevTagMessage,
317
451
  createPackageDependencyReference,
318
452
  devTagFromDependencySpec,
@@ -323,6 +457,7 @@ export {
323
457
  isStableVersion,
324
458
  normalizeGitRemoteForDependency,
325
459
  normalizeGitRemoteForManifest,
460
+ parseTreeseedDevTag,
326
461
  rewriteInternalDependenciesToStableVersions,
327
462
  rewriteProjectInternalDependenciesToStableVersions,
328
463
  tagHasTreeseedDevMetadata,
@@ -244,6 +244,13 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
244
244
  healthcheckPath: string | null;
245
245
  healthcheckTimeoutSeconds: number | null;
246
246
  runtimeMode: string | null;
247
+ volume: {
248
+ id: string;
249
+ name: string;
250
+ mountPath: any;
251
+ created: boolean;
252
+ updated: boolean;
253
+ } | null;
247
254
  } | null;
248
255
  } | undefined)[];
249
256
  }>;
@@ -737,6 +744,13 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
737
744
  healthcheckPath: string | null;
738
745
  healthcheckTimeoutSeconds: number | null;
739
746
  runtimeMode: string | null;
747
+ volume: {
748
+ id: string;
749
+ name: string;
750
+ mountPath: any;
751
+ created: boolean;
752
+ updated: boolean;
753
+ } | null;
740
754
  } | null;
741
755
  } | undefined)[];
742
756
  }>;
@@ -631,9 +631,10 @@ function probeR2(tenantRoot, siteConfig, state, target) {
631
631
  }
632
632
  }
633
633
  function probeScaleConfiguration(siteConfig, state) {
634
- const worker = state.services?.worker ?? {};
634
+ const worker = state.services?.workerRunner ?? state.services?.worker ?? {};
635
+ const workerConfig = siteConfig.services?.workerRunner ?? siteConfig.services?.worker ?? {};
635
636
  const scalerKind = String(process.env.TREESEED_WORKER_POOL_SCALER ?? "").trim();
636
- if (scalerKind !== "railway" && siteConfig.services?.worker?.provider !== "railway") {
637
+ if (scalerKind !== "railway" && workerConfig.provider !== "railway") {
637
638
  return {
638
639
  ok: true,
639
640
  skipped: true,
@@ -53,6 +53,20 @@ export type RailwayCustomDomainSummary = {
53
53
  verificationToken: string | null;
54
54
  dnsRecords: RailwayCustomDomainDnsRecord[];
55
55
  };
56
+ export type RailwayVolumeInstanceSummary = {
57
+ id: string;
58
+ serviceId: string | null;
59
+ environmentId: string | null;
60
+ mountPath: string | null;
61
+ sizeGb: number | null;
62
+ usedGb: number | null;
63
+ };
64
+ export type RailwayVolumeSummary = {
65
+ id: string;
66
+ name: string;
67
+ projectId: string | null;
68
+ instances: RailwayVolumeInstanceSummary[];
69
+ };
56
70
  export declare function isUsableRailwayToken(value: string | undefined | null): boolean;
57
71
  export declare function resolveRailwayApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
58
72
  export declare function resolveRailwayApiUrl(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
@@ -235,6 +249,25 @@ export declare function upsertRailwayVariables({ projectId, environmentId, servi
235
249
  env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
236
250
  fetchImpl?: typeof fetch;
237
251
  }): Promise<void>;
252
+ export declare function listRailwayVolumes({ projectId, env, fetchImpl, }: {
253
+ projectId: string;
254
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
255
+ fetchImpl?: typeof fetch;
256
+ }): Promise<RailwayVolumeSummary[]>;
257
+ export declare function ensureRailwayServiceVolume({ projectId, environmentId, serviceId, name, mountPath, env, fetchImpl, }: {
258
+ projectId: string;
259
+ environmentId: string;
260
+ serviceId: string;
261
+ name: string;
262
+ mountPath: string;
263
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
264
+ fetchImpl?: typeof fetch;
265
+ }): Promise<{
266
+ volume: RailwayVolumeSummary;
267
+ instance: RailwayVolumeInstanceSummary | null;
268
+ created: boolean;
269
+ updated: boolean;
270
+ }>;
238
271
  export declare function listRailwayCustomDomains({ projectId, environmentId, serviceId, env, fetchImpl, }: {
239
272
  projectId: string;
240
273
  environmentId: string;
@@ -191,6 +191,83 @@ function normalizeRailwayCustomDomain(node) {
191
191
  dnsRecords
192
192
  };
193
193
  }
194
+ function normalizeRailwayVolumeInstance(node) {
195
+ const id = railwayConnectionLabel(node.id);
196
+ if (!id) {
197
+ return null;
198
+ }
199
+ const sizeGb = normalizeRailwayNumber(node.sizeGb ?? node.sizeGB ?? node.size_gb ?? node.capacityGb ?? node.capacityGB);
200
+ const usedGb = normalizeRailwayNumber(node.usedGb ?? node.usedGB ?? node.used_gb ?? node.currentUsageGb ?? node.currentUsageGB);
201
+ return {
202
+ id,
203
+ serviceId: railwayConnectionLabel(node.serviceId) || railwayConnectionLabel(node.service?.id) || null,
204
+ environmentId: railwayConnectionLabel(node.environmentId) || railwayConnectionLabel(node.environment?.id) || null,
205
+ mountPath: railwayConnectionLabel(node.mountPath) || railwayConnectionLabel(node.mount_path) || null,
206
+ sizeGb,
207
+ usedGb
208
+ };
209
+ }
210
+ function normalizeVolumeInstances(value) {
211
+ const direct = Array.isArray(value) ? value : null;
212
+ if (direct) {
213
+ return direct.map((entry) => entry && typeof entry === "object" ? normalizeRailwayVolumeInstance(entry) : null).filter(Boolean);
214
+ }
215
+ return normalizeConnectionNodes(value, normalizeRailwayVolumeInstance);
216
+ }
217
+ function normalizeRailwayVolume(node) {
218
+ const id = railwayConnectionLabel(node.id);
219
+ if (!id) {
220
+ return null;
221
+ }
222
+ return {
223
+ id,
224
+ name: railwayConnectionLabel(node.name),
225
+ projectId: railwayConnectionLabel(node.projectId) || null,
226
+ instances: [
227
+ ...normalizeVolumeInstances(node.instances),
228
+ ...normalizeVolumeInstances(node.volumeInstances),
229
+ ...normalizeVolumeInstances(node.volume_instances)
230
+ ]
231
+ };
232
+ }
233
+ function collectRailwayVolumes(value, seen = /* @__PURE__ */ new Set()) {
234
+ const volumes = [];
235
+ const visit = (entry) => {
236
+ if (!entry || typeof entry !== "object") {
237
+ return;
238
+ }
239
+ if (seen.has(entry)) {
240
+ return;
241
+ }
242
+ seen.add(entry);
243
+ if (Array.isArray(entry)) {
244
+ for (const item of entry) {
245
+ visit(item);
246
+ }
247
+ return;
248
+ }
249
+ const record = entry;
250
+ const volume = normalizeRailwayVolume(record);
251
+ if (volume && (record.volumeInstances !== void 0 || record.instances !== void 0 || record.projectId !== void 0 || record.name !== void 0)) {
252
+ volumes.push(volume);
253
+ }
254
+ for (const child of Object.values(record)) {
255
+ visit(child);
256
+ }
257
+ };
258
+ visit(value);
259
+ const byId = /* @__PURE__ */ new Map();
260
+ for (const volume of volumes) {
261
+ const existing = byId.get(volume.id);
262
+ byId.set(volume.id, existing ? {
263
+ ...existing,
264
+ name: existing.name || volume.name,
265
+ projectId: existing.projectId || volume.projectId,
266
+ instances: [...existing.instances, ...volume.instances]
267
+ } : volume);
268
+ }
269
+ return [...byId.values()];
270
+ }
194
271
  async function railwayGraphqlRequest({
195
272
  query,
196
273
  variables,
@@ -811,6 +888,200 @@ mutation TreeseedRailwayVariableCollectionUpsert($input: VariableCollectionUpser
811
888
  fetchImpl
812
889
  });
813
890
  }
891
+ async function listRailwayVolumes({
892
+ projectId,
893
+ env = process.env,
894
+ fetchImpl = fetch
895
+ }) {
896
+ const query = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_LIST_QUERY") || `
897
+ query TreeseedRailwayVolumeList($projectId: String!) {
898
+ project(id: $projectId) {
899
+ id
900
+ volumes {
901
+ edges {
902
+ node {
903
+ id
904
+ name
905
+ projectId
906
+ volumeInstances {
907
+ edges {
908
+ node {
909
+ id
910
+ serviceId
911
+ environmentId
912
+ mountPath
913
+ }
914
+ }
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ }
921
+ `.trim();
922
+ const payload = await railwayGraphqlRequest({
923
+ query,
924
+ variables: { projectId },
925
+ env,
926
+ fetchImpl
927
+ });
928
+ return collectRailwayVolumes(payload.data);
929
+ }
930
+ async function createRailwayVolume({
931
+ projectId,
932
+ environmentId,
933
+ serviceId,
934
+ name,
935
+ mountPath,
936
+ env = process.env,
937
+ fetchImpl = fetch
938
+ }) {
939
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_CREATE_MUTATION") || `
940
+ mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
941
+ volumeCreate(input: $input) {
942
+ id
943
+ name
944
+ projectId
945
+ volumeInstances {
946
+ edges {
947
+ node {
948
+ id
949
+ serviceId
950
+ environmentId
951
+ mountPath
952
+ }
953
+ }
954
+ }
955
+ }
956
+ }
957
+ `.trim();
958
+ const payload = await railwayGraphqlRequest({
959
+ query: mutation,
960
+ variables: {
961
+ input: {
962
+ projectId,
963
+ environmentId,
964
+ serviceId,
965
+ name,
966
+ mountPath
967
+ }
968
+ },
969
+ env,
970
+ fetchImpl
971
+ });
972
+ const volume = collectRailwayVolumes(payload.data)[0] ?? null;
973
+ if (!volume) {
974
+ throw new Error(`Railway volume create did not return a usable volume for ${name}.`);
975
+ }
976
+ return volume;
977
+ }
978
+ async function updateRailwayVolumeName({
979
+ volumeId,
980
+ name,
981
+ env = process.env,
982
+ fetchImpl = fetch
983
+ }) {
984
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_UPDATE_MUTATION") || `
985
+ mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
986
+ volumeUpdate(id: $id, input: $input) {
987
+ id
988
+ name
989
+ projectId
990
+ volumeInstances {
991
+ edges {
992
+ node {
993
+ id
994
+ serviceId
995
+ environmentId
996
+ mountPath
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ `.trim();
1003
+ const payload = await railwayGraphqlRequest({
1004
+ query: mutation,
1005
+ variables: {
1006
+ id: volumeId,
1007
+ input: { name }
1008
+ },
1009
+ env,
1010
+ fetchImpl
1011
+ });
1012
+ return collectRailwayVolumes(payload.data)[0] ?? null;
1013
+ }
1014
+ async function updateRailwayVolumeInstanceMountPath({
1015
+ instanceId,
1016
+ mountPath,
1017
+ env = process.env,
1018
+ fetchImpl = fetch
1019
+ }) {
1020
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_INSTANCE_UPDATE_MUTATION") || `
1021
+ mutation TreeseedRailwayVolumeInstanceUpdate($id: String!, $input: VolumeInstanceUpdateInput!) {
1022
+ volumeInstanceUpdate(id: $id, input: $input) {
1023
+ id
1024
+ serviceId
1025
+ environmentId
1026
+ mountPath
1027
+ }
1028
+ }
1029
+ `.trim();
1030
+ await railwayGraphqlRequest({
1031
+ query: mutation,
1032
+ variables: {
1033
+ id: instanceId,
1034
+ input: { mountPath }
1035
+ },
1036
+ env,
1037
+ fetchImpl
1038
+ });
1039
+ }
1040
+ async function ensureRailwayServiceVolume({
1041
+ projectId,
1042
+ environmentId,
1043
+ serviceId,
1044
+ name,
1045
+ mountPath,
1046
+ env = process.env,
1047
+ fetchImpl = fetch
1048
+ }) {
1049
+ if (!mountPath.startsWith("/")) {
1050
+ throw new Error(`Railway volume mount path must be absolute: ${mountPath}`);
1051
+ }
1052
+ const volumes = await listRailwayVolumes({ projectId, env, fetchImpl });
1053
+ let volume = volumes.find(
1054
+ (candidate) => candidate.instances.some((instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId)
1055
+ ) ?? volumes.find((candidate) => candidate.name === name) ?? null;
1056
+ let created = false;
1057
+ let updated = false;
1058
+ if (!volume) {
1059
+ volume = await createRailwayVolume({
1060
+ projectId,
1061
+ environmentId,
1062
+ serviceId,
1063
+ name,
1064
+ mountPath,
1065
+ env,
1066
+ fetchImpl
1067
+ });
1068
+ created = true;
1069
+ }
1070
+ if (volume.name && volume.name !== name) {
1071
+ volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1072
+ updated = true;
1073
+ }
1074
+ const instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1075
+ if (instance && instance.mountPath !== mountPath) {
1076
+ await updateRailwayVolumeInstanceMountPath({ instanceId: instance.id, mountPath, env, fetchImpl });
1077
+ volume = {
1078
+ ...volume,
1079
+ instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1080
+ };
1081
+ updated = true;
1082
+ }
1083
+ return { volume, instance, created, updated };
1084
+ }
814
1085
  async function listRailwayCustomDomains({
815
1086
  projectId,
816
1087
  environmentId,
@@ -863,6 +1134,7 @@ export {
863
1134
  ensureRailwayProject,
864
1135
  ensureRailwayService,
865
1136
  ensureRailwayServiceInstanceConfiguration,
1137
+ ensureRailwayServiceVolume,
866
1138
  getRailwayAuthProfile,
867
1139
  getRailwayProject,
868
1140
  getRailwayServiceInstance,
@@ -872,6 +1144,7 @@ export {
872
1144
  listRailwayProjects,
873
1145
  listRailwayServices,
874
1146
  listRailwayVariables,
1147
+ listRailwayVolumes,
875
1148
  normalizeRailwayEnvironmentName,
876
1149
  railwayGraphqlRequest,
877
1150
  resolveRailwayApiToken,
@@ -1,4 +1,7 @@
1
1
  import { type TreeseedBootstrapTaskPrefix, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
2
+ export declare function deriveRailwayWorkerRunnerServiceName(projectSlug: any, index?: number): string;
3
+ export declare function deriveRailwayWorkerRunnerVolumeName(serviceName: any): string;
4
+ export declare function railwayServiceRuntimeStartCommand(service: any): any;
2
5
  export declare function isUsableRailwayToken(value: any): boolean;
3
6
  export declare function resolveRailwayAuthToken(env?: NodeJS.ProcessEnv): string;
4
7
  export declare function buildRailwayCommandEnv(env?: NodeJS.ProcessEnv): {
@@ -55,6 +58,10 @@ export declare function configuredRailwayServices(tenantRoot: any, scope: any):
55
58
  runtimeMode: any;
56
59
  schedule: string[];
57
60
  hostingKind: string;
61
+ runnerPool: {
62
+ bootstrapIndex: number;
63
+ volumeMountPath: string;
64
+ } | null;
58
65
  } | null)[];
59
66
  export declare function configuredRailwayScheduledJobs(tenantRoot: any, scope: any, { phase }?: {
60
67
  phase?: string | undefined;
@@ -95,6 +102,10 @@ export declare function validateRailwayServiceConfiguration(tenantRoot: any, sco
95
102
  runtimeMode: any;
96
103
  schedule: string[];
97
104
  hostingKind: string;
105
+ runnerPool: {
106
+ bootstrapIndex: number;
107
+ volumeMountPath: string;
108
+ } | null;
98
109
  } | null)[];
99
110
  schedules: {
100
111
  service: string;
@@ -134,6 +145,10 @@ export declare function validateRailwayDeployPrerequisites(tenantRoot: any, scop
134
145
  runtimeMode: any;
135
146
  schedule: string[];
136
147
  hostingKind: string;
148
+ runnerPool: {
149
+ bootstrapIndex: number;
150
+ volumeMountPath: string;
151
+ } | null;
137
152
  } | null)[];
138
153
  schedules: {
139
154
  service: string;
@@ -296,5 +311,12 @@ export declare function deployRailwayService(tenantRoot: any, service: any, { dr
296
311
  healthcheckPath: string | null;
297
312
  healthcheckTimeoutSeconds: number | null;
298
313
  runtimeMode: string | null;
314
+ volume: {
315
+ id: string;
316
+ name: string;
317
+ mountPath: any;
318
+ created: boolean;
319
+ updated: boolean;
320
+ } | null;
299
321
  } | null;
300
322
  }>;