@treeseed/sdk 0.12.60 → 0.12.62

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 (44) hide show
  1. package/dist/guarantees/index.js +59 -56
  2. package/dist/hosting/contracts.d.ts +0 -19
  3. package/dist/hosting/graph.d.ts +1 -86
  4. package/dist/hosting/graph.js +96 -245
  5. package/dist/local-dev/managed-dev.js +30 -8
  6. package/dist/managed-dependencies.d.ts +3 -0
  7. package/dist/managed-dependencies.js +294 -20
  8. package/dist/operations/services/deploy.js +5 -5
  9. package/dist/operations/services/deployment-readiness.js +5 -4
  10. package/dist/operations/services/git-runner.d.ts +2 -0
  11. package/dist/operations/services/git-runner.js +23 -2
  12. package/dist/operations/services/hosted-service-checks.js +28 -0
  13. package/dist/operations/services/live-hosted-service-checks.js +51 -15
  14. package/dist/operations/services/local-cleanup.d.ts +1 -0
  15. package/dist/operations/services/local-cleanup.js +28 -9
  16. package/dist/operations/services/package-adapters.js +3 -3
  17. package/dist/operations/services/railway-api.d.ts +72 -28
  18. package/dist/operations/services/railway-api.js +321 -876
  19. package/dist/operations/services/railway-cli.d.ts +47 -0
  20. package/dist/operations/services/railway-cli.js +142 -0
  21. package/dist/operations/services/railway-deploy.d.ts +2 -2
  22. package/dist/operations/services/railway-deploy.js +36 -91
  23. package/dist/operations/services/railway-source-policy.d.ts +6 -0
  24. package/dist/operations/services/railway-source-policy.js +52 -9
  25. package/dist/operations/services/repository-save-orchestrator.d.ts +2 -0
  26. package/dist/operations/services/repository-save-orchestrator.js +45 -14
  27. package/dist/operations-types.d.ts +3 -1
  28. package/dist/platform/contracts.d.ts +1 -0
  29. package/dist/platform/deploy-config.js +2 -1
  30. package/dist/reconcile/builtin-adapters.js +519 -684
  31. package/dist/reconcile/desired-state.js +5 -3
  32. package/dist/reconcile/engine.js +34 -28
  33. package/dist/reconcile/live-acceptance.js +2 -11
  34. package/dist/reconcile/providers/railway-iac.d.ts +147 -0
  35. package/dist/reconcile/providers/railway-iac.js +289 -16
  36. package/dist/scenes/runner.js +11 -11
  37. package/dist/scripts/build-dist.js +22 -0
  38. package/dist/workflow/operations.d.ts +12 -0
  39. package/dist/workflow/operations.js +441 -105
  40. package/dist/workflow/runs.d.ts +5 -0
  41. package/dist/workflow/runs.js +9 -3
  42. package/dist/workflow-support.d.ts +1 -1
  43. package/dist/workflow-support.js +3 -1
  44. package/package.json +1 -2
@@ -3,12 +3,10 @@ import { loadTreeseedPlugins } from "../platform/plugins/runtime.js";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { parse as parseYaml } from "yaml";
6
- import { createPersistentDeployTarget } from "../operations/services/deploy.js";
7
- import { collectTreeseedConfigSeedValues, resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
6
+ import { resolveTreeseedMachineEnvironmentValues } from "../operations/services/config-runtime.js";
8
7
  import { classifyTreeseedGitMode, runTreeseedGitText } from "../operations/services/git-runner.js";
9
- import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../operations/services/railway-source-policy.js";
8
+ import { apiRailwayDefaultDockerfilePath, apiRailwayDefaultSourceRepo, assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService, railwayEnvironmentQualifiedServiceName, railwayTreeDxServiceName } from "../operations/services/railway-source-policy.js";
10
9
  import { createTreeseedCanonicalReconcileReport } from "../reconcile/index.js";
11
- import { reconcileTreeseedTarget } from "../reconcile/index.js";
12
10
  import { discoverTreeseedApplications, findTreeseedApplication } from "./apps.js";
13
11
  import {
14
12
  createDefaultHostAdapters,
@@ -18,6 +16,16 @@ import {
18
16
  sanitizedUnitConfig,
19
17
  summarizePlacementStatus
20
18
  } from "./builtins.js";
19
+ const RAILWAY_SERVICE_NAME_MAX_LENGTH = 32;
20
+ const RAILWAY_VOLUME_NAME_MAX_LENGTH = 48;
21
+ function assertRailwayResourceNames(serviceName, volumeName) {
22
+ if (serviceName.length > RAILWAY_SERVICE_NAME_MAX_LENGTH) {
23
+ throw new Error(`Railway service name ${serviceName} exceeds the provider limit of ${RAILWAY_SERVICE_NAME_MAX_LENGTH} characters.`);
24
+ }
25
+ if (volumeName && volumeName.length > RAILWAY_VOLUME_NAME_MAX_LENGTH) {
26
+ throw new Error(`Railway volume name ${volumeName} exceeds the provider limit of ${RAILWAY_VOLUME_NAME_MAX_LENGTH} characters.`);
27
+ }
28
+ }
21
29
  const ENVIRONMENT_NAMES = {
22
30
  local: "local",
23
31
  staging: "staging",
@@ -83,8 +91,8 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
83
91
  imageRef,
84
92
  imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
85
93
  };
86
- const serviceName = "public-treedx-node-01";
87
- assertApiRailwaySourcePolicy(input.environment, { key: serviceName, serviceName, ...policy2 });
94
+ const serviceName = railwayTreeDxServiceName(1, input.environment);
95
+ assertApiRailwaySourcePolicy(input.environment, { key: "public-treedx-node-01", serviceName, ...policy2 });
88
96
  return policy2;
89
97
  }
90
98
  const policy = {
@@ -98,7 +106,7 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
98
106
  };
99
107
  assertApiRailwaySourcePolicy(input.environment, {
100
108
  key: "public-treedx-node-01",
101
- serviceName: "public-treedx-node-01",
109
+ serviceName: railwayTreeDxServiceName(1, input.environment),
102
110
  dockerfilePath: railway.dockerfilePath ?? "/Dockerfile",
103
111
  ...policy
104
112
  });
@@ -176,7 +184,8 @@ function railwaySourcePolicy(input, serviceKey, service, imageRef) {
176
184
  const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
177
185
  const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
178
186
  const baseServiceName = service.railway?.serviceName ?? null;
179
- const serviceName = baseServiceName;
187
+ const environmentConfig = service.environments?.[input.environment];
188
+ const serviceName = typeof environmentConfig?.serviceName === "string" && environmentConfig.serviceName.trim() ? environmentConfig.serviceName.trim() : isApiRailwaySourcePolicyService({ key: serviceKey, serviceName: baseServiceName }) && baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, input.environment) : baseServiceName;
180
189
  const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
181
190
  const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
182
191
  const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
@@ -384,54 +393,81 @@ function buildProfileFromDeployConfig(input) {
384
393
  const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
385
394
  const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
386
395
  const baseServiceName = service.railway?.serviceName ?? null;
387
- const effectiveServiceName = baseServiceName;
388
- services.push({
389
- id: serviceKey,
390
- label: placement === "runner-capacity" ? "Runner Capacity" : serviceKey === "api" ? "API Runtime" : serviceKey,
391
- serviceType,
392
- placement,
393
- projectGroupId: defaultProjectGroup,
394
- config: {
395
- rootDir: service.railway?.rootDir ?? service.rootDir ?? ".",
396
- imageRef: sourcePolicy.imageRef,
397
- imageRefEnv: sourcePolicy.sourceMode === "image" ? imageRefEnv : null,
398
- sourceMode: sourcePolicy.sourceMode,
399
- sourceRepo: sourcePolicy.sourceRepo,
400
- sourceBranch: sourcePolicy.sourceBranch,
401
- sourceCommit: sourcePolicy.sourceCommit,
402
- sourceRootDirectory: sourcePolicy.sourceRootDirectory,
403
- dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
404
- buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
405
- startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
406
- healthcheckPath: service.railway?.healthcheckPath ?? null,
407
- runtimeMode: service.railway?.runtimeMode ?? null,
408
- volumeMountPath: service.railway?.volumeMountPath ?? null,
409
- runnerPool: service.railway?.runnerPool ?? null,
410
- resourceType: service.railway?.resourceType ?? null,
411
- serviceName: effectiveServiceName,
412
- serviceTargets: service.railway?.serviceTargets ?? null
413
- },
414
- secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
415
- variableRefs: serviceKey === "operationsRunner" ? ["TREESEED_PLATFORM_RUNNER_ID", "TREESEED_PLATFORM_RUNNER_DATA_DIR", "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"] : [],
416
- metadata: String(serviceKey).startsWith("capacityProvider") ? { capacityProvider: true, deployByDefault: false } : {},
417
- environments: {
418
- local: {
419
- hostId: serviceType === "relational-database" || serviceType === "runner-pool" || service.railway?.volumeMountPath ? "local-docker" : "local-process",
420
- projectGroupId: void 0,
421
- config: service.environments?.local ?? {}
422
- },
423
- staging: {
424
- hostId: service.provider ?? "railway",
425
- projectGroupId: defaultProjectGroup,
426
- config: service.environments?.staging ?? {}
396
+ const environmentConfig = service.environments?.[input.environment];
397
+ const effectiveServiceName = typeof environmentConfig?.serviceName === "string" && environmentConfig.serviceName.trim() ? environmentConfig.serviceName.trim() : (serviceKey === "operationsRunner" || isApiRailwaySourcePolicyService({ key: serviceKey, serviceName: baseServiceName })) && baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, input.environment) : baseServiceName;
398
+ const runnerPool = serviceKey === "operationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
399
+ const instanceCount = runnerPool ? Math.max(1, Number.parseInt(String(runnerPool.bootstrapCount ?? 1), 10) || 1) : 1;
400
+ const maxRunners = runnerPool ? Math.max(1, Number.parseInt(String(runnerPool.maxRunners ?? instanceCount), 10) || instanceCount) : 1;
401
+ if (runnerPool && instanceCount > maxRunners) {
402
+ throw new Error(`services.operationsRunner.railway.runnerPool.bootstrapCount (${instanceCount}) cannot exceed maxRunners (${maxRunners}).`);
403
+ }
404
+ for (let offset = 0; offset < instanceCount; offset += 1) {
405
+ const runnerIndex = offset + 1;
406
+ const instanceId = serviceKey === "operationsRunner" && runnerIndex > 1 ? `${serviceKey}-${String(runnerIndex).padStart(2, "0")}` : serviceKey;
407
+ const instanceServiceName = serviceKey === "operationsRunner" && effectiveServiceName ? indexedName(effectiveServiceName, runnerIndex) : effectiveServiceName;
408
+ const volumeMountPath = serviceKey === "operationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : service.railway?.volumeMountPath ?? null;
409
+ if (instanceServiceName && (service.provider === "railway" || service.railway)) {
410
+ assertRailwayResourceNames(instanceServiceName, volumeMountPath ? `${instanceServiceName}-volume` : null);
411
+ }
412
+ const environmentBinding = (bindingEnvironment) => {
413
+ const configured = service.environments?.[bindingEnvironment] ?? {};
414
+ if (!runnerPool) return configured;
415
+ const configuredName = typeof configured.serviceName === "string" && configured.serviceName.trim() ? configured.serviceName.trim() : baseServiceName ? railwayEnvironmentQualifiedServiceName(baseServiceName, bindingEnvironment) : instanceServiceName;
416
+ const serviceName = configuredName ? indexedName(configuredName, runnerIndex) : instanceServiceName;
417
+ return { ...configured, serviceName, railwayServiceName: serviceName };
418
+ };
419
+ services.push({
420
+ id: instanceId,
421
+ label: placement === "runner-capacity" ? `Runner Capacity ${String(runnerIndex).padStart(2, "0")}` : serviceKey === "api" ? "API Runtime" : serviceKey,
422
+ serviceType,
423
+ placement,
424
+ projectGroupId: defaultProjectGroup,
425
+ config: {
426
+ rootDir: service.railway?.rootDir ?? service.rootDir ?? ".",
427
+ imageRef: sourcePolicy.imageRef,
428
+ imageRefEnv: sourcePolicy.sourceMode === "image" ? imageRefEnv : null,
429
+ sourceMode: sourcePolicy.sourceMode,
430
+ sourceRepo: sourcePolicy.sourceRepo,
431
+ sourceBranch: sourcePolicy.sourceBranch,
432
+ sourceCommit: sourcePolicy.sourceCommit,
433
+ sourceRootDirectory: sourcePolicy.sourceRootDirectory,
434
+ dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
435
+ buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
436
+ startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
437
+ healthcheckPath: service.railway?.healthcheckPath ?? null,
438
+ runtimeMode: service.railway?.runtimeMode ?? null,
439
+ volumeMountPath,
440
+ volumeName: volumeMountPath && instanceServiceName ? `${instanceServiceName}-volume` : null,
441
+ runnerPool: runnerPool ? { ...runnerPool, bootstrapCount: instanceCount, maxRunners } : null,
442
+ runnerIndex: runnerPool ? runnerIndex : null,
443
+ poolKey: runnerPool ? serviceKey : null,
444
+ runnerId: runnerPool ? instanceServiceName : null,
445
+ resourceType: service.railway?.resourceType ?? null,
446
+ serviceName: instanceServiceName,
447
+ serviceTargets: service.railway?.serviceTargets ?? null
427
448
  },
428
- prod: {
429
- hostId: service.provider ?? "railway",
430
- projectGroupId: defaultProjectGroup,
431
- config: service.environments?.prod ?? {}
449
+ secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
450
+ variableRefs: serviceKey === "operationsRunner" ? ["TREESEED_PLATFORM_RUNNER_ID", "TREESEED_PLATFORM_RUNNER_DATA_DIR", "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"] : [],
451
+ metadata: String(serviceKey).startsWith("capacityProvider") ? { capacityProvider: true, deployByDefault: false } : runnerPool ? { poolKey: serviceKey, runnerIndex } : {},
452
+ environments: {
453
+ local: {
454
+ hostId: serviceType === "relational-database" || serviceType === "runner-pool" || service.railway?.volumeMountPath ? "local-docker" : "local-process",
455
+ projectGroupId: void 0,
456
+ config: environmentBinding("local")
457
+ },
458
+ staging: {
459
+ hostId: service.provider ?? "railway",
460
+ projectGroupId: defaultProjectGroup,
461
+ config: environmentBinding("staging")
462
+ },
463
+ prod: {
464
+ hostId: service.provider ?? "railway",
465
+ projectGroupId: defaultProjectGroup,
466
+ config: environmentBinding("prod")
467
+ }
432
468
  }
433
- }
434
- });
469
+ });
470
+ }
435
471
  }
436
472
  if (config.cloudflare?.r2) {
437
473
  services.push({
@@ -470,9 +506,11 @@ function buildProfileFromDeployConfig(input) {
470
506
  const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
471
507
  const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
472
508
  const nodeIndex = offset + 1;
473
- const serviceName = indexedName("public-treedx-node", nodeIndex);
509
+ const logicalServiceName = indexedName("public-treedx-node", nodeIndex);
510
+ const serviceName = railwayTreeDxServiceName(nodeIndex, input.environment);
511
+ assertRailwayResourceNames(serviceName, `${serviceName}-volume`);
474
512
  return {
475
- id: serviceName,
513
+ id: logicalServiceName,
476
514
  label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
477
515
  serviceType: "treedx-node",
478
516
  placement: "knowledge-library",
@@ -658,7 +696,7 @@ function filterHostingUnits(units, filter) {
658
696
  const serviceIds = normalizeFilterValues(filter?.serviceIds);
659
697
  const placements = normalizeFilterValues(filter?.placements);
660
698
  const hosts = normalizeFilterValues(filter?.hosts);
661
- const allServiceIds = new Set(units.map((unit) => unit.id));
699
+ const allServiceIds = new Set(units.flatMap((unit) => [unit.id, typeof unit.config.poolKey === "string" ? unit.config.poolKey : null]).filter((value) => Boolean(value)));
662
700
  const missingServices = [...serviceIds].filter((serviceId) => !allServiceIds.has(serviceId));
663
701
  if (missingServices.length > 0) {
664
702
  throw new Error(`Unknown hosting service id${missingServices.length === 1 ? "" : "s"}: ${missingServices.join(", ")}.`);
@@ -666,7 +704,7 @@ function filterHostingUnits(units, filter) {
666
704
  if (serviceIds.size === 0 && placements.size === 0 && hosts.size === 0) {
667
705
  return units.filter((unit) => unit.metadata.deployByDefault !== false);
668
706
  }
669
- return units.filter((unit) => (serviceIds.size === 0 || serviceIds.has(unit.id)) && (placements.size === 0 || placements.has(unit.placement)) && (hosts.size === 0 || hosts.has(unit.host.id)));
707
+ return units.filter((unit) => (serviceIds.size === 0 || serviceIds.has(unit.id) || typeof unit.config.poolKey === "string" && serviceIds.has(unit.config.poolKey)) && (placements.size === 0 || placements.has(unit.placement)) && (hosts.size === 0 || hosts.has(unit.host.id)));
670
708
  }
671
709
  function compileSingleTreeseedHostingGraph(input, application) {
672
710
  const environment = normalizeEnvironment(input.environment);
@@ -796,112 +834,6 @@ function railwayReconcileSystemsForUnits(units) {
796
834
  }
797
835
  return [...systems];
798
836
  }
799
- function isInfrastructureHostedUnit(unit) {
800
- return ["railway", "cloudflare", "cloudflare-dns", "local-docker"].includes(unit.host.id);
801
- }
802
- function railwayEnvForHostingApply(input, graph) {
803
- const seedValues = collectTreeseedConfigSeedValues(input.tenantRoot, graph.environment);
804
- return {
805
- ...process.env,
806
- ...seedValues
807
- };
808
- }
809
- async function applyTreeseedHostingGraph(input) {
810
- const plan = await planTreeseedHostingGraph(input);
811
- const graph = compileTreeseedHostingGraph(input);
812
- const selectedSystems = railwayReconcileSystemsForUnits(graph.units);
813
- const infrastructureUnits = graph.units.filter(isInfrastructureHostedUnit);
814
- if (plan.planOnly) {
815
- return {
816
- environment: plan.environment,
817
- planOnly: true,
818
- selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
819
- selectedSystems,
820
- skippedSystems: ["web", "data", "github"].filter((system) => !selectedSystems.includes(system)).map((system) => ({ system, reason: selectedSystems.length > 0 ? "Not selected by hosting app filter." : "No Railway reconciliation selected." })),
821
- results: plan.units.map((entry) => ({
822
- unit: entry.unit,
823
- plan: entry.plan,
824
- result: entry.observed,
825
- verification: entry.verification
826
- })),
827
- placements: plan.placements,
828
- warnings: plan.warnings
829
- };
830
- }
831
- if (infrastructureUnits.length > 0 && selectedSystems.length === 0) {
832
- throw new Error(`Hosting apply selected infrastructure resources but no provider reconciliation system was selected: ${infrastructureUnits.map((unit) => `${unit.id} (${unit.host.id})`).join(", ")}.`);
833
- }
834
- const reconcile = await reconcileTreeseedTarget({
835
- tenantRoot: graph.tenantRoot,
836
- target: createPersistentDeployTarget(graph.environment),
837
- systems: selectedSystems.length > 0 ? selectedSystems : void 0,
838
- env: railwayEnvForHostingApply(input, graph),
839
- planOnly: plan.planOnly
840
- });
841
- const resultByUnit = /* @__PURE__ */ new Map();
842
- for (const entry of reconcile.results) {
843
- resultByUnit.set(entry.unit.unitId, entry);
844
- resultByUnit.set(entry.unit.logicalName, entry);
845
- }
846
- const results = plan.units.map((entry) => {
847
- const unit = graph.units.find((candidate) => candidate.id === entry.unit.id) ?? entry.unit;
848
- const reconcileResult = resultByUnit.get(entry.unit.id) ?? resultByUnit.get(unit.id) ?? resultByUnit.get(unit.logicalName) ?? null;
849
- return {
850
- unit,
851
- plan: entry.plan,
852
- result: {
853
- status: reconcileResult?.verification?.ready || plan.planOnly ? "ready" : "blocked",
854
- locators: reconcileResult?.resourceLocators ?? {},
855
- state: reconcileResult?.state ?? {
856
- unitId: unit.id,
857
- action: plan.planOnly ? "plan" : "unmatched"
858
- },
859
- warnings: reconcileResult?.warnings ?? []
860
- },
861
- verification: reconcileResult?.verification ? {
862
- unitId: unit.id,
863
- status: reconcileResult.verification.verified ? "ready" : "blocked",
864
- verified: reconcileResult.verification.verified,
865
- checks: reconcileResult.verification.checks.map((check) => ({
866
- key: check.key,
867
- label: check.description,
868
- ok: check.verified,
869
- expected: check.expected,
870
- observed: check.observed,
871
- issues: check.issues
872
- })),
873
- warnings: reconcileResult.verification.warnings
874
- } : {
875
- unitId: unit.id,
876
- status: plan.planOnly ? "ready" : "blocked",
877
- verified: plan.planOnly,
878
- checks: plan.planOnly ? [] : [{
879
- key: "reconcile-result",
880
- label: "Hosting unit matched a reconcile result",
881
- ok: false,
882
- issues: [`No reconcile result matched hosting unit ${unit.id}.`]
883
- }],
884
- warnings: []
885
- }
886
- };
887
- });
888
- return {
889
- environment: plan.environment,
890
- planOnly: plan.planOnly,
891
- selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
892
- selectedSystems,
893
- skippedSystems: ["web", "data", "github"].filter((system) => !selectedSystems.includes(system)).map((system) => ({ system, reason: selectedSystems.length > 0 ? "Not selected by hosting app filter." : "No Railway reconciliation selected." })),
894
- transport: selectedSystems.length > 0 ? {
895
- railway: {
896
- reconcile: "api",
897
- deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
898
- }
899
- } : void 0,
900
- results,
901
- placements: plan.placements,
902
- warnings: plan.warnings
903
- };
904
- }
905
837
  function serializeHostingUnit(unit) {
906
838
  return sanitizedUnitConfig(unit);
907
839
  }
@@ -1017,58 +949,6 @@ function canonicalHostingReportFromPlan(plan) {
1017
949
  }
1018
950
  });
1019
951
  }
1020
- function canonicalHostingReportFromApplyResult(result) {
1021
- const desiredGraph = result.results.map((entry) => canonicalHostingNode(entry.unit));
1022
- const observedGraph = result.results.map((entry) => canonicalHostingNode(entry.unit, entry.result));
1023
- const diff = result.results.flatMap((entry) => [
1024
- ...entry.plan.action && entry.plan.action !== "noop" ? [{
1025
- id: `${entry.unit.id}:diff`,
1026
- resourceId: entry.unit.id,
1027
- severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
1028
- reason: hostingPlanReason(entry.plan, "Applied"),
1029
- provider: entry.unit.host.id,
1030
- type: entry.unit.serviceType.id,
1031
- expected: serializeHostingUnit(entry.unit),
1032
- observed: entry.result
1033
- }] : [],
1034
- ...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
1035
- ]);
1036
- const providerLimitations = result.results.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
1037
- const actions = result.results.map((entry) => ({
1038
- id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
1039
- kind: canonicalActionKind(entry.plan.action),
1040
- resourceId: entry.unit.id,
1041
- reason: hostingPlanReason(entry.plan, "Applied"),
1042
- provider: entry.unit.host.id,
1043
- type: entry.unit.serviceType.id,
1044
- before: entry.result,
1045
- after: serializeHostingUnit(entry.unit)
1046
- }));
1047
- return createTreeseedCanonicalReconcileReport({
1048
- desiredGraph,
1049
- observedGraph,
1050
- stateGraph: [],
1051
- diff,
1052
- actions,
1053
- postconditions: result.results.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
1054
- selectedResources: result.results.map((entry) => entry.unit.id),
1055
- skippedResources: result.skippedSystems.map((entry) => ({ id: entry.system, reason: entry.reason })),
1056
- blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
1057
- providerLimitations,
1058
- retainedResources: result.results.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
1059
- id: `${entry.unit.id}:retained:${index + 1}`,
1060
- provider: entry.unit.host.id,
1061
- type: "retained-resource",
1062
- owner: entry.unit.application?.id ?? null,
1063
- state: resource
1064
- }))),
1065
- liveVerification: {
1066
- ok: result.results.every((entry) => entry.verification.verified === true),
1067
- source: "hosting-apply",
1068
- issues: result.results.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass after apply`)
1069
- }
1070
- });
1071
- }
1072
952
  function serializeHostingPlan(plan) {
1073
953
  const selectedSystems = railwayReconcileSystemsForUnits(plan.units.map((entry) => entry.unit));
1074
954
  const canonical = canonicalHostingReportFromPlan(plan);
@@ -1101,42 +981,13 @@ function serializeHostingPlan(plan) {
1101
981
  warnings: plan.warnings
1102
982
  };
1103
983
  }
1104
- function serializeHostingApplyResult(result) {
1105
- const canonical = canonicalHostingReportFromApplyResult(result);
1106
- return {
1107
- environment: result.environment,
1108
- planOnly: result.planOnly,
1109
- ...canonical,
1110
- selectedApps: result.selectedApps ?? [],
1111
- selectedSystems: result.selectedSystems ?? [],
1112
- skippedSystems: result.skippedSystems ?? [],
1113
- transport: result.transport,
1114
- placements: result.placements,
1115
- results: result.results.map((entry) => ({
1116
- unit: serializeHostingUnit(entry.unit),
1117
- desired: serializeHostingUnit(entry.unit),
1118
- observed: entry.result,
1119
- diff: entry.plan,
1120
- actions: entry.plan.actions ?? [entry.plan.action],
1121
- retainedResources: entry.plan.retainedResources ?? [],
1122
- blockedDrift: entry.plan.blockedDrift ?? [],
1123
- providerLimitations: entry.plan.providerLimitations ?? [],
1124
- plan: entry.plan,
1125
- result: entry.result,
1126
- verification: entry.verification
1127
- })),
1128
- warnings: result.warnings
1129
- };
1130
- }
1131
984
  function hostingEnvironmentLabel(environment) {
1132
985
  return ENVIRONMENT_NAMES[environment];
1133
986
  }
1134
987
  export {
1135
- applyTreeseedHostingGraph,
1136
988
  compileTreeseedHostingGraph,
1137
989
  hostingEnvironmentLabel,
1138
990
  planTreeseedHostingGraph,
1139
- serializeHostingApplyResult,
1140
991
  serializeHostingPlan,
1141
992
  serializeHostingUnit
1142
993
  };
@@ -31,6 +31,31 @@ async function waitForPidsToExit(pids, timeoutMs = 3e3) {
31
31
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
32
32
  }
33
33
  }
34
+ function processGroupAlive(pid) {
35
+ if (process.platform === "win32") return pidAlive(pid);
36
+ try {
37
+ process.kill(-pid, 0);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function terminateManagedProcess(pid) {
44
+ const signal = (value) => {
45
+ try {
46
+ process.kill(process.platform === "win32" ? pid : -pid, value);
47
+ } catch {
48
+ }
49
+ };
50
+ signal("SIGTERM");
51
+ const startedAt = Date.now();
52
+ while (processGroupAlive(pid) && Date.now() - startedAt < 3e3) {
53
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
54
+ }
55
+ if (processGroupAlive(pid)) {
56
+ signal("SIGKILL");
57
+ }
58
+ }
34
59
  async function stopConflictingPortListeners(spec, allowedPid = null) {
35
60
  const conflicts = portListenerPids(spec.port).filter((pid) => pid !== process.pid && pid !== allowedPid);
36
61
  if (conflicts.length === 0) return [];
@@ -249,7 +274,7 @@ function instanceFromSpec(spec) {
249
274
  return {
250
275
  ...spec,
251
276
  pid: Number.isFinite(pid) ? pid : null,
252
- running: pidAlive(Number.isFinite(pid) ? pid : null),
277
+ running: Number.isFinite(pid) ? processGroupAlive(pid) : false,
253
278
  startedAt: typeof record?.startedAt === "string" ? record.startedAt : null
254
279
  };
255
280
  }
@@ -296,13 +321,10 @@ function listTreeseedDevInstances(input = {}) {
296
321
  if (!existsSync(instanceDir)) return [];
297
322
  return readdirSync(instanceDir).filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -".json".length)).map((surface) => readTreeseedDevInstance({ cwd: input.cwd, surface })).filter((entry) => Boolean(entry));
298
323
  }
299
- function stopSpec(spec) {
324
+ async function stopSpec(spec) {
300
325
  const instance = instanceFromSpec(spec);
301
326
  if (instance.pid && instance.running) {
302
- try {
303
- process.kill(instance.pid, "SIGTERM");
304
- } catch {
305
- }
327
+ await terminateManagedProcess(instance.pid);
306
328
  }
307
329
  rmSync(spec.pidPath, { force: true });
308
330
  rmSync(spec.instancePath, { force: true });
@@ -314,7 +336,7 @@ async function startSpec(spec, force = false, forceConflicts = false) {
314
336
  return existing;
315
337
  }
316
338
  if (existing.running && force) {
317
- stopSpec(spec);
339
+ await stopSpec(spec);
318
340
  }
319
341
  if (forceConflicts) {
320
342
  await stopConflictingPortListeners(spec, existing.pid);
@@ -359,7 +381,7 @@ async function startTreeseedManagedDev(options = {}) {
359
381
  }
360
382
  async function stopTreeseedManagedDev(options = {}) {
361
383
  const plan = createTreeseedIntegratedDevPlan(options);
362
- const instances = plan.processes.map((spec) => stopSpec(spec));
384
+ const instances = await Promise.all(plan.processes.map((spec) => stopSpec(spec)));
363
385
  return { ok: true, action: "stop", plan, instances };
364
386
  }
365
387
  function stopTreeseedDevInstance(options = {}) {
@@ -70,6 +70,9 @@ export declare function createTreeseedManagedToolEnv(env?: NodeJS.ProcessEnv): {
70
70
  DOCKERHUB_USERNAME?: string;
71
71
  CODEX_API_KEY?: string;
72
72
  };
73
+ export declare function collectInstalledNativeDependencyIssues(tenantRoot: string): string[];
74
+ export declare function repairInstalledNativeDependencies(tenantRoot: string, options: Required<Pick<DependencyInstallerOptions, 'env' | 'spawn'>> & Pick<DependencyInstallerOptions, 'write'>): number | null;
75
+ export declare function staleNpmGitClonePath(detail: string): string | null;
73
76
  export declare function formatTreeseedDependencyFailureDetails(result: Pick<TreeseedDependencyInstallResult, 'npmInstalls' | 'reports'>): string;
74
77
  export declare function resolveTreeseedToolBinary(toolName: TreeseedManagedToolName, options?: {
75
78
  env?: NodeJS.ProcessEnv;