@treeseed/sdk 0.12.59 → 0.12.61

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 -247
  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 +6 -6
  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 +56 -14
  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 +5 -0
  22. package/dist/operations/services/railway-deploy.js +47 -75
  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 +524 -680
  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 +148 -0
  35. package/dist/reconcile/providers/railway-iac.js +294 -18
  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 +265 -90
  40. package/dist/workflow/runs.d.ts +4 -0
  41. package/dist/workflow/runs.js +25 -0
  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 = input.environment === "prod" ? "public-treedx-node-production-01" : "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,8 +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 configuredEnvironmentServiceName = service.environments?.[input.environment]?.railwayServiceName;
180
- const serviceName = typeof configuredEnvironmentServiceName === "string" && configuredEnvironmentServiceName.trim() ? configuredEnvironmentServiceName.trim() : input.environment === "prod" && ["api", "operationsRunner"].includes(serviceKey) && baseServiceName ? `${baseServiceName}-production` : 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;
181
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 });
182
190
  const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
183
191
  const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
@@ -384,56 +392,82 @@ function buildProfileFromDeployConfig(input) {
384
392
  const imageRefEnv = railwayImageRefEnvForService(serviceKey);
385
393
  const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
386
394
  const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
387
- const environmentConfig = service.environments?.[input.environment];
388
395
  const baseServiceName = service.railway?.serviceName ?? null;
389
- const effectiveServiceName = environmentConfig?.railwayServiceName ?? (input.environment === "prod" && ["api", "operationsRunner"].includes(serviceKey) && baseServiceName ? `${baseServiceName}-production` : baseServiceName);
390
- services.push({
391
- id: serviceKey,
392
- label: placement === "runner-capacity" ? "Runner Capacity" : serviceKey === "api" ? "API Runtime" : serviceKey,
393
- serviceType,
394
- placement,
395
- projectGroupId: defaultProjectGroup,
396
- config: {
397
- rootDir: service.railway?.rootDir ?? service.rootDir ?? ".",
398
- imageRef: sourcePolicy.imageRef,
399
- imageRefEnv: sourcePolicy.sourceMode === "image" ? imageRefEnv : null,
400
- sourceMode: sourcePolicy.sourceMode,
401
- sourceRepo: sourcePolicy.sourceRepo,
402
- sourceBranch: sourcePolicy.sourceBranch,
403
- sourceCommit: sourcePolicy.sourceCommit,
404
- sourceRootDirectory: sourcePolicy.sourceRootDirectory,
405
- dockerfilePath: sourcePolicy.sourceMode === "git" ? service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName: service.railway?.serviceName ?? null }) : null,
406
- buildCommand: sourcePolicy.imageRef || sourcePolicy.sourceMode === "git" && service.railway?.dockerfilePath ? null : service.railway?.buildCommand ?? null,
407
- startCommand: sourcePolicy.imageRef ? null : service.railway?.startCommand ?? null,
408
- healthcheckPath: service.railway?.healthcheckPath ?? null,
409
- runtimeMode: service.railway?.runtimeMode ?? null,
410
- volumeMountPath: service.railway?.volumeMountPath ?? null,
411
- runnerPool: service.railway?.runnerPool ?? null,
412
- resourceType: service.railway?.resourceType ?? null,
413
- serviceName: effectiveServiceName,
414
- serviceTargets: service.railway?.serviceTargets ?? null
415
- },
416
- secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
417
- variableRefs: serviceKey === "operationsRunner" ? ["TREESEED_PLATFORM_RUNNER_ID", "TREESEED_PLATFORM_RUNNER_DATA_DIR", "TREESEED_PLATFORM_RUNNER_ENVIRONMENT"] : [],
418
- metadata: String(serviceKey).startsWith("capacityProvider") ? { capacityProvider: true, deployByDefault: false } : {},
419
- environments: {
420
- local: {
421
- hostId: serviceType === "relational-database" || serviceType === "runner-pool" || service.railway?.volumeMountPath ? "local-docker" : "local-process",
422
- projectGroupId: void 0,
423
- config: service.environments?.local ?? {}
424
- },
425
- staging: {
426
- hostId: service.provider ?? "railway",
427
- projectGroupId: defaultProjectGroup,
428
- 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
429
448
  },
430
- prod: {
431
- hostId: service.provider ?? "railway",
432
- projectGroupId: defaultProjectGroup,
433
- 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
+ }
434
468
  }
435
- }
436
- });
469
+ });
470
+ }
437
471
  }
438
472
  if (config.cloudflare?.r2) {
439
473
  services.push({
@@ -472,9 +506,11 @@ function buildProfileFromDeployConfig(input) {
472
506
  const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
473
507
  const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
474
508
  const nodeIndex = offset + 1;
475
- const serviceName = indexedName(input.environment === "prod" ? "public-treedx-node-production" : "public-treedx-node", nodeIndex);
509
+ const logicalServiceName = indexedName("public-treedx-node", nodeIndex);
510
+ const serviceName = railwayTreeDxServiceName(nodeIndex, input.environment);
511
+ assertRailwayResourceNames(serviceName, `${serviceName}-volume`);
476
512
  return {
477
- id: serviceName,
513
+ id: logicalServiceName,
478
514
  label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
479
515
  serviceType: "treedx-node",
480
516
  placement: "knowledge-library",
@@ -660,7 +696,7 @@ function filterHostingUnits(units, filter) {
660
696
  const serviceIds = normalizeFilterValues(filter?.serviceIds);
661
697
  const placements = normalizeFilterValues(filter?.placements);
662
698
  const hosts = normalizeFilterValues(filter?.hosts);
663
- 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)));
664
700
  const missingServices = [...serviceIds].filter((serviceId) => !allServiceIds.has(serviceId));
665
701
  if (missingServices.length > 0) {
666
702
  throw new Error(`Unknown hosting service id${missingServices.length === 1 ? "" : "s"}: ${missingServices.join(", ")}.`);
@@ -668,7 +704,7 @@ function filterHostingUnits(units, filter) {
668
704
  if (serviceIds.size === 0 && placements.size === 0 && hosts.size === 0) {
669
705
  return units.filter((unit) => unit.metadata.deployByDefault !== false);
670
706
  }
671
- 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)));
672
708
  }
673
709
  function compileSingleTreeseedHostingGraph(input, application) {
674
710
  const environment = normalizeEnvironment(input.environment);
@@ -798,112 +834,6 @@ function railwayReconcileSystemsForUnits(units) {
798
834
  }
799
835
  return [...systems];
800
836
  }
801
- function isInfrastructureHostedUnit(unit) {
802
- return ["railway", "cloudflare", "cloudflare-dns", "local-docker"].includes(unit.host.id);
803
- }
804
- function railwayEnvForHostingApply(input, graph) {
805
- const seedValues = collectTreeseedConfigSeedValues(input.tenantRoot, graph.environment);
806
- return {
807
- ...process.env,
808
- ...seedValues
809
- };
810
- }
811
- async function applyTreeseedHostingGraph(input) {
812
- const plan = await planTreeseedHostingGraph(input);
813
- const graph = compileTreeseedHostingGraph(input);
814
- const selectedSystems = railwayReconcileSystemsForUnits(graph.units);
815
- const infrastructureUnits = graph.units.filter(isInfrastructureHostedUnit);
816
- if (plan.planOnly) {
817
- return {
818
- environment: plan.environment,
819
- planOnly: true,
820
- selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
821
- selectedSystems,
822
- 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." })),
823
- results: plan.units.map((entry) => ({
824
- unit: entry.unit,
825
- plan: entry.plan,
826
- result: entry.observed,
827
- verification: entry.verification
828
- })),
829
- placements: plan.placements,
830
- warnings: plan.warnings
831
- };
832
- }
833
- if (infrastructureUnits.length > 0 && selectedSystems.length === 0) {
834
- throw new Error(`Hosting apply selected infrastructure resources but no provider reconciliation system was selected: ${infrastructureUnits.map((unit) => `${unit.id} (${unit.host.id})`).join(", ")}.`);
835
- }
836
- const reconcile = await reconcileTreeseedTarget({
837
- tenantRoot: graph.tenantRoot,
838
- target: createPersistentDeployTarget(graph.environment),
839
- systems: selectedSystems.length > 0 ? selectedSystems : void 0,
840
- env: railwayEnvForHostingApply(input, graph),
841
- planOnly: plan.planOnly
842
- });
843
- const resultByUnit = /* @__PURE__ */ new Map();
844
- for (const entry of reconcile.results) {
845
- resultByUnit.set(entry.unit.unitId, entry);
846
- resultByUnit.set(entry.unit.logicalName, entry);
847
- }
848
- const results = plan.units.map((entry) => {
849
- const unit = graph.units.find((candidate) => candidate.id === entry.unit.id) ?? entry.unit;
850
- const reconcileResult = resultByUnit.get(entry.unit.id) ?? resultByUnit.get(unit.id) ?? resultByUnit.get(unit.logicalName) ?? null;
851
- return {
852
- unit,
853
- plan: entry.plan,
854
- result: {
855
- status: reconcileResult?.verification?.ready || plan.planOnly ? "ready" : "blocked",
856
- locators: reconcileResult?.resourceLocators ?? {},
857
- state: reconcileResult?.state ?? {
858
- unitId: unit.id,
859
- action: plan.planOnly ? "plan" : "unmatched"
860
- },
861
- warnings: reconcileResult?.warnings ?? []
862
- },
863
- verification: reconcileResult?.verification ? {
864
- unitId: unit.id,
865
- status: reconcileResult.verification.verified ? "ready" : "blocked",
866
- verified: reconcileResult.verification.verified,
867
- checks: reconcileResult.verification.checks.map((check) => ({
868
- key: check.key,
869
- label: check.description,
870
- ok: check.verified,
871
- expected: check.expected,
872
- observed: check.observed,
873
- issues: check.issues
874
- })),
875
- warnings: reconcileResult.verification.warnings
876
- } : {
877
- unitId: unit.id,
878
- status: plan.planOnly ? "ready" : "blocked",
879
- verified: plan.planOnly,
880
- checks: plan.planOnly ? [] : [{
881
- key: "reconcile-result",
882
- label: "Hosting unit matched a reconcile result",
883
- ok: false,
884
- issues: [`No reconcile result matched hosting unit ${unit.id}.`]
885
- }],
886
- warnings: []
887
- }
888
- };
889
- });
890
- return {
891
- environment: plan.environment,
892
- planOnly: plan.planOnly,
893
- selectedApps: [...new Set(graph.units.map((unit) => unit.application?.id).filter((value) => Boolean(value)))],
894
- selectedSystems,
895
- 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." })),
896
- transport: selectedSystems.length > 0 ? {
897
- railway: {
898
- reconcile: "api",
899
- deploy: process.env.TREESEED_RAILWAY_DEPLOY_TRANSPORT === "cli-fallback" ? "cli-fallback" : "api"
900
- }
901
- } : void 0,
902
- results,
903
- placements: plan.placements,
904
- warnings: plan.warnings
905
- };
906
- }
907
837
  function serializeHostingUnit(unit) {
908
838
  return sanitizedUnitConfig(unit);
909
839
  }
@@ -1019,58 +949,6 @@ function canonicalHostingReportFromPlan(plan) {
1019
949
  }
1020
950
  });
1021
951
  }
1022
- function canonicalHostingReportFromApplyResult(result) {
1023
- const desiredGraph = result.results.map((entry) => canonicalHostingNode(entry.unit));
1024
- const observedGraph = result.results.map((entry) => canonicalHostingNode(entry.unit, entry.result));
1025
- const diff = result.results.flatMap((entry) => [
1026
- ...entry.plan.action && entry.plan.action !== "noop" ? [{
1027
- id: `${entry.unit.id}:diff`,
1028
- resourceId: entry.unit.id,
1029
- severity: canonicalActionKind(entry.plan.action) === "blocked" ? "blocking" : "info",
1030
- reason: hostingPlanReason(entry.plan, "Applied"),
1031
- provider: entry.unit.host.id,
1032
- type: entry.unit.serviceType.id,
1033
- expected: serializeHostingUnit(entry.unit),
1034
- observed: entry.result
1035
- }] : [],
1036
- ...canonicalHostingDrift(entry.unit, entry.plan.blockedDrift, "Blocked provider drift.")
1037
- ]);
1038
- const providerLimitations = result.results.flatMap((entry) => canonicalHostingDrift(entry.unit, entry.plan.providerLimitations, "Provider limitation."));
1039
- const actions = result.results.map((entry) => ({
1040
- id: `${entry.unit.id}:${entry.plan.action ?? "noop"}`,
1041
- kind: canonicalActionKind(entry.plan.action),
1042
- resourceId: entry.unit.id,
1043
- reason: hostingPlanReason(entry.plan, "Applied"),
1044
- provider: entry.unit.host.id,
1045
- type: entry.unit.serviceType.id,
1046
- before: entry.result,
1047
- after: serializeHostingUnit(entry.unit)
1048
- }));
1049
- return createTreeseedCanonicalReconcileReport({
1050
- desiredGraph,
1051
- observedGraph,
1052
- stateGraph: [],
1053
- diff,
1054
- actions,
1055
- postconditions: result.results.map((entry) => canonicalHostingPostcondition(entry.unit, entry.verification)),
1056
- selectedResources: result.results.map((entry) => entry.unit.id),
1057
- skippedResources: result.skippedSystems.map((entry) => ({ id: entry.system, reason: entry.reason })),
1058
- blockedDrift: diff.filter((entry) => entry.severity === "blocking"),
1059
- providerLimitations,
1060
- retainedResources: result.results.flatMap((entry) => (entry.plan.retainedResources ?? []).map((resource, index) => ({
1061
- id: `${entry.unit.id}:retained:${index + 1}`,
1062
- provider: entry.unit.host.id,
1063
- type: "retained-resource",
1064
- owner: entry.unit.application?.id ?? null,
1065
- state: resource
1066
- }))),
1067
- liveVerification: {
1068
- ok: result.results.every((entry) => entry.verification.verified === true),
1069
- source: "hosting-apply",
1070
- issues: result.results.filter((entry) => entry.verification.verified !== true).map((entry) => `${entry.unit.id}: verification did not pass after apply`)
1071
- }
1072
- });
1073
- }
1074
952
  function serializeHostingPlan(plan) {
1075
953
  const selectedSystems = railwayReconcileSystemsForUnits(plan.units.map((entry) => entry.unit));
1076
954
  const canonical = canonicalHostingReportFromPlan(plan);
@@ -1103,42 +981,13 @@ function serializeHostingPlan(plan) {
1103
981
  warnings: plan.warnings
1104
982
  };
1105
983
  }
1106
- function serializeHostingApplyResult(result) {
1107
- const canonical = canonicalHostingReportFromApplyResult(result);
1108
- return {
1109
- environment: result.environment,
1110
- planOnly: result.planOnly,
1111
- ...canonical,
1112
- selectedApps: result.selectedApps ?? [],
1113
- selectedSystems: result.selectedSystems ?? [],
1114
- skippedSystems: result.skippedSystems ?? [],
1115
- transport: result.transport,
1116
- placements: result.placements,
1117
- results: result.results.map((entry) => ({
1118
- unit: serializeHostingUnit(entry.unit),
1119
- desired: serializeHostingUnit(entry.unit),
1120
- observed: entry.result,
1121
- diff: entry.plan,
1122
- actions: entry.plan.actions ?? [entry.plan.action],
1123
- retainedResources: entry.plan.retainedResources ?? [],
1124
- blockedDrift: entry.plan.blockedDrift ?? [],
1125
- providerLimitations: entry.plan.providerLimitations ?? [],
1126
- plan: entry.plan,
1127
- result: entry.result,
1128
- verification: entry.verification
1129
- })),
1130
- warnings: result.warnings
1131
- };
1132
- }
1133
984
  function hostingEnvironmentLabel(environment) {
1134
985
  return ENVIRONMENT_NAMES[environment];
1135
986
  }
1136
987
  export {
1137
- applyTreeseedHostingGraph,
1138
988
  compileTreeseedHostingGraph,
1139
989
  hostingEnvironmentLabel,
1140
990
  planTreeseedHostingGraph,
1141
- serializeHostingApplyResult,
1142
991
  serializeHostingPlan,
1143
992
  serializeHostingUnit
1144
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;