@treeseed/sdk 0.10.5 → 0.10.7

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 (41) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +58 -0
  3. package/dist/operations/repository-operations.d.ts +129 -0
  4. package/dist/operations/repository-operations.js +634 -0
  5. package/dist/operations/services/config-runtime.d.ts +7 -6
  6. package/dist/operations/services/config-runtime.js +45 -25
  7. package/dist/operations/services/deploy.d.ts +42 -0
  8. package/dist/operations/services/deploy.js +1 -1
  9. package/dist/operations/services/project-platform.d.ts +41 -1
  10. package/dist/operations/services/project-platform.js +13 -0
  11. package/dist/operations/services/railway-api.d.ts +35 -1
  12. package/dist/operations/services/railway-api.js +240 -35
  13. package/dist/operations/services/railway-deploy.d.ts +16 -234
  14. package/dist/operations/services/railway-deploy.js +177 -62
  15. package/dist/operations/services/release-candidate.js +1 -2
  16. package/dist/operations/services/runtime-tools.d.ts +14 -0
  17. package/dist/operations/services/runtime-tools.js +15 -1
  18. package/dist/operations/services/workspace-save.d.ts +24 -0
  19. package/dist/operations/services/workspace-save.js +143 -3
  20. package/dist/operations/services/workspace-tools.js +1 -1
  21. package/dist/platform/env.yaml +163 -2
  22. package/dist/platform/environment.d.ts +1 -0
  23. package/dist/platform/environment.js +9 -0
  24. package/dist/platform-operation-store.d.ts +90 -0
  25. package/dist/platform-operation-store.js +505 -0
  26. package/dist/platform-operations.d.ts +265 -0
  27. package/dist/platform-operations.js +421 -0
  28. package/dist/reconcile/bootstrap-systems.js +3 -3
  29. package/dist/reconcile/builtin-adapters.js +225 -29
  30. package/dist/reconcile/contracts.d.ts +1 -1
  31. package/dist/reconcile/desired-state.d.ts +14 -0
  32. package/dist/reconcile/desired-state.js +4 -0
  33. package/dist/reconcile/engine.d.ts +28 -0
  34. package/dist/reconcile/state.js +3 -0
  35. package/dist/reconcile/units.js +2 -0
  36. package/dist/workflow/operations.d.ts +13 -5
  37. package/dist/workflow/operations.js +69 -12
  38. package/dist/workflow-state.d.ts +2 -0
  39. package/dist/workflow-state.js +7 -2
  40. package/dist/workflow.d.ts +2 -0
  41. package/package.json +15 -2
@@ -1,5 +1,7 @@
1
1
  const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
2
2
  const DEFAULT_RAILWAY_WORKSPACE = "knowledge-coop";
3
+ const RAILWAY_POSTGRES_TEMPLATE_ID = "b55da7dc-09be-4140-bc65-1284d15d349c";
4
+ const RAILWAY_POSTGRES_TEMPLATE_SERVICE_ID = "b55da7dc-09be-4140-bc65-1284b15d349b";
3
5
  function normalizeRailwayEnvironmentName(value) {
4
6
  const normalized = typeof value === "string" ? value.trim() : "";
5
7
  if (!normalized) {
@@ -114,6 +116,7 @@ function normalizeProject(node) {
114
116
  id,
115
117
  name,
116
118
  workspaceId: railwayConnectionLabel(node.workspaceId) || null,
119
+ deletedAt: railwayConnectionLabel(node.deletedAt) || null,
117
120
  environments: normalizeConnectionNodes(node.environments, normalizeEnvironment),
118
121
  services: normalizeConnectionNodes(node.services, normalizeService)
119
122
  };
@@ -203,10 +206,15 @@ function normalizeRailwayVolumeInstance(node) {
203
206
  serviceId: railwayConnectionLabel(node.serviceId) || railwayConnectionLabel(node.service?.id) || null,
204
207
  environmentId: railwayConnectionLabel(node.environmentId) || railwayConnectionLabel(node.environment?.id) || null,
205
208
  mountPath: railwayConnectionLabel(node.mountPath) || railwayConnectionLabel(node.mount_path) || null,
209
+ state: railwayConnectionLabel(node.state) || null,
206
210
  sizeGb,
207
211
  usedGb
208
212
  };
209
213
  }
214
+ function isActiveRailwayVolumeInstance(instance) {
215
+ const state = String(instance.state ?? "READY").toUpperCase();
216
+ return state !== "DELETING" && state !== "DELETED";
217
+ }
210
218
  function normalizeVolumeInstances(value) {
211
219
  const direct = Array.isArray(value) ? value : null;
212
220
  if (direct) {
@@ -389,6 +397,7 @@ query TreeseedRailwayProjects($workspaceId: String!, $first: Int!) {
389
397
  id
390
398
  name
391
399
  workspaceId
400
+ deletedAt
392
401
  environments(first: 50) {
393
402
  edges {
394
403
  node {
@@ -428,6 +437,7 @@ query TreeseedRailwayProject($projectId: String!) {
428
437
  id
429
438
  name
430
439
  workspaceId
440
+ deletedAt
431
441
  environments(first: 50) {
432
442
  edges {
433
443
  node {
@@ -466,7 +476,7 @@ async function ensureRailwayProject({
466
476
  const desiredProjectName = railwayConnectionLabel(projectName);
467
477
  const desiredProjectId = railwayConnectionLabel(projectId);
468
478
  const existing = projects.find(
469
- (project2) => desiredProjectId && project2.id === desiredProjectId || desiredProjectName && project2.name === desiredProjectName
479
+ (project2) => !project2.deletedAt && (desiredProjectId && project2.id === desiredProjectId || desiredProjectName && project2.name === desiredProjectName)
470
480
  ) ?? null;
471
481
  if (existing) {
472
482
  return { workspace: workspaceContext, project: existing, created: false };
@@ -481,6 +491,7 @@ mutation TreeseedRailwayProjectCreate($input: ProjectCreateInput!) {
481
491
  id
482
492
  name
483
493
  workspaceId
494
+ deletedAt
484
495
  environments(first: 50) {
485
496
  edges {
486
497
  node {
@@ -622,6 +633,81 @@ mutation TreeseedRailwayServiceCreate($input: ServiceCreateInput!) {
622
633
  }
623
634
  return { service, created: true };
624
635
  }
636
+ async function updateRailwayServiceName({
637
+ serviceId,
638
+ name,
639
+ env = process.env,
640
+ fetchImpl = fetch
641
+ }) {
642
+ const desiredName = railwayConnectionLabel(name);
643
+ if (!serviceId || !desiredName) {
644
+ throw new Error("Railway service rename requires a service id and name.");
645
+ }
646
+ const payload = await railwayGraphqlRequest({
647
+ query: `
648
+ mutation TreeseedRailwayServiceUpdate($id: String!, $input: ServiceUpdateInput!) {
649
+ serviceUpdate(id: $id, input: $input) {
650
+ id
651
+ name
652
+ }
653
+ }
654
+ `.trim(),
655
+ variables: {
656
+ id: serviceId,
657
+ input: { name: desiredName }
658
+ },
659
+ env,
660
+ fetchImpl
661
+ });
662
+ const service = payload.data?.serviceUpdate ? normalizeService(payload.data.serviceUpdate) : null;
663
+ if (!service) {
664
+ throw new Error(`Railway service rename did not return a usable service for ${desiredName}.`);
665
+ }
666
+ return service;
667
+ }
668
+ async function ensureRailwayPostgresService({
669
+ projectId,
670
+ environmentId,
671
+ serviceName,
672
+ env = process.env,
673
+ fetchImpl = fetch
674
+ }) {
675
+ const desiredServiceName = railwayConnectionLabel(serviceName);
676
+ if (!desiredServiceName) {
677
+ throw new Error("Railway Postgres service creation requires a service name.");
678
+ }
679
+ const services = await listRailwayServices({ projectId, env, fetchImpl });
680
+ const existing = services.find((service2) => service2.name === desiredServiceName || service2.id === desiredServiceName) ?? null;
681
+ if (existing) {
682
+ return { service: existing, created: false };
683
+ }
684
+ const created = await railwayGraphqlRequest({
685
+ query: `
686
+ mutation TreeseedRailwayPostgresServiceCreate($input: ServiceCreateInput!) {
687
+ serviceCreate(input: $input) {
688
+ id
689
+ name
690
+ }
691
+ }
692
+ `.trim(),
693
+ variables: {
694
+ input: {
695
+ projectId,
696
+ environmentId,
697
+ name: desiredServiceName,
698
+ templateId: RAILWAY_POSTGRES_TEMPLATE_ID,
699
+ templateServiceId: RAILWAY_POSTGRES_TEMPLATE_SERVICE_ID
700
+ }
701
+ },
702
+ env,
703
+ fetchImpl
704
+ });
705
+ const service = created.data?.serviceCreate ? normalizeService(created.data.serviceCreate) : null;
706
+ if (!service) {
707
+ throw new Error(`Railway Postgres service create did not return a usable service for ${desiredServiceName}.`);
708
+ }
709
+ return { service, created: true };
710
+ }
625
711
  async function listRailwayServices({
626
712
  projectId,
627
713
  env = process.env,
@@ -877,25 +963,49 @@ async function upsertRailwayVariables({
877
963
  if (Object.keys(variables).length === 0) {
878
964
  return;
879
965
  }
880
- await railwayGraphqlRequest({
881
- query: `
966
+ const query = `
882
967
  mutation TreeseedRailwayVariableCollectionUpsert($input: VariableCollectionUpsertInput!) {
883
968
  variableCollectionUpsert(input: $input)
884
969
  }
885
- `.trim(),
886
- variables: {
887
- input: {
888
- projectId,
889
- environmentId,
890
- serviceId: serviceId || null,
891
- variables,
892
- replace: false,
893
- skipDeploys: true
894
- }
895
- },
896
- env,
897
- fetchImpl
898
- });
970
+ `.trim();
971
+ const input = {
972
+ projectId,
973
+ environmentId,
974
+ serviceId: serviceId || null,
975
+ variables,
976
+ replace: false,
977
+ skipDeploys: true
978
+ };
979
+ try {
980
+ await railwayGraphqlRequest({
981
+ query,
982
+ variables: { input },
983
+ env,
984
+ fetchImpl
985
+ });
986
+ } catch (error) {
987
+ const message = error instanceof Error ? error.message : String(error ?? "");
988
+ if (!/Problem processing request/iu.test(message) || Object.keys(variables).length <= 1) {
989
+ throw error;
990
+ }
991
+ for (const [key, value] of Object.entries(variables)) {
992
+ await railwayGraphqlRequest({
993
+ query,
994
+ variables: {
995
+ input: {
996
+ projectId,
997
+ environmentId,
998
+ serviceId: serviceId || null,
999
+ variables: { [key]: value },
1000
+ replace: false,
1001
+ skipDeploys: true
1002
+ }
1003
+ },
1004
+ env,
1005
+ fetchImpl
1006
+ });
1007
+ }
1008
+ }
899
1009
  }
900
1010
  async function listRailwayVolumes({
901
1011
  projectId,
@@ -919,6 +1029,7 @@ query TreeseedRailwayVolumeList($projectId: String!) {
919
1029
  serviceId
920
1030
  environmentId
921
1031
  mountPath
1032
+ state
922
1033
  }
923
1034
  }
924
1035
  }
@@ -958,6 +1069,7 @@ mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
958
1069
  serviceId
959
1070
  environmentId
960
1071
  mountPath
1072
+ state
961
1073
  }
962
1074
  }
963
1075
  }
@@ -971,7 +1083,6 @@ mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
971
1083
  projectId,
972
1084
  environmentId,
973
1085
  serviceId,
974
- name,
975
1086
  mountPath
976
1087
  }
977
1088
  },
@@ -982,6 +1093,25 @@ mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
982
1093
  if (!volume) {
983
1094
  throw new Error(`Railway volume create did not return a usable volume for ${name}.`);
984
1095
  }
1096
+ if (name && volume.name !== name) {
1097
+ try {
1098
+ const renamed = await updateRailwayVolumeName({
1099
+ volumeId: volume.id,
1100
+ name,
1101
+ env,
1102
+ fetchImpl
1103
+ });
1104
+ return {
1105
+ ...volume,
1106
+ name: renamed.name || name
1107
+ };
1108
+ } catch {
1109
+ return {
1110
+ ...volume,
1111
+ name: volume.name || name
1112
+ };
1113
+ }
1114
+ }
985
1115
  return volume;
986
1116
  }
987
1117
  async function updateRailwayVolumeName({
@@ -991,8 +1121,8 @@ async function updateRailwayVolumeName({
991
1121
  fetchImpl = fetch
992
1122
  }) {
993
1123
  const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_UPDATE_MUTATION") || `
994
- mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
995
- volumeUpdate(id: $id, input: $input) {
1124
+ mutation TreeseedRailwayVolumeUpdate($volumeId: String!, $input: VolumeUpdateInput!) {
1125
+ volumeUpdate(volumeId: $volumeId, input: $input) {
996
1126
  id
997
1127
  name
998
1128
  projectId
@@ -1003,6 +1133,7 @@ mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
1003
1133
  serviceId
1004
1134
  environmentId
1005
1135
  mountPath
1136
+ state
1006
1137
  }
1007
1138
  }
1008
1139
  }
@@ -1012,7 +1143,7 @@ mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
1012
1143
  const payload = await railwayGraphqlRequest({
1013
1144
  query: mutation,
1014
1145
  variables: {
1015
- id: volumeId,
1146
+ volumeId,
1016
1147
  input: { name }
1017
1148
  },
1018
1149
  env,
@@ -1021,26 +1152,25 @@ mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
1021
1152
  return collectRailwayVolumes(payload.data)[0] ?? null;
1022
1153
  }
1023
1154
  async function updateRailwayVolumeInstanceMountPath({
1024
- instanceId,
1155
+ volumeId,
1156
+ serviceId,
1025
1157
  mountPath,
1026
1158
  env = process.env,
1027
1159
  fetchImpl = fetch
1028
1160
  }) {
1029
1161
  const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_INSTANCE_UPDATE_MUTATION") || `
1030
- mutation TreeseedRailwayVolumeInstanceUpdate($id: String!, $input: VolumeInstanceUpdateInput!) {
1031
- volumeInstanceUpdate(id: $id, input: $input) {
1032
- id
1033
- serviceId
1034
- environmentId
1035
- mountPath
1036
- }
1162
+ mutation TreeseedRailwayVolumeInstanceUpdate($volumeId: String!, $input: VolumeInstanceUpdateInput!) {
1163
+ volumeInstanceUpdate(volumeId: $volumeId, input: $input)
1037
1164
  }
1038
1165
  `.trim();
1039
1166
  await railwayGraphqlRequest({
1040
1167
  query: mutation,
1041
1168
  variables: {
1042
- id: instanceId,
1043
- input: { mountPath }
1169
+ volumeId,
1170
+ input: {
1171
+ ...serviceId ? { serviceId } : {},
1172
+ mountPath
1173
+ }
1044
1174
  },
1045
1175
  env,
1046
1176
  fetchImpl
@@ -1059,9 +1189,13 @@ async function ensureRailwayServiceVolume({
1059
1189
  throw new Error(`Railway volume mount path must be absolute: ${mountPath}`);
1060
1190
  }
1061
1191
  const volumes = await listRailwayVolumes({ projectId, env, fetchImpl });
1062
- let volume = volumes.find(
1192
+ const activeVolumes = volumes.map((candidate) => ({
1193
+ ...candidate,
1194
+ instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
1195
+ })).filter((candidate) => candidate.instances.length > 0);
1196
+ let volume = activeVolumes.find(
1063
1197
  (candidate) => candidate.instances.some((instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId)
1064
- ) ?? volumes.find((candidate) => candidate.name === name) ?? null;
1198
+ ) ?? activeVolumes.find((candidate) => candidate.name === name) ?? null;
1065
1199
  let created = false;
1066
1200
  let updated = false;
1067
1201
  if (!volume) {
@@ -1080,9 +1214,15 @@ async function ensureRailwayServiceVolume({
1080
1214
  volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1081
1215
  updated = true;
1082
1216
  }
1083
- const instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1217
+ let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1218
+ if (!instance && volume.instances.some((entry) => entry.environmentId === environmentId)) {
1219
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1220
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1221
+ instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1222
+ updated = true;
1223
+ }
1084
1224
  if (instance && instance.mountPath !== mountPath) {
1085
- await updateRailwayVolumeInstanceMountPath({ instanceId: instance.id, mountPath, env, fetchImpl });
1225
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
1086
1226
  volume = {
1087
1227
  ...volume,
1088
1228
  instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
@@ -1138,8 +1278,72 @@ query TreeseedRailwayCustomDomains($projectId: String!, $environmentId: String!,
1138
1278
  });
1139
1279
  return Array.isArray(payload.data?.domains?.customDomains) ? payload.data.domains.customDomains.map((entry) => entry && typeof entry === "object" ? normalizeRailwayCustomDomain(entry) : null).filter(Boolean) : [];
1140
1280
  }
1281
+ async function ensureRailwayCustomDomain({
1282
+ projectId,
1283
+ environmentId,
1284
+ serviceId,
1285
+ domain,
1286
+ env = process.env,
1287
+ fetchImpl = fetch
1288
+ }) {
1289
+ const normalizedDomain = railwayConnectionLabel(domain);
1290
+ if (!normalizedDomain) {
1291
+ throw new Error("Railway custom domain creation requires a domain.");
1292
+ }
1293
+ const existing = await listRailwayCustomDomains({ projectId, environmentId, serviceId, env, fetchImpl });
1294
+ const matched = existing.find((entry) => entry.domain === normalizedDomain) ?? null;
1295
+ if (matched) {
1296
+ return { domain: matched, created: false };
1297
+ }
1298
+ const payload = await railwayGraphqlRequest({
1299
+ query: `
1300
+ mutation TreeseedRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
1301
+ customDomainCreate(input: $input) {
1302
+ id
1303
+ domain
1304
+ environmentId
1305
+ serviceId
1306
+ targetPort
1307
+ status {
1308
+ verified
1309
+ certificateStatus
1310
+ verificationDnsHost
1311
+ verificationToken
1312
+ dnsRecords {
1313
+ fqdn
1314
+ hostlabel
1315
+ recordType
1316
+ requiredValue
1317
+ currentValue
1318
+ status
1319
+ zone
1320
+ purpose
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ `.trim(),
1326
+ variables: {
1327
+ input: {
1328
+ projectId,
1329
+ environmentId,
1330
+ serviceId,
1331
+ domain: normalizedDomain
1332
+ }
1333
+ },
1334
+ env,
1335
+ fetchImpl
1336
+ });
1337
+ const created = payload.data?.customDomainCreate ? normalizeRailwayCustomDomain(payload.data.customDomainCreate) : null;
1338
+ if (!created) {
1339
+ throw new Error(`Railway custom domain create did not return a usable domain for ${normalizedDomain}.`);
1340
+ }
1341
+ return { domain: created, created: true };
1342
+ }
1141
1343
  export {
1344
+ ensureRailwayCustomDomain,
1142
1345
  ensureRailwayEnvironment,
1346
+ ensureRailwayPostgresService,
1143
1347
  ensureRailwayProject,
1144
1348
  ensureRailwayService,
1145
1349
  ensureRailwayServiceInstanceConfiguration,
@@ -1160,5 +1364,6 @@ export {
1160
1364
  resolveRailwayApiUrl,
1161
1365
  resolveRailwayWorkspace,
1162
1366
  resolveRailwayWorkspaceContext,
1367
+ updateRailwayServiceName,
1163
1368
  upsertRailwayVariables
1164
1369
  };