@treeseed/sdk 0.10.23 → 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.
@@ -1,3 +1,4 @@
1
+ import { type TreeseedTimingEntry } from '../../timing.ts';
1
2
  export type TreeseedBootstrapExecution = 'parallel' | 'sequential';
2
3
  export type TreeseedBootstrapStream = 'stdout' | 'stderr';
3
4
  export type TreeseedBootstrapWriter = (line: string, stream?: TreeseedBootstrapStream) => void;
@@ -11,13 +12,16 @@ export type TreeseedBootstrapDagNode<TResult = unknown> = {
11
12
  id: string;
12
13
  dependencies?: string[];
13
14
  run: () => Promise<TResult> | TResult;
15
+ label?: string;
14
16
  };
15
17
  export declare function formatTreeseedBootstrapPrefix(prefix: TreeseedBootstrapTaskPrefix): string;
16
18
  export declare function formatTreeseedBootstrapLine(prefix: TreeseedBootstrapTaskPrefix, line: string): string;
17
19
  export declare function writeTreeseedBootstrapLine(write: TreeseedBootstrapWriter | undefined, prefix: TreeseedBootstrapTaskPrefix, line: string, stream?: TreeseedBootstrapStream): void;
18
- export declare function runTreeseedBootstrapDag<TResult = unknown>({ nodes, execution, }: {
20
+ export declare function runTreeseedBootstrapDag<TResult = unknown>({ nodes, execution, write, timings, }: {
19
21
  nodes: Array<TreeseedBootstrapDagNode<TResult>>;
20
22
  execution?: TreeseedBootstrapExecution;
23
+ write?: TreeseedBootstrapWriter;
24
+ timings?: TreeseedTimingEntry[];
21
25
  }): Promise<Map<string, TResult>>;
22
26
  export declare function sleep(milliseconds: number): Promise<void>;
23
27
  export declare function runPrefixedCommand(command: string, args: string[], { cwd, env, input, write, prefix, }: {
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { elapsedMs, formatDurationMs } from "../../timing.js";
2
3
  function formatTreeseedBootstrapPrefix(prefix) {
3
4
  return `[${prefix.scope}][${prefix.system}][${prefix.task}][${prefix.stage}]`;
4
5
  }
@@ -43,19 +44,47 @@ function dependencyLevels(nodes) {
43
44
  }
44
45
  async function runTreeseedBootstrapDag({
45
46
  nodes,
46
- execution = "parallel"
47
+ execution = "parallel",
48
+ write,
49
+ timings
47
50
  }) {
48
51
  const results = /* @__PURE__ */ new Map();
52
+ const recordNode = async (node) => {
53
+ const label = node.label ?? node.id;
54
+ const startMs = performance.now();
55
+ const entry = {
56
+ name: `bootstrap:${label}`,
57
+ durationMs: 0,
58
+ status: "running",
59
+ metadata: { nodeId: node.id, dependencies: node.dependencies ?? [] }
60
+ };
61
+ timings?.push(entry);
62
+ write?.(`[bootstrap][${node.id}] started`);
63
+ try {
64
+ const result = await Promise.resolve(node.run());
65
+ entry.durationMs = elapsedMs(startMs);
66
+ entry.status = "success";
67
+ write?.(`[bootstrap][${node.id}] completed in ${formatDurationMs(entry.durationMs)}`);
68
+ results.set(node.id, result);
69
+ } catch (error) {
70
+ entry.durationMs = elapsedMs(startMs);
71
+ entry.status = "failed";
72
+ entry.metadata = {
73
+ ...entry.metadata ?? {},
74
+ error: error instanceof Error ? error.message : String(error)
75
+ };
76
+ write?.(`[bootstrap][${node.id}] failed after ${formatDurationMs(entry.durationMs)}`, "stderr");
77
+ throw error;
78
+ }
79
+ };
49
80
  for (const level of dependencyLevels(nodes)) {
50
81
  if (execution === "sequential") {
51
82
  for (const node of level) {
52
- results.set(node.id, await Promise.resolve(node.run()));
83
+ await recordNode(node);
53
84
  }
54
85
  continue;
55
86
  }
56
- await Promise.all(level.map(async (node) => {
57
- results.set(node.id, await Promise.resolve(node.run()));
58
- }));
87
+ await Promise.all(level.map(recordNode));
59
88
  }
60
89
  return results;
61
90
  }
@@ -385,6 +385,7 @@ export declare function initializeTreeseedPersistentEnvironment({ tenantRoot, sc
385
385
  plans: import("../../reconcile/contracts.ts").TreeseedReconcilePlan[];
386
386
  results: import("../../reconcile/contracts.ts").TreeseedReconcileResult[];
387
387
  state: import("../../reconcile/contracts.ts").TreeseedReconcileStateRecord;
388
+ timings: import("../../timing.ts").TreeseedTimingEntry[];
388
389
  };
389
390
  secrets: string[];
390
391
  }>;
@@ -1,3 +1,4 @@
1
+ import { type TreeseedTimingEntry } from '../../timing.ts';
1
2
  import { type ControlPlaneReporter } from '../../control-plane.ts';
2
3
  import type { TreeseedRunnableBootstrapSystem } from '../../reconcile/index.ts';
3
4
  import { type TreeseedBootstrapExecution, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
@@ -59,6 +60,7 @@ export declare function provisionProjectPlatform(options: ProjectPlatformActionO
59
60
  plans: import("../../reconcile/contracts.ts").TreeseedReconcilePlan[];
60
61
  results: import("../../reconcile/contracts.ts").TreeseedReconcileResult[];
61
62
  state: import("../../reconcile/contracts.ts").TreeseedReconcileStateRecord;
63
+ timings: TreeseedTimingEntry[];
62
64
  };
63
65
  verification: {
64
66
  target: import("../../reconcile/contracts.ts").TreeseedReconcileTarget;
@@ -76,6 +78,7 @@ export declare function provisionProjectPlatform(options: ProjectPlatformActionO
76
78
  verification: import("../../reconcile/contracts.ts").TreeseedUnitVerificationResult | null;
77
79
  }[];
78
80
  };
81
+ timings: TreeseedTimingEntry[];
79
82
  railway: {
80
83
  services: any[];
81
84
  schedules: any[];
@@ -104,6 +107,10 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
104
107
  objectKey: string;
105
108
  skipped?: undefined;
106
109
  reason?: undefined;
110
+ } | {
111
+ ok: boolean;
112
+ skipped: boolean;
113
+ reason: string;
107
114
  };
108
115
  queue: {
109
116
  ok: boolean;
@@ -241,6 +248,7 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
241
248
  };
242
249
  };
243
250
  };
251
+ timings: TreeseedTimingEntry[];
244
252
  };
245
253
  hostingRepair: {
246
254
  ok: boolean;
@@ -285,6 +293,7 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
285
293
  } | null;
286
294
  } | null;
287
295
  } | undefined)[];
296
+ timings: TreeseedTimingEntry[];
288
297
  }>;
289
298
  export declare function resolveRailwayServiceDeployDependencies({ includeDataDependency, previousRailwayDeployNodeId, }: {
290
299
  includeDataDependency: boolean;
@@ -313,6 +322,10 @@ export declare function monitorProjectPlatform(options: ProjectPlatformActionOpt
313
322
  objectKey: string;
314
323
  skipped?: undefined;
315
324
  reason?: undefined;
325
+ } | {
326
+ ok: boolean;
327
+ skipped: boolean;
328
+ reason: string;
316
329
  };
317
330
  queue: {
318
331
  ok: boolean;
@@ -450,6 +463,7 @@ export declare function monitorProjectPlatform(options: ProjectPlatformActionOpt
450
463
  };
451
464
  };
452
465
  };
466
+ timings: TreeseedTimingEntry[];
453
467
  }>;
454
468
  export declare function syncControlPlaneState(options: ProjectPlatformActionOptions): Promise<void>;
455
469
  export declare function runProjectPlatformAction(action: ProjectPlatformAction, options: ProjectPlatformActionOptions): Promise<{
@@ -474,6 +488,10 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
474
488
  objectKey: string;
475
489
  skipped?: undefined;
476
490
  reason?: undefined;
491
+ } | {
492
+ ok: boolean;
493
+ skipped: boolean;
494
+ reason: string;
477
495
  };
478
496
  queue: {
479
497
  ok: boolean;
@@ -611,6 +629,7 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
611
629
  };
612
630
  };
613
631
  };
632
+ timings: TreeseedTimingEntry[];
614
633
  } | {
615
634
  ok: boolean;
616
635
  scope: ProjectPlatformScope;
@@ -628,6 +647,10 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
628
647
  objectKey: string;
629
648
  skipped?: undefined;
630
649
  reason?: undefined;
650
+ } | {
651
+ ok: boolean;
652
+ skipped: boolean;
653
+ reason: string;
631
654
  };
632
655
  queue: {
633
656
  ok: boolean;
@@ -765,6 +788,7 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
765
788
  };
766
789
  };
767
790
  };
791
+ timings: TreeseedTimingEntry[];
768
792
  };
769
793
  hostingRepair: {
770
794
  ok: boolean;
@@ -809,5 +833,6 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
809
833
  } | null;
810
834
  } | null;
811
835
  } | undefined)[];
836
+ timings: TreeseedTimingEntry[];
812
837
  }>;
813
838
  export {};
@@ -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 = [];
@@ -1173,6 +1212,7 @@ async function provisionProjectPlatform(options) {
1173
1212
  target: deployTargetLabel(target),
1174
1213
  summary,
1175
1214
  verification,
1215
+ timings,
1176
1216
  reconcileActions: summary.results.map((result) => ({
1177
1217
  unitId: result.unit.unitId,
1178
1218
  action: result.action,
@@ -1190,6 +1230,7 @@ async function provisionProjectPlatform(options) {
1190
1230
  target: deployTargetLabel(target),
1191
1231
  summary,
1192
1232
  verification,
1233
+ timings,
1193
1234
  railway: {
1194
1235
  services: railwayValidation.services.map((service) => service.key),
1195
1236
  schedules: railwaySchedules,
@@ -1198,6 +1239,8 @@ async function provisionProjectPlatform(options) {
1198
1239
  };
1199
1240
  }
1200
1241
  async function deployProjectPlatform(options) {
1242
+ const timings = [];
1243
+ const deployStartMs = performance.now();
1201
1244
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1202
1245
  const commitSha = currentCommit(options.tenantRoot);
1203
1246
  const branchName = currentRef(options.tenantRoot);
@@ -1216,7 +1259,8 @@ async function deployProjectPlatform(options) {
1216
1259
  metadata: { scope: options.scope }
1217
1260
  });
1218
1261
  if (!options.skipProvision) {
1219
- await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
1262
+ const provision = await timedPhase(timings, "deploy:provision", () => provisionProjectPlatform({ ...options, reporter, bootstrapSystems }));
1263
+ timings.push(...provision.timings ?? []);
1220
1264
  }
1221
1265
  const nodes = [];
1222
1266
  let cloudflareContext = null;
@@ -1335,7 +1379,7 @@ async function deployProjectPlatform(options) {
1335
1379
  }
1336
1380
  });
1337
1381
  }
1338
- await runTreeseedBootstrapDag({ nodes, execution });
1382
+ await runTreeseedBootstrapDag({ nodes, execution, write, timings });
1339
1383
  const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
1340
1384
  if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
1341
1385
  finalizeDeploymentState(options.tenantRoot, {
@@ -1358,8 +1402,11 @@ async function deployProjectPlatform(options) {
1358
1402
  scheduleVerification: railwayScheduleVerification
1359
1403
  });
1360
1404
  }
1361
- const monitor = await monitorProjectPlatform({ ...options, reporter, bootstrapSystems });
1362
- 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);
1363
1410
  await reportDeployment(reporter, {
1364
1411
  environment: options.scope,
1365
1412
  deploymentKind: "code",
@@ -1371,7 +1418,8 @@ async function deployProjectPlatform(options) {
1371
1418
  scope: options.scope,
1372
1419
  railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
1373
1420
  monitor,
1374
- hostingRepair
1421
+ hostingRepair,
1422
+ timings
1375
1423
  },
1376
1424
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1377
1425
  });
@@ -1380,7 +1428,8 @@ async function deployProjectPlatform(options) {
1380
1428
  scope: options.scope,
1381
1429
  monitor,
1382
1430
  hostingRepair,
1383
- serviceResults
1431
+ serviceResults,
1432
+ timings
1384
1433
  };
1385
1434
  }
1386
1435
  function resolveRailwayServiceDeployDependencies({
@@ -1397,6 +1446,7 @@ async function publishProjectContent(options) {
1397
1446
  return publishContent(options, reporter);
1398
1447
  }
1399
1448
  async function monitorProjectPlatform(options) {
1449
+ const timings = [];
1400
1450
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1401
1451
  const env = { ...process.env, ...options.env ?? {} };
1402
1452
  const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
@@ -1408,23 +1458,23 @@ async function monitorProjectPlatform(options) {
1408
1458
  const webProbeUrl = resolveImmediatePagesProbeUrl(siteConfig, state, target);
1409
1459
  const apiBaseUrl = resolveImmediateApiProbeUrl(siteConfig, state, target);
1410
1460
  const apiMonitorEndpoints = resolveApiMonitorEndpoints(siteConfig, apiBaseUrl);
1411
- 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, {
1412
1462
  env,
1413
1463
  settleDeployments: true,
1414
1464
  onProgress: options.write
1415
- });
1465
+ }));
1416
1466
  const skippedApiCheck = apiSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "api_not_selected" };
1417
1467
  const skippedAgentCheck = agentsSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "agents_not_selected" };
1418
1468
  const skippedD1Check = apiMonitorEndpoints.processingAgentApi ? { ok: true, skipped: true, reason: "processing_agent_api" } : skippedApiCheck;
1419
1469
  const checks = {
1420
- pages: await probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 }),
1421
- apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1422
- apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
1423
- d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 }) : skippedD1Check,
1424
- agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 }) : skippedAgentCheck,
1425
- r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
1426
- queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
1427
- 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)),
1428
1478
  railwayResources,
1429
1479
  readiness: state.readiness,
1430
1480
  apiMonitor: {
@@ -1464,14 +1514,16 @@ ${failedChecks.join("\n")}`);
1464
1514
  metadata: {
1465
1515
  mode: "monitor",
1466
1516
  target: deployTargetLabel(target),
1467
- checks: resolvedChecks
1517
+ checks: resolvedChecks,
1518
+ timings
1468
1519
  },
1469
1520
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1470
1521
  });
1471
1522
  return {
1472
1523
  ok,
1473
1524
  target: deployTargetLabel(target),
1474
- checks: resolvedChecks
1525
+ checks: resolvedChecks,
1526
+ timings
1475
1527
  };
1476
1528
  }
1477
1529
  async function syncControlPlaneState(options) {
@@ -31,18 +31,47 @@ export declare function runRailway(args: any, { cwd, capture, allowFailure, inpu
31
31
  retryAttempts?: number | undefined;
32
32
  retryDelayMs?: number | undefined;
33
33
  }): import("child_process").SpawnSyncReturns<string> | null;
34
- 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, }?: {
35
35
  services?: unknown[] | undefined;
36
36
  env?: NodeJS.ProcessEnv | undefined;
37
37
  timeoutMs?: number | undefined;
38
38
  pollMs?: number | undefined;
39
+ fetchImpl?: typeof fetch | undefined;
39
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
+ } | {
40
59
  ok: boolean;
41
60
  checks: any;
61
+ settle: {
62
+ durationMs: number;
63
+ pollCount: number;
64
+ status: string;
65
+ };
42
66
  message?: undefined;
43
67
  } | {
44
68
  ok: boolean;
45
69
  checks: any;
70
+ settle: {
71
+ durationMs: number;
72
+ pollCount: number;
73
+ status: string;
74
+ };
46
75
  message: string;
47
76
  }>;
48
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 : []
@@ -474,8 +497,10 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
474
497
  env = process.env,
475
498
  timeoutMs = 6e5,
476
499
  pollMs = 15e3,
500
+ fetchImpl = fetch,
477
501
  onProgress
478
502
  } = {}) {
503
+ const startMs = performance.now();
479
504
  const deadline = Date.now() + timeoutMs;
480
505
  const projectId = services.find((service) => typeof service.projectId === "string" && service.projectId.trim())?.projectId ?? null;
481
506
  if (!projectId) {
@@ -488,6 +513,11 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
488
513
  environment: resolveRailwayEnvironmentForScope(scope),
489
514
  ok: false,
490
515
  status: "missing_project",
516
+ settle: {
517
+ durationMs: elapsedMs(startMs),
518
+ pollCount: 0,
519
+ finalStatus: "missing_project"
520
+ },
491
521
  message: `Railway deployment status for ${service.serviceName} cannot be checked without a project id.`
492
522
  }))
493
523
  };
@@ -495,12 +525,15 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
495
525
  let checks = [];
496
526
  let lastError = null;
497
527
  let lastSummary = "";
528
+ let pollCount = 0;
498
529
  for (; ; ) {
499
530
  lastError = null;
531
+ pollCount += 1;
500
532
  try {
501
533
  const statusPayload = await fetchRailwayProjectDeploymentStatus({
502
534
  projectId,
503
- env
535
+ env,
536
+ fetchImpl
504
537
  });
505
538
  checks = collectRailwayDeploymentStatusChecks(statusPayload, scope, services);
506
539
  } catch (error) {
@@ -512,28 +545,63 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
512
545
  environment: resolveRailwayEnvironmentForScope(scope),
513
546
  ok: false,
514
547
  status: "status_error",
548
+ settle: {
549
+ durationMs: elapsedMs(startMs),
550
+ pollCount,
551
+ finalStatus: "status_error"
552
+ },
515
553
  message: error instanceof Error ? error.message : String(error)
516
554
  }));
517
555
  }
518
556
  const summary = formatRailwayDeploymentStatusSummary(scope, checks);
519
- if (summary !== lastSummary || !checks.every((entry) => entry.ok === true)) {
520
- onProgress?.(summary, "stdout");
521
- 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;
522
561
  }
523
- if (checks.every((entry) => entry.ok === true)) {
524
- 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
+ };
525
580
  }
526
581
  if (Date.now() >= deadline) {
527
582
  return {
528
583
  ok: false,
529
- 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
+ },
530
598
  message: lastError instanceof Error ? lastError.message : "Railway deployments did not settle before the monitor timeout."
531
599
  };
532
600
  }
533
601
  await sleep(pollMs);
534
602
  }
535
603
  }
536
- async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env }) {
604
+ async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env, fetchImpl = fetch }) {
537
605
  const payload = await railwayGraphqlRequest({
538
606
  query: `
539
607
  query TreeseedRailwayDeploymentStatus($projectId: String!) {
@@ -571,7 +639,8 @@ query TreeseedRailwayDeploymentStatus($projectId: String!) {
571
639
  }
572
640
  `.trim(),
573
641
  variables: { projectId },
574
- env
642
+ env,
643
+ fetchImpl
575
644
  });
576
645
  return payload.data?.project ?? null;
577
646
  }
@@ -1300,6 +1369,7 @@ async function verifyRailwayManagedResources(tenantRoot, scope, {
1300
1369
  const settled = await waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
1301
1370
  services: deploymentStatusServices.length > 0 ? deploymentStatusServices : services,
1302
1371
  env: effectiveEnv,
1372
+ fetchImpl,
1303
1373
  timeoutMs: settleTimeoutMs,
1304
1374
  pollMs: settlePollMs,
1305
1375
  onProgress
@@ -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;
@@ -9,7 +9,8 @@ import {
9
9
  } from "./plugins/constants.js";
10
10
  const deployConfigFieldAliases = {
11
11
  siteUrl: { key: "siteUrl", aliases: ["site_url"] },
12
- contactEmail: { key: "contactEmail", aliases: ["contact_email"] }
12
+ contactEmail: { key: "contactEmail", aliases: ["contact_email"] },
13
+ projectRoot: { key: "projectRoot", aliases: ["project_root"] }
13
14
  };
14
15
  const hostingFieldAliases = {
15
16
  kind: { key: "kind", aliases: ["kind"] },
@@ -547,6 +548,7 @@ function parseDeployConfig(raw) {
547
548
  slug: expectString(parsed.slug, "slug"),
548
549
  siteUrl: expectString(parsed.siteUrl, "siteUrl"),
549
550
  contactEmail: expectString(parsed.contactEmail, "contactEmail"),
551
+ projectRoot: optionalString(parsed.projectRoot),
550
552
  hosting: compatibilityHosting,
551
553
  hub,
552
554
  runtime,
@@ -630,10 +632,15 @@ function loadTreeseedDeployConfig(configPath = "treeseed.site.yaml") {
630
632
  function loadTreeseedDeployConfigFromPath(resolvedConfigPath) {
631
633
  const tenantRoot = dirname(resolvedConfigPath);
632
634
  const parsed = parseDeployConfig(readFileSync(resolvedConfigPath, "utf8"));
635
+ const projectRoot = parsed.projectRoot ? resolve(tenantRoot, parsed.projectRoot) : tenantRoot;
633
636
  Object.defineProperty(parsed, "__tenantRoot", {
634
637
  value: tenantRoot,
635
638
  enumerable: false
636
639
  });
640
+ Object.defineProperty(parsed, "__projectRoot", {
641
+ value: projectRoot,
642
+ enumerable: false
643
+ });
637
644
  Object.defineProperty(parsed, "__configPath", {
638
645
  value: resolvedConfigPath,
639
646
  enumerable: false
@@ -6,6 +6,7 @@ function defaultDeployConfig() {
6
6
  slug: "treeseed-site",
7
7
  siteUrl: "https://example.com",
8
8
  contactEmail: "contact@example.com",
9
+ projectRoot: ".",
9
10
  cloudflare: {
10
11
  accountId: "",
11
12
  workerName: "treeseed-site"
@@ -1,5 +1,6 @@
1
1
  import type { TreeseedObservedUnitState, TreeseedReconcilePlan, TreeseedReconcileResult, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitVerificationResult } from './contracts.ts';
2
2
  import { type TreeseedRunnableBootstrapSystem } from './bootstrap-systems.ts';
3
+ import { type TreeseedTimingEntry } from '../timing.ts';
3
4
  export declare function observeTreeseedUnits({ tenantRoot, target, env, systems, write, }: {
4
5
  tenantRoot: string;
5
6
  target: TreeseedReconcileTarget;
@@ -417,6 +418,7 @@ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, syste
417
418
  plans: TreeseedReconcilePlan[];
418
419
  results: TreeseedReconcileResult[];
419
420
  state: TreeseedReconcileStateRecord;
421
+ timings: TreeseedTimingEntry[];
420
422
  }>;
421
423
  export declare function destroyTreeseedTargetUnits({ tenantRoot, target, env, write, }: {
422
424
  tenantRoot: string;
@@ -3,6 +3,7 @@ import { deriveTreeseedDesiredUnits } from "./desired-state.js";
3
3
  import { ensureTreeseedPersistedUnitState, desiredUnitSpecHash, loadTreeseedReconcileState, updateTreeseedPersistedUnitState, writeTreeseedReconcileState } from "./state.js";
4
4
  import { reverseTopologicallySortedUnits, topologicallySortDesiredUnits } from "./units.js";
5
5
  import { filterTreeseedDesiredUnitsByBootstrapSystems } from "./bootstrap-systems.js";
6
+ import { elapsedMs, formatDurationMs } from "../timing.js";
6
7
  function nowIso() {
7
8
  return (/* @__PURE__ */ new Date()).toISOString();
8
9
  }
@@ -209,7 +210,9 @@ async function reconcileTreeseedTarget({
209
210
  const context = createRunContext(tenantRoot, target, env, write);
210
211
  const results = [];
211
212
  const verificationMap = /* @__PURE__ */ new Map();
213
+ const timingEntries = [];
212
214
  context.session.set("treeseed:verification-results", verificationMap);
215
+ context.session.set("treeseed:timings", timingEntries);
213
216
  const planByUnitId = new Map(planned.plans.map((plan) => [plan.unit.unitId, plan]));
214
217
  let persistChain = Promise.resolve();
215
218
  const persistVerifiedResult = async (persisted, verifiedResult) => {
@@ -221,20 +224,43 @@ async function reconcileTreeseedTarget({
221
224
  };
222
225
  await runByDependencyLevel(topologicallySortDesiredUnits(planned.units), async (unit) => {
223
226
  const plan = planByUnitId.get(unit.unitId);
224
- write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType}...`);
227
+ const unitTiming = {
228
+ name: `reconcile:${plan.unit.provider}:${plan.unit.unitType}:${plan.unit.logicalName}`,
229
+ durationMs: 0,
230
+ status: "running",
231
+ children: [],
232
+ metadata: { unitId: plan.unit.unitId, action: plan.diff.action }
233
+ };
234
+ const unitStartMs = performance.now();
235
+ timingEntries.push(unitTiming);
236
+ write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
225
237
  const adapter = registry.get(plan.unit.unitType, plan.unit.provider);
226
238
  const persisted = ensureTreeseedPersistedUnitState(planned.state, plan.unit);
227
239
  try {
240
+ const stageStartMs = performance.now();
228
241
  await Promise.resolve(adapter.validate?.({
229
242
  context,
230
243
  unit: plan.unit,
231
244
  persistedState: persisted
232
245
  }));
246
+ unitTiming.children?.push({
247
+ name: `${unitTiming.name}:validate`,
248
+ durationMs: elapsedMs(stageStartMs),
249
+ status: "success"
250
+ });
233
251
  } catch (error) {
252
+ unitTiming.children?.push({
253
+ name: `${unitTiming.name}:validate`,
254
+ durationMs: elapsedMs(unitStartMs),
255
+ status: "failed"
256
+ });
257
+ unitTiming.durationMs = elapsedMs(unitStartMs);
258
+ unitTiming.status = "failed";
234
259
  wrapAdapterFailure("validate", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
235
260
  }
236
261
  let result;
237
262
  try {
263
+ const stageStartMs = performance.now();
238
264
  result = await Promise.resolve(adapter.reconcile({
239
265
  context,
240
266
  unit: plan.unit,
@@ -242,10 +268,22 @@ async function reconcileTreeseedTarget({
242
268
  observed: plan.observed,
243
269
  diff: plan.diff
244
270
  }));
271
+ unitTiming.children?.push({
272
+ name: `${unitTiming.name}:reconcile`,
273
+ durationMs: elapsedMs(stageStartMs),
274
+ status: "success"
275
+ });
245
276
  } catch (error) {
277
+ unitTiming.children?.push({
278
+ name: `${unitTiming.name}:reconcile`,
279
+ durationMs: elapsedMs(unitStartMs),
280
+ status: "failed"
281
+ });
282
+ unitTiming.durationMs = elapsedMs(unitStartMs);
283
+ unitTiming.status = "failed";
246
284
  wrapAdapterFailure("reconcile", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
247
285
  }
248
- write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType}...`);
286
+ write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
249
287
  const postconditions = await Promise.resolve(adapter.requiredPostconditions?.({
250
288
  context,
251
289
  unit: plan.unit,
@@ -253,6 +291,7 @@ async function reconcileTreeseedTarget({
253
291
  }) ?? []);
254
292
  let verification;
255
293
  try {
294
+ const stageStartMs = performance.now();
256
295
  verification = await Promise.resolve(adapter.verify({
257
296
  context,
258
297
  unit: plan.unit,
@@ -262,7 +301,19 @@ async function reconcileTreeseedTarget({
262
301
  result,
263
302
  postconditions
264
303
  }));
304
+ unitTiming.children?.push({
305
+ name: `${unitTiming.name}:verify`,
306
+ durationMs: elapsedMs(stageStartMs),
307
+ status: verification.verified ? "success" : "failed"
308
+ });
265
309
  } catch (error) {
310
+ unitTiming.children?.push({
311
+ name: `${unitTiming.name}:verify`,
312
+ durationMs: elapsedMs(unitStartMs),
313
+ status: "failed"
314
+ });
315
+ unitTiming.durationMs = elapsedMs(unitStartMs);
316
+ unitTiming.status = "failed";
266
317
  wrapAdapterFailure("verify", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
267
318
  }
268
319
  const verifiedResult = {
@@ -274,6 +325,9 @@ async function reconcileTreeseedTarget({
274
325
  ]
275
326
  };
276
327
  verificationMap.set(plan.unit.unitId, verification);
328
+ unitTiming.durationMs = elapsedMs(unitStartMs);
329
+ unitTiming.status = verification.verified ? "success" : "failed";
330
+ write?.(`Finished ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName}) in ${formatDurationMs(unitTiming.durationMs)}.`);
277
331
  if (!verification.verified) {
278
332
  await persistVerifiedResult(persisted, verifiedResult);
279
333
  throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
@@ -287,7 +341,8 @@ async function reconcileTreeseedTarget({
287
341
  units: planned.units,
288
342
  plans: planned.plans,
289
343
  results,
290
- state: planned.state
344
+ state: planned.state,
345
+ timings: timingEntries
291
346
  };
292
347
  }
293
348
  async function destroyTreeseedTargetUnits({
@@ -0,0 +1,20 @@
1
+ export type TreeseedTimingEntry = {
2
+ name: string;
3
+ durationMs: number;
4
+ status?: string;
5
+ metadata?: Record<string, unknown>;
6
+ children?: TreeseedTimingEntry[];
7
+ };
8
+ export declare function nowMs(): number;
9
+ export declare function elapsedMs(startMs: number): number;
10
+ export declare function formatDurationMs(durationMs: number): string;
11
+ export declare function summarizeSlowestTimings(entries: TreeseedTimingEntry[], limit?: number): TreeseedTimingEntry[];
12
+ export declare function flattenTimings(entries: TreeseedTimingEntry[]): TreeseedTimingEntry[];
13
+ export declare function formatTimingSummary(entries: TreeseedTimingEntry[], { title, limit }?: {
14
+ title?: string | undefined;
15
+ limit?: number | undefined;
16
+ }): string;
17
+ export declare function formatTimingMarkdown(entries: TreeseedTimingEntry[], { title, limit }?: {
18
+ title?: string | undefined;
19
+ limit?: number | undefined;
20
+ }): string;
package/dist/timing.js ADDED
@@ -0,0 +1,73 @@
1
+ function nowMs() {
2
+ return performance.now();
3
+ }
4
+ function elapsedMs(startMs) {
5
+ return Math.max(0, performance.now() - startMs);
6
+ }
7
+ function formatDurationMs(durationMs) {
8
+ const value = Math.max(0, Number(durationMs) || 0);
9
+ if (value < 1e3) {
10
+ return `${Math.round(value)}ms`;
11
+ }
12
+ if (value < 6e4) {
13
+ return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}s`;
14
+ }
15
+ const minutes = Math.floor(value / 6e4);
16
+ const seconds = Math.round(value % 6e4 / 1e3);
17
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
18
+ }
19
+ function summarizeSlowestTimings(entries, limit = 8) {
20
+ return flattenTimings(entries).sort((left, right) => right.durationMs - left.durationMs).slice(0, Math.max(0, limit));
21
+ }
22
+ function flattenTimings(entries) {
23
+ const flattened = [];
24
+ const visit = (entry) => {
25
+ flattened.push(entry);
26
+ for (const child of entry.children ?? []) {
27
+ visit(child);
28
+ }
29
+ };
30
+ for (const entry of entries) {
31
+ visit(entry);
32
+ }
33
+ return flattened;
34
+ }
35
+ function formatTimingSummary(entries, { title = "Provider deploy timing summary", limit = 12 } = {}) {
36
+ const slowest = summarizeSlowestTimings(entries, limit);
37
+ const lines = [`${title}:`];
38
+ if (slowest.length === 0) {
39
+ lines.push("- no timed steps recorded");
40
+ return lines.join("\n");
41
+ }
42
+ for (const entry of slowest) {
43
+ const status = entry.status ? ` [${entry.status}]` : "";
44
+ lines.push(`- ${entry.name}: ${formatDurationMs(entry.durationMs)}${status}`);
45
+ }
46
+ return lines.join("\n");
47
+ }
48
+ function formatTimingMarkdown(entries, { title = "Provider deploy timing summary", limit = 20 } = {}) {
49
+ const slowest = summarizeSlowestTimings(entries, limit);
50
+ const lines = [`### ${title}`, "", "| Step | Duration | Status |", "| --- | ---: | --- |"];
51
+ if (slowest.length === 0) {
52
+ lines.push("| No timed steps recorded | 0ms | skipped |");
53
+ return `${lines.join("\n")}
54
+ `;
55
+ }
56
+ for (const entry of slowest) {
57
+ lines.push(`| ${escapeMarkdownCell(entry.name)} | ${formatDurationMs(entry.durationMs)} | ${escapeMarkdownCell(entry.status ?? "")} |`);
58
+ }
59
+ return `${lines.join("\n")}
60
+ `;
61
+ }
62
+ function escapeMarkdownCell(value) {
63
+ return value.replace(/\\/gu, "\\\\").replace(/\|/gu, "\\|").replace(/\n/gu, " ");
64
+ }
65
+ export {
66
+ elapsedMs,
67
+ flattenTimings,
68
+ formatDurationMs,
69
+ formatTimingMarkdown,
70
+ formatTimingSummary,
71
+ nowMs,
72
+ summarizeSlowestTimings
73
+ };
@@ -47,6 +47,156 @@
47
47
  "relatedBooks": [],
48
48
  "relatedKnowledge": [],
49
49
  "relatedObjectives": []
50
+ },
51
+ {
52
+ "id": "starter-research",
53
+ "displayName": "TreeSeed Research",
54
+ "description": "First-party TreeSeed starter for freestanding research projects and knowledge-pack publishing.",
55
+ "summary": "A research starter for building source-backed books, gathered assets, synthesis notes, and reusable knowledge packs.",
56
+ "status": "live",
57
+ "featured": true,
58
+ "category": "starter",
59
+ "audience": [
60
+ "researchers",
61
+ "maintainers"
62
+ ],
63
+ "tags": [
64
+ "starter",
65
+ "research",
66
+ "books",
67
+ "knowledge-packs",
68
+ "agents"
69
+ ],
70
+ "publisher": {
71
+ "id": "treeseed",
72
+ "name": "TreeSeed",
73
+ "url": "https://treeseed.ai"
74
+ },
75
+ "publisherVerified": true,
76
+ "templateVersion": "1.0.0",
77
+ "templateApiVersion": 1,
78
+ "minCliVersion": "0.1.1",
79
+ "minCoreVersion": "0.1.2",
80
+ "fulfillment": {
81
+ "mode": "git",
82
+ "source": {
83
+ "kind": "git",
84
+ "repoUrl": "https://github.com/treeseed-ai/starter-research.git",
85
+ "directory": ".",
86
+ "ref": "main",
87
+ "integrity": "pending-external-repo"
88
+ },
89
+ "hooksPolicy": "builtin_only",
90
+ "supportsReconcile": true
91
+ },
92
+ "offer": {
93
+ "priceModel": "free",
94
+ "license": "AGPL-3.0-only",
95
+ "support": "community"
96
+ },
97
+ "relatedBooks": [],
98
+ "relatedKnowledge": [],
99
+ "relatedObjectives": []
100
+ },
101
+ {
102
+ "id": "starter-engineering",
103
+ "displayName": "TreeSeed Engineering",
104
+ "description": "First-party TreeSeed starter for integrated software guidance, docs, and engineering agents.",
105
+ "summary": "An engineering starter for guiding software projects with research, architecture, implementation, review, and release agents.",
106
+ "status": "live",
107
+ "featured": true,
108
+ "category": "starter",
109
+ "audience": [
110
+ "engineers",
111
+ "maintainers"
112
+ ],
113
+ "tags": [
114
+ "starter",
115
+ "engineering",
116
+ "software",
117
+ "docs",
118
+ "agents"
119
+ ],
120
+ "publisher": {
121
+ "id": "treeseed",
122
+ "name": "TreeSeed",
123
+ "url": "https://treeseed.ai"
124
+ },
125
+ "publisherVerified": true,
126
+ "templateVersion": "1.0.0",
127
+ "templateApiVersion": 1,
128
+ "minCliVersion": "0.1.1",
129
+ "minCoreVersion": "0.1.2",
130
+ "fulfillment": {
131
+ "mode": "git",
132
+ "source": {
133
+ "kind": "git",
134
+ "repoUrl": "https://github.com/treeseed-ai/starter-engineering.git",
135
+ "directory": ".",
136
+ "ref": "main",
137
+ "integrity": "pending-external-repo"
138
+ },
139
+ "hooksPolicy": "builtin_only",
140
+ "supportsReconcile": true
141
+ },
142
+ "offer": {
143
+ "priceModel": "free",
144
+ "license": "AGPL-3.0-only",
145
+ "support": "community"
146
+ },
147
+ "relatedBooks": [],
148
+ "relatedKnowledge": [],
149
+ "relatedObjectives": []
150
+ },
151
+ {
152
+ "id": "starter-information-hub",
153
+ "displayName": "TreeSeed Information Hub",
154
+ "description": "First-party TreeSeed starter for recurring information retrieval, distillation, and downstream distribution.",
155
+ "summary": "An information distribution starter for retrieving updates over time, distilling derived knowledge, and packaging it for downstream projects.",
156
+ "status": "live",
157
+ "featured": true,
158
+ "category": "starter",
159
+ "audience": [
160
+ "curators",
161
+ "maintainers"
162
+ ],
163
+ "tags": [
164
+ "starter",
165
+ "information-hub",
166
+ "distribution",
167
+ "knowledge-packs",
168
+ "agents"
169
+ ],
170
+ "publisher": {
171
+ "id": "treeseed",
172
+ "name": "TreeSeed",
173
+ "url": "https://treeseed.ai"
174
+ },
175
+ "publisherVerified": true,
176
+ "templateVersion": "1.0.0",
177
+ "templateApiVersion": 1,
178
+ "minCliVersion": "0.1.1",
179
+ "minCoreVersion": "0.1.2",
180
+ "fulfillment": {
181
+ "mode": "git",
182
+ "source": {
183
+ "kind": "git",
184
+ "repoUrl": "https://github.com/treeseed-ai/starter-information-hub.git",
185
+ "directory": ".",
186
+ "ref": "main",
187
+ "integrity": "pending-external-repo"
188
+ },
189
+ "hooksPolicy": "builtin_only",
190
+ "supportsReconcile": true
191
+ },
192
+ "offer": {
193
+ "priceModel": "free",
194
+ "license": "AGPL-3.0-only",
195
+ "support": "community"
196
+ },
197
+ "relatedBooks": [],
198
+ "relatedKnowledge": [],
199
+ "relatedObjectives": []
50
200
  }
51
201
  ],
52
202
  "meta": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.10.23",
3
+ "version": "0.10.24",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -120,7 +120,11 @@ __WORKING_DIRECTORY_BLOCK__ web:
120
120
  shell: bash
121
121
  run: |
122
122
  set -euo pipefail
123
+ TIMING_SUMMARY="${RUNNER_TEMP:-/tmp}/treeseed-provider-timing.md"
124
+ : > "${TIMING_SUMMARY}"
125
+ export TREESEED_PROVIDER_TIMING_SUMMARY_PATH="${TIMING_SUMMARY}"
123
126
  EXTRA_ARGS=()
124
127
  if [[ -n "${TREESEED_WORKFLOW_PROJECT:-}" ]]; then EXTRA_ARGS+=(--project-id "${TREESEED_WORKFLOW_PROJECT}"); fi
125
128
  if [[ -n "${TREESEED_WORKFLOW_PREVIEW_ID:-}" ]]; then EXTRA_ARGS+=(--preview-id "${TREESEED_WORKFLOW_PREVIEW_ID}"); fi
126
129
  node ./packages/sdk/scripts/run-ts.mjs ./packages/sdk/scripts/tenant-workflow-action.ts --action "${TREESEED_WORKFLOW_ACTION}" --environment "${TREESEED_WORKFLOW_ENVIRONMENT}" "${EXTRA_ARGS[@]}"
130
+ if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -s "${TIMING_SUMMARY}" ]]; then cat "${TIMING_SUMMARY}" >> "${GITHUB_STEP_SUMMARY}"; fi