@treeseed/sdk 0.10.22 → 0.10.24

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 (47) hide show
  1. package/dist/db/market-schema.js +3 -2
  2. package/dist/market-client.d.ts +4 -0
  3. package/dist/market-client.js +6 -0
  4. package/dist/operations/providers/default.js +26 -4
  5. package/dist/operations/repository-operations.js +6 -2
  6. package/dist/operations/services/bootstrap-runner.d.ts +5 -1
  7. package/dist/operations/services/bootstrap-runner.js +34 -5
  8. package/dist/operations/services/config-runtime.d.ts +2 -1
  9. package/dist/operations/services/deploy.d.ts +18 -1
  10. package/dist/operations/services/deploy.js +176 -24
  11. package/dist/operations/services/github-automation.d.ts +10 -1
  12. package/dist/operations/services/github-automation.js +18 -4
  13. package/dist/operations/services/hosting-audit.d.ts +2 -1
  14. package/dist/operations/services/hosting-audit.js +12 -1
  15. package/dist/operations/services/hub-launch.d.ts +1 -0
  16. package/dist/operations/services/hub-launch.js +1 -0
  17. package/dist/operations/services/hub-provider-launch.d.ts +9 -0
  18. package/dist/operations/services/hub-provider-launch.js +140 -40
  19. package/dist/operations/services/managed-host-security.d.ts +1 -1
  20. package/dist/operations/services/managed-host-security.js +4 -1
  21. package/dist/operations/services/project-platform.d.ts +25 -0
  22. package/dist/operations/services/project-platform.js +91 -23
  23. package/dist/operations/services/railway-api.js +2 -1
  24. package/dist/operations/services/railway-deploy.d.ts +32 -2
  25. package/dist/operations/services/railway-deploy.js +94 -27
  26. package/dist/operations/services/template-registry.js +33 -3
  27. package/dist/platform/contracts.d.ts +1 -0
  28. package/dist/platform/deploy-config.js +8 -1
  29. package/dist/platform/deploy-runtime.js +1 -0
  30. package/dist/platform/environment.d.ts +1 -1
  31. package/dist/platform/environment.js +1 -1
  32. package/dist/reconcile/builtin-adapters.js +155 -25
  33. package/dist/reconcile/contracts.d.ts +1 -1
  34. package/dist/reconcile/desired-state.js +17 -1
  35. package/dist/reconcile/engine.d.ts +2 -0
  36. package/dist/reconcile/engine.js +58 -3
  37. package/dist/reconcile/units.js +1 -0
  38. package/dist/sdk-types.d.ts +1 -1
  39. package/dist/sdk-types.js +2 -0
  40. package/dist/timing.d.ts +20 -0
  41. package/dist/timing.js +73 -0
  42. package/dist/treeseed/template-catalog/catalog.fixture.json +150 -0
  43. package/dist/workflow/operations.d.ts +2 -0
  44. package/drizzle/market/0000_market_control_plane.sql +3 -3
  45. package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
  46. package/package.json +1 -1
  47. package/templates/github/deploy-web.workflow.yml +4 -0
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
3
3
  import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, extname, join, resolve } from "node:path";
6
+ import { elapsedMs, formatTimingMarkdown, formatTimingSummary } from "../../timing.js";
6
7
  import {
7
8
  createControlPlaneReporter
8
9
  } from "../../control-plane.js";
@@ -52,6 +53,39 @@ const PROCESSING_PLATFORM_BOOTSTRAP_SYSTEMS = ["api", "agents"];
52
53
  function stableHash(value) {
53
54
  return createHash("sha256").update(value).digest("hex");
54
55
  }
56
+ function recordTiming(timings, name, startMs, status = "success", metadata) {
57
+ const entry = {
58
+ name,
59
+ durationMs: elapsedMs(startMs),
60
+ status,
61
+ ...metadata ? { metadata } : {}
62
+ };
63
+ timings.push(entry);
64
+ return entry;
65
+ }
66
+ async function timedPhase(timings, name, run, metadata) {
67
+ const startMs = performance.now();
68
+ try {
69
+ const result = await Promise.resolve(run());
70
+ recordTiming(timings, name, startMs, "success", metadata);
71
+ return result;
72
+ } catch (error) {
73
+ recordTiming(timings, name, startMs, "failed", {
74
+ ...metadata ?? {},
75
+ error: error instanceof Error ? error.message : String(error)
76
+ });
77
+ throw error;
78
+ }
79
+ }
80
+ function writeProviderTimingSummary(options, timings) {
81
+ const text = formatTimingSummary(timings);
82
+ options.write?.(text);
83
+ const summaryPath = String(options.env?.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? process.env.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? "").trim();
84
+ if (!summaryPath) {
85
+ return;
86
+ }
87
+ writeFileSync(summaryPath, formatTimingMarkdown(timings), { flag: "a" });
88
+ }
55
89
  function inferEnvironmentFromBranch(tenantRoot) {
56
90
  const branch = currentManagedBranch(tenantRoot);
57
91
  if (branch === STAGING_BRANCH) {
@@ -1024,25 +1058,30 @@ async function publishContent(options, reporter, publishOptions = {}) {
1024
1058
  }
1025
1059
  }
1026
1060
  async function provisionProjectPlatform(options) {
1061
+ const timings = [];
1027
1062
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1028
1063
  const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
1029
1064
  const siteConfig = loadCliDeployConfig(options.tenantRoot);
1030
1065
  const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
1031
1066
  const selectedSystems = new Set(bootstrapSystems);
1032
1067
  const env = { ...process.env, ...options.env ?? {} };
1033
- const summary = await reconcileTreeseedTarget({
1068
+ const summary = await timedPhase(timings, "provision:reconcile", () => reconcileTreeseedTarget({
1034
1069
  tenantRoot: options.tenantRoot,
1035
1070
  target,
1036
1071
  env,
1037
- systems: bootstrapSystems
1038
- });
1039
- const verification = await collectTreeseedReconcileStatus({
1072
+ systems: bootstrapSystems,
1073
+ write: options.write
1074
+ }));
1075
+ timings.push(...summary.timings ?? []);
1076
+ const verification = await timedPhase(timings, "provision:collect-reconcile-status", () => collectTreeseedReconcileStatus({
1040
1077
  tenantRoot: options.tenantRoot,
1041
1078
  target,
1042
1079
  env,
1043
1080
  systems: bootstrapSystems
1081
+ }));
1082
+ await timedPhase(timings, "provision:ensure-wrangler-config", () => {
1083
+ ensureGeneratedWranglerConfig(options.tenantRoot, { target });
1044
1084
  });
1045
- ensureGeneratedWranglerConfig(options.tenantRoot, { target });
1046
1085
  const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
1047
1086
  const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
1048
1087
  const railwaySchedules = [];
@@ -1087,6 +1126,22 @@ async function provisionProjectPlatform(options) {
1087
1126
  locator: state.lastDeployedUrl ?? null,
1088
1127
  metadata: { workerName: state.workerName }
1089
1128
  },
1129
+ {
1130
+ environment: options.scope,
1131
+ provider: "cloudflare",
1132
+ resourceKind: "kv",
1133
+ logicalName: state.kvNamespaces?.FORM_GUARD_KV?.name ?? "form-guard",
1134
+ locator: state.kvNamespaces?.FORM_GUARD_KV?.id ?? null,
1135
+ metadata: state.kvNamespaces?.FORM_GUARD_KV ?? {}
1136
+ },
1137
+ {
1138
+ environment: options.scope,
1139
+ provider: "cloudflare",
1140
+ resourceKind: "turnstile-widget",
1141
+ logicalName: state.turnstileWidgets?.formGuard?.name ?? "form-guard-turnstile",
1142
+ locator: state.turnstileWidgets?.formGuard?.sitekey ?? null,
1143
+ metadata: state.turnstileWidgets?.formGuard ?? {}
1144
+ },
1090
1145
  {
1091
1146
  environment: options.scope,
1092
1147
  provider: "cloudflare",
@@ -1157,6 +1212,7 @@ async function provisionProjectPlatform(options) {
1157
1212
  target: deployTargetLabel(target),
1158
1213
  summary,
1159
1214
  verification,
1215
+ timings,
1160
1216
  reconcileActions: summary.results.map((result) => ({
1161
1217
  unitId: result.unit.unitId,
1162
1218
  action: result.action,
@@ -1174,6 +1230,7 @@ async function provisionProjectPlatform(options) {
1174
1230
  target: deployTargetLabel(target),
1175
1231
  summary,
1176
1232
  verification,
1233
+ timings,
1177
1234
  railway: {
1178
1235
  services: railwayValidation.services.map((service) => service.key),
1179
1236
  schedules: railwaySchedules,
@@ -1182,6 +1239,8 @@ async function provisionProjectPlatform(options) {
1182
1239
  };
1183
1240
  }
1184
1241
  async function deployProjectPlatform(options) {
1242
+ const timings = [];
1243
+ const deployStartMs = performance.now();
1185
1244
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1186
1245
  const commitSha = currentCommit(options.tenantRoot);
1187
1246
  const branchName = currentRef(options.tenantRoot);
@@ -1200,7 +1259,8 @@ async function deployProjectPlatform(options) {
1200
1259
  metadata: { scope: options.scope }
1201
1260
  });
1202
1261
  if (!options.skipProvision) {
1203
- await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
1262
+ const provision = await timedPhase(timings, "deploy:provision", () => provisionProjectPlatform({ ...options, reporter, bootstrapSystems }));
1263
+ timings.push(...provision.timings ?? []);
1204
1264
  }
1205
1265
  const nodes = [];
1206
1266
  let cloudflareContext = null;
@@ -1319,7 +1379,7 @@ async function deployProjectPlatform(options) {
1319
1379
  }
1320
1380
  });
1321
1381
  }
1322
- await runTreeseedBootstrapDag({ nodes, execution });
1382
+ await runTreeseedBootstrapDag({ nodes, execution, write, timings });
1323
1383
  const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
1324
1384
  if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
1325
1385
  finalizeDeploymentState(options.tenantRoot, {
@@ -1342,8 +1402,11 @@ async function deployProjectPlatform(options) {
1342
1402
  scheduleVerification: railwayScheduleVerification
1343
1403
  });
1344
1404
  }
1345
- const monitor = await monitorProjectPlatform({ ...options, reporter, bootstrapSystems });
1346
- const hostingRepair = await repairHostingAfterSuccessfulDeploy(options, bootstrapSystems);
1405
+ const monitor = await timedPhase(timings, "deploy:monitor", () => monitorProjectPlatform({ ...options, reporter, bootstrapSystems }));
1406
+ timings.push(...monitor.timings ?? []);
1407
+ const hostingRepair = await timedPhase(timings, "deploy:hosting-repair", () => repairHostingAfterSuccessfulDeploy(options, bootstrapSystems));
1408
+ recordTiming(timings, "deploy:total", deployStartMs);
1409
+ writeProviderTimingSummary(options, timings);
1347
1410
  await reportDeployment(reporter, {
1348
1411
  environment: options.scope,
1349
1412
  deploymentKind: "code",
@@ -1355,7 +1418,8 @@ async function deployProjectPlatform(options) {
1355
1418
  scope: options.scope,
1356
1419
  railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
1357
1420
  monitor,
1358
- hostingRepair
1421
+ hostingRepair,
1422
+ timings
1359
1423
  },
1360
1424
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1361
1425
  });
@@ -1364,7 +1428,8 @@ async function deployProjectPlatform(options) {
1364
1428
  scope: options.scope,
1365
1429
  monitor,
1366
1430
  hostingRepair,
1367
- serviceResults
1431
+ serviceResults,
1432
+ timings
1368
1433
  };
1369
1434
  }
1370
1435
  function resolveRailwayServiceDeployDependencies({
@@ -1381,6 +1446,7 @@ async function publishProjectContent(options) {
1381
1446
  return publishContent(options, reporter);
1382
1447
  }
1383
1448
  async function monitorProjectPlatform(options) {
1449
+ const timings = [];
1384
1450
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1385
1451
  const env = { ...process.env, ...options.env ?? {} };
1386
1452
  const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
@@ -1392,23 +1458,23 @@ async function monitorProjectPlatform(options) {
1392
1458
  const webProbeUrl = resolveImmediatePagesProbeUrl(siteConfig, state, target);
1393
1459
  const apiBaseUrl = resolveImmediateApiProbeUrl(siteConfig, state, target);
1394
1460
  const apiMonitorEndpoints = resolveApiMonitorEndpoints(siteConfig, apiBaseUrl);
1395
- const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await verifyRailwayManagedResources(options.tenantRoot, options.scope, {
1461
+ const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await timedPhase(timings, "monitor:railway-resources", () => verifyRailwayManagedResources(options.tenantRoot, options.scope, {
1396
1462
  env,
1397
1463
  settleDeployments: true,
1398
1464
  onProgress: options.write
1399
- });
1465
+ }));
1400
1466
  const skippedApiCheck = apiSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "api_not_selected" };
1401
1467
  const skippedAgentCheck = agentsSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "agents_not_selected" };
1402
1468
  const skippedD1Check = apiMonitorEndpoints.processingAgentApi ? { ok: true, skipped: true, reason: "processing_agent_api" } : skippedApiCheck;
1403
1469
  const checks = {
1404
- pages: await probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 }),
1405
- apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1406
- apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1407
- d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 }) : skippedD1Check,
1408
- agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 }) : skippedAgentCheck,
1409
- r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
1410
- queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
1411
- scaleProbe: probeScaleConfiguration(siteConfig, state),
1470
+ pages: await timedPhase(timings, "monitor:probe-pages", () => probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 })),
1471
+ apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await timedPhase(timings, "monitor:probe-api-health", () => probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
1472
+ apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await timedPhase(timings, "monitor:probe-api-ready", () => probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
1473
+ d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await timedPhase(timings, "monitor:probe-d1-health", () => probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 })) : skippedD1Check,
1474
+ agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await timedPhase(timings, "monitor:probe-agent-health", () => probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 })) : skippedAgentCheck,
1475
+ r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : timedPhase(timings, "monitor:probe-r2", () => probeR2(options.tenantRoot, siteConfig, state, target)),
1476
+ queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : timedPhase(timings, "monitor:probe-queue", () => probeQueue(siteConfig, state)),
1477
+ scaleProbe: await timedPhase(timings, "monitor:probe-scale", () => probeScaleConfiguration(siteConfig, state)),
1412
1478
  railwayResources,
1413
1479
  readiness: state.readiness,
1414
1480
  apiMonitor: {
@@ -1448,14 +1514,16 @@ ${failedChecks.join("\n")}`);
1448
1514
  metadata: {
1449
1515
  mode: "monitor",
1450
1516
  target: deployTargetLabel(target),
1451
- checks: resolvedChecks
1517
+ checks: resolvedChecks,
1518
+ timings
1452
1519
  },
1453
1520
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1454
1521
  });
1455
1522
  return {
1456
1523
  ok,
1457
1524
  target: deployTargetLabel(target),
1458
- checks: resolvedChecks
1525
+ checks: resolvedChecks,
1526
+ timings
1459
1527
  };
1460
1528
  }
1461
1529
  async function syncControlPlaneState(options) {
@@ -1295,7 +1295,8 @@ async function ensureRailwayServiceVolume({
1295
1295
  if (!volume) {
1296
1296
  volume = await createReplacementVolume();
1297
1297
  }
1298
- if (volume.name && volume.name !== name) {
1298
+ const desiredNameInUse = volumes.some((candidate) => candidate.id !== volume?.id && candidate.name === name);
1299
+ if (volume.name && volume.name !== name && !desiredNameInUse) {
1299
1300
  try {
1300
1301
  volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1301
1302
  } catch (error) {
@@ -6,9 +6,10 @@ export declare function deriveRailwayMarketOperationsRunnerVolumeName(serviceNam
6
6
  export declare function railwayServiceRuntimeStartCommand(service: any): any;
7
7
  export declare function parseRailwayJsonOutput(output: any): any;
8
8
  export declare function collectRailwayDeploymentStatusChecks(statusPayload: any, scope: any, services: any): any;
9
- export declare function listRailwayServiceVolumesWithCli({ cwd, serviceId, environmentId, name, mountPath, env, }: {
9
+ export declare function listRailwayServiceVolumesWithCli({ cwd, serviceId, serviceName, environmentId, name, mountPath, env, }: {
10
10
  cwd: any;
11
11
  serviceId: any;
12
+ serviceName: any;
12
13
  environmentId: any;
13
14
  name: any;
14
15
  mountPath: any;
@@ -30,18 +31,47 @@ export declare function runRailway(args: any, { cwd, capture, allowFailure, inpu
30
31
  retryAttempts?: number | undefined;
31
32
  retryDelayMs?: number | undefined;
32
33
  }): import("child_process").SpawnSyncReturns<string> | null;
33
- export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, onProgress, }?: {
34
+ export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, fetchImpl, onProgress, }?: {
34
35
  services?: unknown[] | undefined;
35
36
  env?: NodeJS.ProcessEnv | undefined;
36
37
  timeoutMs?: number | undefined;
37
38
  pollMs?: number | undefined;
39
+ fetchImpl?: typeof fetch | undefined;
38
40
  }): Promise<{
41
+ ok: boolean;
42
+ checks: {
43
+ type: string;
44
+ service: any;
45
+ serviceName: any;
46
+ environment: string;
47
+ ok: boolean;
48
+ status: string;
49
+ settle: {
50
+ durationMs: number;
51
+ pollCount: number;
52
+ finalStatus: string;
53
+ };
54
+ message: string;
55
+ }[];
56
+ settle?: undefined;
57
+ message?: undefined;
58
+ } | {
39
59
  ok: boolean;
40
60
  checks: any;
61
+ settle: {
62
+ durationMs: number;
63
+ pollCount: number;
64
+ status: string;
65
+ };
41
66
  message?: undefined;
42
67
  } | {
43
68
  ok: boolean;
44
69
  checks: any;
70
+ settle: {
71
+ durationMs: number;
72
+ pollCount: number;
73
+ status: string;
74
+ };
45
75
  message: string;
46
76
  }>;
47
77
  export declare function setRailwaySecretVariable({ cwd, service, environment, key, value, env, capture, allowFailure }: {
@@ -24,6 +24,7 @@ import {
24
24
  resolveRailwayWorkspaceContext,
25
25
  upsertRailwayVariables
26
26
  } from "./railway-api.js";
27
+ import { elapsedMs, formatDurationMs } from "../../timing.js";
27
28
  function normalizeScope(scope) {
28
29
  return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
29
30
  }
@@ -171,6 +172,26 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
171
172
  };
172
173
  }
173
174
  const deployment = instance.latestDeployment ?? null;
175
+ if (!deployment) {
176
+ return {
177
+ type: "deployment-status",
178
+ service: service.key,
179
+ serviceName: service.serviceName,
180
+ environment: normalizeRailwayEnvironmentName(environment.name),
181
+ ok: true,
182
+ skipped: true,
183
+ status: "no_active_deployment",
184
+ observed: {
185
+ status: null,
186
+ deploymentId: null,
187
+ deploymentCreatedAt: null,
188
+ deploymentStopped: null,
189
+ instanceStatuses: [],
190
+ volumeMounts: []
191
+ },
192
+ message: `Railway service ${service.serviceName} has no active deployment to wait for.`
193
+ };
194
+ }
174
195
  const status = String(deployment?.status ?? "").trim().toUpperCase();
175
196
  const instanceStatuses = Array.isArray(deployment?.instances) ? deployment.instances.map((entry) => String(entry?.status ?? "").trim()).filter(Boolean) : [];
176
197
  const ok = railwayStatusDeploymentSettled(status);
@@ -183,6 +204,8 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
183
204
  status: status || "missing_deployment",
184
205
  observed: {
185
206
  status: status || null,
207
+ deploymentId: deployment?.id ?? null,
208
+ deploymentCreatedAt: deployment?.createdAt ?? null,
186
209
  deploymentStopped: deployment?.deploymentStopped ?? null,
187
210
  instanceStatuses,
188
211
  volumeMounts: Array.isArray(deployment?.meta?.volumeMounts) ? deployment.meta.volumeMounts : []
@@ -191,7 +214,7 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
191
214
  };
192
215
  });
193
216
  }
194
- function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackName, fallbackMountPath }) {
217
+ function normalizeRailwayCliVolume(value, { serviceId, serviceName, environmentId, fallbackName, fallbackMountPath }) {
195
218
  if (!value || typeof value !== "object") {
196
219
  return null;
197
220
  }
@@ -200,6 +223,10 @@ function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackNa
200
223
  if (!id) {
201
224
  return null;
202
225
  }
226
+ const listedServiceName = typeof record.serviceName === "string" && record.serviceName.trim() ? record.serviceName.trim() : "";
227
+ if (listedServiceName && serviceName && listedServiceName !== serviceName) {
228
+ return null;
229
+ }
203
230
  const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : fallbackName;
204
231
  const mountPath = typeof record.mountPath === "string" && record.mountPath.trim() ? record.mountPath.trim() : fallbackMountPath;
205
232
  const sizeMb = typeof record.sizeMB === "number" ? record.sizeMB : null;
@@ -228,6 +255,7 @@ function normalizeRailwayCliVolumeList(value, options) {
228
255
  function listRailwayServiceVolumesWithCli({
229
256
  cwd,
230
257
  serviceId,
258
+ serviceName,
231
259
  environmentId,
232
260
  name,
233
261
  mountPath,
@@ -244,6 +272,7 @@ function listRailwayServiceVolumesWithCli({
244
272
  }
245
273
  return normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
246
274
  serviceId,
275
+ serviceName,
247
276
  environmentId,
248
277
  fallbackName: name,
249
278
  fallbackMountPath: mountPath
@@ -468,8 +497,10 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
468
497
  env = process.env,
469
498
  timeoutMs = 6e5,
470
499
  pollMs = 15e3,
500
+ fetchImpl = fetch,
471
501
  onProgress
472
502
  } = {}) {
503
+ const startMs = performance.now();
473
504
  const deadline = Date.now() + timeoutMs;
474
505
  const projectId = services.find((service) => typeof service.projectId === "string" && service.projectId.trim())?.projectId ?? null;
475
506
  if (!projectId) {
@@ -482,6 +513,11 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
482
513
  environment: resolveRailwayEnvironmentForScope(scope),
483
514
  ok: false,
484
515
  status: "missing_project",
516
+ settle: {
517
+ durationMs: elapsedMs(startMs),
518
+ pollCount: 0,
519
+ finalStatus: "missing_project"
520
+ },
485
521
  message: `Railway deployment status for ${service.serviceName} cannot be checked without a project id.`
486
522
  }))
487
523
  };
@@ -489,12 +525,15 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
489
525
  let checks = [];
490
526
  let lastError = null;
491
527
  let lastSummary = "";
528
+ let pollCount = 0;
492
529
  for (; ; ) {
493
530
  lastError = null;
531
+ pollCount += 1;
494
532
  try {
495
533
  const statusPayload = await fetchRailwayProjectDeploymentStatus({
496
534
  projectId,
497
- env
535
+ env,
536
+ fetchImpl
498
537
  });
499
538
  checks = collectRailwayDeploymentStatusChecks(statusPayload, scope, services);
500
539
  } catch (error) {
@@ -506,28 +545,63 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
506
545
  environment: resolveRailwayEnvironmentForScope(scope),
507
546
  ok: false,
508
547
  status: "status_error",
548
+ settle: {
549
+ durationMs: elapsedMs(startMs),
550
+ pollCount,
551
+ finalStatus: "status_error"
552
+ },
509
553
  message: error instanceof Error ? error.message : String(error)
510
554
  }));
511
555
  }
512
556
  const summary = formatRailwayDeploymentStatusSummary(scope, checks);
513
- if (summary !== lastSummary || !checks.every((entry) => entry.ok === true)) {
514
- onProgress?.(summary, "stdout");
515
- lastSummary = summary;
557
+ const progress = `${summary} poll=${pollCount} elapsed=${formatDurationMs(elapsedMs(startMs))}`;
558
+ if (progress !== lastSummary || !checks.every((entry) => entry.ok === true || entry.skipped === true)) {
559
+ onProgress?.(progress, "stdout");
560
+ lastSummary = progress;
516
561
  }
517
- if (checks.every((entry) => entry.ok === true)) {
518
- return { ok: true, checks };
562
+ if (checks.every((entry) => entry.ok === true || entry.skipped === true)) {
563
+ return {
564
+ ok: true,
565
+ checks: checks.map((check) => ({
566
+ ...check,
567
+ settle: {
568
+ durationMs: elapsedMs(startMs),
569
+ pollCount,
570
+ finalStatus: check.status,
571
+ fastSkipped: check.skipped === true
572
+ }
573
+ })),
574
+ settle: {
575
+ durationMs: elapsedMs(startMs),
576
+ pollCount,
577
+ status: checks.every((entry) => entry.skipped === true) ? "skipped" : "settled"
578
+ }
579
+ };
519
580
  }
520
581
  if (Date.now() >= deadline) {
521
582
  return {
522
583
  ok: false,
523
- checks,
584
+ checks: checks.map((check) => ({
585
+ ...check,
586
+ settle: {
587
+ durationMs: elapsedMs(startMs),
588
+ pollCount,
589
+ finalStatus: check.status,
590
+ timeout: true
591
+ }
592
+ })),
593
+ settle: {
594
+ durationMs: elapsedMs(startMs),
595
+ pollCount,
596
+ status: "timeout"
597
+ },
524
598
  message: lastError instanceof Error ? lastError.message : "Railway deployments did not settle before the monitor timeout."
525
599
  };
526
600
  }
527
601
  await sleep(pollMs);
528
602
  }
529
603
  }
530
- async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env }) {
604
+ async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env, fetchImpl = fetch }) {
531
605
  const payload = await railwayGraphqlRequest({
532
606
  query: `
533
607
  query TreeseedRailwayDeploymentStatus($projectId: String!) {
@@ -565,7 +639,8 @@ query TreeseedRailwayDeploymentStatus($projectId: String!) {
565
639
  }
566
640
  `.trim(),
567
641
  variables: { projectId },
568
- env
642
+ env,
643
+ fetchImpl
569
644
  });
570
645
  return payload.data?.project ?? null;
571
646
  }
@@ -1294,6 +1369,7 @@ async function verifyRailwayManagedResources(tenantRoot, scope, {
1294
1369
  const settled = await waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
1295
1370
  services: deploymentStatusServices.length > 0 ? deploymentStatusServices : services,
1296
1371
  env: effectiveEnv,
1372
+ fetchImpl,
1297
1373
  timeoutMs: settleTimeoutMs,
1298
1374
  pollMs: settlePollMs,
1299
1375
  onProgress
@@ -1776,6 +1852,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1776
1852
  const listResult = runRailway([...volumeArgs, "list", "--json"], cliOptions);
1777
1853
  const existingVolumes = normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
1778
1854
  serviceId,
1855
+ serviceName,
1779
1856
  environmentId,
1780
1857
  fallbackName: name,
1781
1858
  fallbackMountPath: mountPath
@@ -1787,6 +1864,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1787
1864
  const createResult = runRailway([...volumeArgs, "add", "--mount-path", mountPath, "--json"], cliOptions);
1788
1865
  volume = normalizeRailwayCliVolume(parseRailwayJsonOutput(createResult.stdout ?? ""), {
1789
1866
  serviceId,
1867
+ serviceName,
1790
1868
  environmentId,
1791
1869
  fallbackName: name,
1792
1870
  fallbackMountPath: mountPath
@@ -1810,6 +1888,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1810
1888
  }
1811
1889
  const attachedVolume = (attachResult.status ?? 1) === 0 ? normalizeRailwayCliVolume(parseRailwayJsonOutput(attachResult.stdout ?? ""), {
1812
1890
  serviceId,
1891
+ serviceName,
1813
1892
  environmentId,
1814
1893
  fallbackName: name,
1815
1894
  fallbackMountPath: mountPath
@@ -1833,21 +1912,6 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1833
1912
  instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1834
1913
  updated = true;
1835
1914
  }
1836
- if (volume.name !== name || instance?.mountPath !== mountPath) {
1837
- const updateResult = runRailway([...volumeArgs, "update", "--volume", volume.id, "--name", name, "--mount-path", mountPath, "--json"], cliOptions);
1838
- const updatedVolume = normalizeRailwayCliVolume(parseRailwayJsonOutput(updateResult.stdout ?? ""), {
1839
- serviceId,
1840
- environmentId,
1841
- fallbackName: name,
1842
- fallbackMountPath: mountPath
1843
- });
1844
- volume = updatedVolume ?? {
1845
- ...volume,
1846
- name,
1847
- instances: volume.instances.map((entry) => ({ ...entry, mountPath }))
1848
- };
1849
- updated = true;
1850
- }
1851
1915
  const apiVolume = await waitForRailwayServiceVolumeMount({
1852
1916
  projectId,
1853
1917
  volumeId: volume.id,
@@ -1859,6 +1923,8 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1859
1923
  });
1860
1924
  if (apiVolume) {
1861
1925
  volume = apiVolume;
1926
+ } else {
1927
+ throw new Error(`Railway volume ${name} was not attached to ${serviceName} at ${mountPath}.`);
1862
1928
  }
1863
1929
  return {
1864
1930
  volume,
@@ -1878,11 +1944,12 @@ async function waitForRailwayServiceVolumeMount({
1878
1944
  }) {
1879
1945
  for (let attempt = 0; attempt <= 24; attempt += 1) {
1880
1946
  const volumes = await listRailwayVolumes({ projectId, env });
1881
- const match = volumes.find(
1882
- (entry) => entry.id === volumeId || entry.name === volumeName || entry.instances.some(
1947
+ const mounted = volumes.find(
1948
+ (entry) => entry.instances.some(
1883
1949
  (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1884
1950
  )
1885
1951
  ) ?? null;
1952
+ const match = mounted ?? volumes.find((entry) => entry.id === volumeId) ?? volumes.find((entry) => entry.name === volumeName) ?? null;
1886
1953
  if (match?.instances.some(
1887
1954
  (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1888
1955
  )) {
@@ -11,6 +11,7 @@ import {
11
11
  cliPackageVersion,
12
12
  agentPackageVersion,
13
13
  corePackageVersion,
14
+ cliPackageRoot,
14
15
  localTemplateArtifactsRoot,
15
16
  sdkPackageVersion
16
17
  } from "./runtime-paths.js";
@@ -37,10 +38,31 @@ function listFiles(root) {
37
38
  return files;
38
39
  }
39
40
  function listTemplateArtifactIds() {
40
- if (!existsSync(localTemplateArtifactsRoot)) {
41
- return [];
41
+ const packagedIds = existsSync(localTemplateArtifactsRoot) ? readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
42
+ const localStarterIds = listLocalStarterArtifacts().map((entry) => entry.id);
43
+ return [.../* @__PURE__ */ new Set([...packagedIds, ...localStarterIds])].sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
44
+ }
45
+ const LOCAL_STARTER_ID_TO_DIRECTORY = {
46
+ "starter-research": "research",
47
+ "starter-engineering": "engineering",
48
+ "starter-information-hub": "information-hub"
49
+ };
50
+ function localStartersRoot() {
51
+ return resolve(cliPackageRoot, "..", "..", "starters");
52
+ }
53
+ function resolveLocalStarterArtifactRoot(id) {
54
+ const directory = LOCAL_STARTER_ID_TO_DIRECTORY[id];
55
+ if (!directory) {
56
+ return null;
42
57
  }
43
- return readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
58
+ const artifactRoot = resolve(localStartersRoot(), directory);
59
+ return existsSync(resolve(artifactRoot, "template.config.json")) && existsSync(resolve(artifactRoot, "template")) ? artifactRoot : null;
60
+ }
61
+ function listLocalStarterArtifacts() {
62
+ return Object.keys(LOCAL_STARTER_ID_TO_DIRECTORY).map((id) => {
63
+ const artifactRoot = resolveLocalStarterArtifactRoot(id);
64
+ return artifactRoot ? { id, artifactRoot } : null;
65
+ }).filter((entry) => Boolean(entry));
44
66
  }
45
67
  function isTextFile(filePath) {
46
68
  return !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot|pdf|zip|gz)$/iu.test(filePath);
@@ -166,6 +188,14 @@ function materializeR2TemplateSource(product) {
166
188
  );
167
189
  }
168
190
  function resolveTemplateDefinitionPaths(product, options) {
191
+ const localStarterArtifactRoot = resolveLocalStarterArtifactRoot(product.id);
192
+ if (localStarterArtifactRoot) {
193
+ return {
194
+ artifactRoot: localStarterArtifactRoot,
195
+ manifestPath: resolve(localStarterArtifactRoot, "template.config.json"),
196
+ templateRoot: resolve(localStarterArtifactRoot, "template")
197
+ };
198
+ }
169
199
  if (existsSync(product.artifactManifestPath) && existsSync(product.templateRoot)) {
170
200
  return {
171
201
  artifactRoot: product.artifactRoot,
@@ -283,6 +283,7 @@ export interface TreeseedDeployConfig {
283
283
  slug: string;
284
284
  siteUrl: string;
285
285
  contactEmail: string;
286
+ projectRoot?: string;
286
287
  hosting?: TreeseedHostingConfig;
287
288
  hub: TreeseedHubConfig;
288
289
  runtime: TreeseedRuntimeConfig;