@treeseed/sdk 0.6.0 → 0.6.2

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 (32) hide show
  1. package/dist/operations/services/bootstrap-runner.d.ts +33 -0
  2. package/dist/operations/services/bootstrap-runner.js +136 -0
  3. package/dist/operations/services/config-runtime.d.ts +27 -8
  4. package/dist/operations/services/config-runtime.js +297 -124
  5. package/dist/operations/services/github-api.d.ts +33 -0
  6. package/dist/operations/services/github-api.js +118 -4
  7. package/dist/operations/services/github-automation.d.ts +30 -0
  8. package/dist/operations/services/github-automation.js +107 -1
  9. package/dist/operations/services/project-platform.d.ts +38 -2
  10. package/dist/operations/services/project-platform.js +281 -9
  11. package/dist/operations/services/railway-deploy.d.ts +6 -2
  12. package/dist/operations/services/railway-deploy.js +26 -18
  13. package/dist/operations/services/runtime-tools.d.ts +0 -2
  14. package/dist/operations/services/runtime-tools.js +0 -2
  15. package/dist/platform/env.yaml +68 -96
  16. package/dist/platform/environment.js +51 -0
  17. package/dist/reconcile/bootstrap-systems.d.ts +32 -0
  18. package/dist/reconcile/bootstrap-systems.js +175 -0
  19. package/dist/reconcile/builtin-adapters.js +24 -9
  20. package/dist/reconcile/desired-state.js +16 -14
  21. package/dist/reconcile/engine.d.ts +9 -4
  22. package/dist/reconcile/engine.js +57 -14
  23. package/dist/reconcile/index.d.ts +1 -0
  24. package/dist/reconcile/index.js +1 -0
  25. package/dist/scripts/config-treeseed.js +30 -0
  26. package/dist/scripts/package-tools.js +0 -2
  27. package/dist/scripts/tenant-deploy.js +16 -36
  28. package/dist/scripts/test-cloudflare-local.js +0 -2
  29. package/dist/workflow/operations.js +23 -4
  30. package/dist/workflow.d.ts +5 -0
  31. package/package.json +1 -1
  32. package/templates/github/deploy.workflow.yml +15 -15
@@ -0,0 +1,175 @@
1
+ const TREESEED_BOOTSTRAP_SYSTEMS = ["github", "data", "web", "api", "agents"];
2
+ const BOOTSTRAP_SYSTEM_SET = new Set(TREESEED_BOOTSTRAP_SYSTEMS);
3
+ const SYSTEM_DEPENDENCIES = {
4
+ github: [],
5
+ data: [],
6
+ web: ["data"],
7
+ api: ["data"],
8
+ agents: ["data"]
9
+ };
10
+ function uniqueSystems(values) {
11
+ return [...new Set(values)];
12
+ }
13
+ function parseTreeseedBootstrapSystems(value) {
14
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : ["all"];
15
+ const systems = raw.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
16
+ if (systems.length === 0 || systems.includes("all")) {
17
+ return ["all"];
18
+ }
19
+ const invalid = systems.filter((system) => !BOOTSTRAP_SYSTEM_SET.has(system));
20
+ if (invalid.length > 0) {
21
+ throw new Error(`Unknown Treeseed bootstrap system "${invalid[0]}". Expected one of all, ${TREESEED_BOOTSTRAP_SYSTEMS.join(", ")}.`);
22
+ }
23
+ return uniqueSystems(systems);
24
+ }
25
+ function expandBootstrapSystems(systems) {
26
+ const expanded = /* @__PURE__ */ new Set();
27
+ const visit = (system) => {
28
+ for (const dependency of SYSTEM_DEPENDENCIES[system]) {
29
+ visit(dependency);
30
+ }
31
+ expanded.add(system);
32
+ };
33
+ for (const system of systems) {
34
+ visit(system);
35
+ }
36
+ return [...expanded];
37
+ }
38
+ function serviceEnabled(config, serviceKey) {
39
+ const service = config.services?.[serviceKey];
40
+ return Boolean(service && service.enabled !== false && (service.provider ?? "railway") === "railway");
41
+ }
42
+ function apiSystemDisabled(config) {
43
+ if (config.runtime?.mode === "none") {
44
+ return "runtime.mode is none.";
45
+ }
46
+ if (config.surfaces?.api?.enabled === false) {
47
+ return "surfaces.api.enabled is false.";
48
+ }
49
+ if (!serviceEnabled(config, "api")) {
50
+ return "services.api is not enabled for Railway.";
51
+ }
52
+ return null;
53
+ }
54
+ function agentsSystemDisabled(config) {
55
+ if (config.runtime?.mode === "none") {
56
+ return "runtime.mode is none.";
57
+ }
58
+ const enabled = ["manager", "worker", "workdayStart", "workdayReport"].some((serviceKey) => serviceEnabled(config, serviceKey));
59
+ return enabled ? null : "No agent Railway services are enabled.";
60
+ }
61
+ function hasValue(env, key) {
62
+ return typeof env[key] === "string" && String(env[key]).trim().length > 0;
63
+ }
64
+ function missingForSystem(system, env) {
65
+ switch (system) {
66
+ case "github":
67
+ return hasValue(env, "GH_TOKEN") || hasValue(env, "GITHUB_TOKEN") ? [] : ["GH_TOKEN"];
68
+ case "data":
69
+ case "web":
70
+ return hasValue(env, "CLOUDFLARE_API_TOKEN") ? [] : ["CLOUDFLARE_API_TOKEN"];
71
+ case "api":
72
+ case "agents":
73
+ return hasValue(env, "RAILWAY_API_TOKEN") ? [] : ["RAILWAY_API_TOKEN"];
74
+ default:
75
+ return [];
76
+ }
77
+ }
78
+ function resolveTreeseedBootstrapSelection({
79
+ deployConfig,
80
+ env,
81
+ systems,
82
+ skipUnavailable
83
+ }) {
84
+ const requested = parseTreeseedBootstrapSystems(systems);
85
+ const explicit = !(systems === void 0 || systems === null) && requested.length > 0;
86
+ const selected = requested.includes("all") ? [...TREESEED_BOOTSTRAP_SYSTEMS] : requested;
87
+ const expanded = expandBootstrapSystems(selected);
88
+ const defaultSkipUnavailable = requested.includes("all");
89
+ const effectiveSkipUnavailable = skipUnavailable ?? defaultSkipUnavailable;
90
+ const canSkipUnavailable = (system) => effectiveSkipUnavailable && (skipUnavailable === true || system === "api" || system === "agents");
91
+ const statuses = [];
92
+ const configDisabled = [];
93
+ const unavailable = [];
94
+ const skipped = [];
95
+ const runnable = [];
96
+ for (const system of expanded) {
97
+ const disabledReason = system === "api" ? apiSystemDisabled(deployConfig) : system === "agents" ? agentsSystemDisabled(deployConfig) : null;
98
+ if (disabledReason) {
99
+ const status2 = { system, status: "config_disabled", reason: disabledReason, missing: [] };
100
+ configDisabled.push(status2);
101
+ skipped.push(status2);
102
+ statuses.push(status2);
103
+ continue;
104
+ }
105
+ const missing = missingForSystem(system, env);
106
+ if (missing.length > 0) {
107
+ const reason = `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not configured.`;
108
+ const status2 = {
109
+ system,
110
+ status: "unavailable",
111
+ reason,
112
+ missing
113
+ };
114
+ if (!canSkipUnavailable(system) && (selected.includes(system) || !requested.includes("all"))) {
115
+ unavailable.push(status2);
116
+ statuses.push(status2);
117
+ continue;
118
+ }
119
+ unavailable.push(status2);
120
+ skipped.push(status2);
121
+ statuses.push(status2);
122
+ continue;
123
+ }
124
+ const status = {
125
+ system,
126
+ status: selected.includes(system) ? "selected" : "included_dependency",
127
+ reason: selected.includes(system) ? "Selected for bootstrap." : "Included as a dependency.",
128
+ missing: []
129
+ };
130
+ runnable.push(system);
131
+ statuses.push(status);
132
+ }
133
+ return {
134
+ requested,
135
+ explicit,
136
+ skipUnavailable: effectiveSkipUnavailable,
137
+ selected,
138
+ expanded,
139
+ runnable,
140
+ configDisabled,
141
+ unavailable,
142
+ skipped,
143
+ statuses
144
+ };
145
+ }
146
+ function bootstrapSystemForUnit(unit) {
147
+ const metadataSystem = unit.metadata?.bootstrapSystem;
148
+ if (typeof metadataSystem === "string" && BOOTSTRAP_SYSTEM_SET.has(metadataSystem)) {
149
+ return metadataSystem;
150
+ }
151
+ if (unit.unitType === "queue" || unit.unitType === "database" || unit.unitType === "content-store") {
152
+ return "data";
153
+ }
154
+ if (unit.unitType === "api-runtime" || unit.unitType === "railway-service:api" || unit.unitType === "custom-domain:api") {
155
+ return "api";
156
+ }
157
+ if (unit.unitType.startsWith("railway-service:") || unit.unitType.endsWith("-runtime")) {
158
+ return "agents";
159
+ }
160
+ return "web";
161
+ }
162
+ function filterTreeseedDesiredUnitsByBootstrapSystems(units, systems) {
163
+ if (!systems || systems.length === 0) {
164
+ return units;
165
+ }
166
+ const allowed = new Set(systems);
167
+ return units.filter((unit) => allowed.has(bootstrapSystemForUnit(unit)));
168
+ }
169
+ export {
170
+ TREESEED_BOOTSTRAP_SYSTEMS,
171
+ bootstrapSystemForUnit,
172
+ filterTreeseedDesiredUnitsByBootstrapSystems,
173
+ parseTreeseedBootstrapSystems,
174
+ resolveTreeseedBootstrapSelection
175
+ };
@@ -155,28 +155,43 @@ function providerCache(input, key, loader, forceRefresh = false) {
155
155
  input.context.session.set(key, value);
156
156
  return value;
157
157
  }
158
+ function normalizeEnvironmentValues(env) {
159
+ return Object.fromEntries(
160
+ Object.entries(env ?? {}).filter((entry) => typeof entry[1] === "string").map(([key, value]) => [key, value])
161
+ );
162
+ }
163
+ function resolveReconcileEnvironmentValues(input, scope) {
164
+ if (scope === "local") {
165
+ return resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
166
+ }
167
+ const values = {
168
+ ...normalizeEnvironmentValues(process.env),
169
+ ...normalizeEnvironmentValues(input.context.launchEnv)
170
+ };
171
+ return values;
172
+ }
158
173
  function buildCloudflareEnv(input) {
159
174
  const scope = scopeFromTarget(toDeployTarget(input.context.target));
160
- const machineValues = resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
175
+ const values = resolveReconcileEnvironmentValues(input, scope);
161
176
  return {
162
- CLOUDFLARE_ACCOUNT_ID: machineValues.CLOUDFLARE_ACCOUNT_ID ?? input.context.launchEnv.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? resolveConfiguredCloudflareAccountId(input.context.deployConfig),
163
- CLOUDFLARE_API_TOKEN: machineValues.CLOUDFLARE_API_TOKEN ?? input.context.launchEnv.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
177
+ CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID ?? input.context.launchEnv.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? resolveConfiguredCloudflareAccountId(input.context.deployConfig),
178
+ CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN ?? input.context.launchEnv.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
164
179
  };
165
180
  }
166
181
  function hasLiveResourceId(value) {
167
182
  return typeof value === "string" && value.length > 0 && !value.startsWith("dryrun-") && !value.startsWith("local-") && !value.endsWith("-id") && !value.endsWith("-preview-id");
168
183
  }
169
184
  function buildRailwayEnv(input, scope) {
170
- const machineValues = resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
185
+ const values = resolveReconcileEnvironmentValues(input, scope);
171
186
  const token = [
172
- machineValues.RAILWAY_API_TOKEN,
187
+ values.RAILWAY_API_TOKEN,
173
188
  input.context.launchEnv.RAILWAY_API_TOKEN,
174
189
  process.env.RAILWAY_API_TOKEN
175
190
  ].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
176
191
  return {
177
192
  RAILWAY_API_TOKEN: token,
178
- TREESEED_RAILWAY_API_URL: machineValues.TREESEED_RAILWAY_API_URL ?? input.context.launchEnv.TREESEED_RAILWAY_API_URL ?? process.env.TREESEED_RAILWAY_API_URL ?? "",
179
- TREESEED_RAILWAY_WORKSPACE: machineValues.TREESEED_RAILWAY_WORKSPACE ?? input.context.launchEnv.TREESEED_RAILWAY_WORKSPACE ?? process.env.TREESEED_RAILWAY_WORKSPACE ?? ""
193
+ TREESEED_RAILWAY_API_URL: values.TREESEED_RAILWAY_API_URL ?? input.context.launchEnv.TREESEED_RAILWAY_API_URL ?? process.env.TREESEED_RAILWAY_API_URL ?? "",
194
+ TREESEED_RAILWAY_WORKSPACE: values.TREESEED_RAILWAY_WORKSPACE ?? input.context.launchEnv.TREESEED_RAILWAY_WORKSPACE ?? process.env.TREESEED_RAILWAY_WORKSPACE ?? ""
180
195
  };
181
196
  }
182
197
  function findCloudflareQueueByName(input, env, expectedName, { attempts = 6, delayMs = 350 } = {}) {
@@ -434,7 +449,7 @@ ${result.stdout ?? ""}`;
434
449
  function collectCloudflareEnvironmentSync(input) {
435
450
  const target = toDeployTarget(input.context.target);
436
451
  const scope = scopeFromTarget(target);
437
- const values = resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
452
+ const values = resolveReconcileEnvironmentValues(input, scope);
438
453
  const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
439
454
  const state = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target });
440
455
  const generatedSecrets = buildSecretMap(input.context.deployConfig, state);
@@ -1463,7 +1478,7 @@ async function observeRailwayUnit(input, { refresh = false } = {}) {
1463
1478
  }
1464
1479
  function collectRailwayEnvironmentSync(input) {
1465
1480
  const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
1466
- const values = resolveTreeseedMachineEnvironmentValues(input.context.tenantRoot, scope);
1481
+ const values = resolveReconcileEnvironmentValues(input, scope);
1467
1482
  const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
1468
1483
  const state = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target: toDeployTarget(input.context.target) });
1469
1484
  const secrets = Object.fromEntries(
@@ -49,7 +49,7 @@ function deriveTreeseedDesiredUnits({
49
49
  binding: legacyState.queues.agentWork.binding
50
50
  },
51
51
  secrets: {},
52
- metadata: {}
52
+ metadata: { bootstrapSystem: "data" }
53
53
  });
54
54
  const databaseId = add({
55
55
  unitId: createTreeseedReconcileUnitId("database", legacyState.d1Databases.SITE_DATA_DB.databaseName),
@@ -64,7 +64,7 @@ function deriveTreeseedDesiredUnits({
64
64
  binding: "SITE_DATA_DB"
65
65
  },
66
66
  secrets: {},
67
- metadata: {}
67
+ metadata: { bootstrapSystem: "data" }
68
68
  });
69
69
  const formGuardKvId = add({
70
70
  unitId: createTreeseedReconcileUnitId("kv-form-guard", legacyState.kvNamespaces.FORM_GUARD_KV.name),
@@ -76,7 +76,7 @@ function deriveTreeseedDesiredUnits({
76
76
  dependencies: [],
77
77
  spec: { binding: "FORM_GUARD_KV", name: legacyState.kvNamespaces.FORM_GUARD_KV.name },
78
78
  secrets: {},
79
- metadata: {}
79
+ metadata: { bootstrapSystem: "web" }
80
80
  });
81
81
  const sessionKvId = add({
82
82
  unitId: createTreeseedReconcileUnitId("kv-session", legacyState.kvNamespaces.SESSION.name),
@@ -88,7 +88,7 @@ function deriveTreeseedDesiredUnits({
88
88
  dependencies: [],
89
89
  spec: { binding: "SESSION", name: legacyState.kvNamespaces.SESSION.name },
90
90
  secrets: {},
91
- metadata: {}
91
+ metadata: { bootstrapSystem: "web" }
92
92
  });
93
93
  const contentStoreId = add({
94
94
  unitId: createTreeseedReconcileUnitId("content-store", legacyState.content.bucketName ?? deployConfig.slug),
@@ -106,7 +106,7 @@ function deriveTreeseedDesiredUnits({
106
106
  previewRootTemplate: legacyState.content.previewRootTemplate
107
107
  },
108
108
  secrets: {},
109
- metadata: { shared: true }
109
+ metadata: { shared: true, bootstrapSystem: "data" }
110
110
  });
111
111
  const pagesProjectId = add({
112
112
  unitId: createTreeseedReconcileUnitId("pages-project", legacyState.pages.projectName),
@@ -123,7 +123,7 @@ function deriveTreeseedDesiredUnits({
123
123
  buildOutputDir: legacyState.pages.buildOutputDir
124
124
  },
125
125
  secrets: {},
126
- metadata: {}
126
+ metadata: { bootstrapSystem: "web" }
127
127
  });
128
128
  const edgeWorkerId = add({
129
129
  unitId: createTreeseedReconcileUnitId("edge-worker", legacyState.workerName),
@@ -137,7 +137,7 @@ function deriveTreeseedDesiredUnits({
137
137
  workerName: legacyState.workerName
138
138
  },
139
139
  secrets: {},
140
- metadata: {}
140
+ metadata: { bootstrapSystem: "web" }
141
141
  });
142
142
  if (deployConfig.surfaces?.web?.enabled !== false) {
143
143
  const scope2 = target.kind === "persistent" ? target.scope : "staging";
@@ -155,7 +155,7 @@ function deriveTreeseedDesiredUnits({
155
155
  projectName: legacyState.pages.projectName
156
156
  },
157
157
  secrets: {},
158
- metadata: { surface: "web" }
158
+ metadata: { surface: "web", bootstrapSystem: "web" }
159
159
  }) : null;
160
160
  const webDnsUnitId = webDomain ? add({
161
161
  unitId: createTreeseedReconcileUnitId("dns-record", `web:${webDomain}`),
@@ -175,7 +175,7 @@ function deriveTreeseedDesiredUnits({
175
175
  targetKind: "pages-project"
176
176
  },
177
177
  secrets: {},
178
- metadata: { surface: "web" }
178
+ metadata: { surface: "web", bootstrapSystem: "web" }
179
179
  }) : null;
180
180
  add({
181
181
  unitId: createTreeseedReconcileUnitId("web-ui", "web-ui"),
@@ -196,7 +196,7 @@ function deriveTreeseedDesiredUnits({
196
196
  localBaseUrl: deployConfig.surfaces?.web?.localBaseUrl ?? null
197
197
  },
198
198
  secrets: {},
199
- metadata: {}
199
+ metadata: { bootstrapSystem: "web" }
200
200
  });
201
201
  }
202
202
  const scope = target.kind === "persistent" ? target.scope : "staging";
@@ -208,6 +208,7 @@ function deriveTreeseedDesiredUnits({
208
208
  continue;
209
209
  }
210
210
  const concreteType = railwayConcreteUnitTypeForServiceKey(serviceKey);
211
+ const serviceBootstrapSystem = serviceKey === "api" ? "api" : "agents";
211
212
  const concreteId = add({
212
213
  unitId: createTreeseedReconcileUnitId(concreteType, serviceState?.serviceName ?? configuredService.serviceName ?? serviceKey),
213
214
  unitType: concreteType,
@@ -238,7 +239,8 @@ function deriveTreeseedDesiredUnits({
238
239
  serviceKey,
239
240
  scheduleManaged: Array.isArray(configuredService.schedule) && configuredService.schedule.length > 0,
240
241
  scheduleBootstrap: false,
241
- scheduleDeployScopes: ["prod"]
242
+ scheduleDeployScopes: ["prod"],
243
+ bootstrapSystem: serviceBootstrapSystem
242
244
  }
243
245
  });
244
246
  const apiDomain = serviceKey === "api" && target.kind === "persistent" ? resolveConfiguredSurfaceDomain(deployConfig, target, "api") : null;
@@ -257,7 +259,7 @@ function deriveTreeseedDesiredUnits({
257
259
  environment: normalizeRailwayEnvironmentName(serviceState?.environment ?? configuredService.railwayEnvironment)
258
260
  },
259
261
  secrets: {},
260
- metadata: { surface: "api", serviceKey }
262
+ metadata: { surface: "api", serviceKey, bootstrapSystem: "api" }
261
263
  }) : null;
262
264
  const apiDnsUnitId = apiDomain ? add({
263
265
  unitId: createTreeseedReconcileUnitId("dns-record", `api:${apiDomain}`),
@@ -274,7 +276,7 @@ function deriveTreeseedDesiredUnits({
274
276
  serviceKey
275
277
  },
276
278
  secrets: {},
277
- metadata: { surface: "api", serviceKey }
279
+ metadata: { surface: "api", serviceKey, bootstrapSystem: "api" }
278
280
  }) : null;
279
281
  const runtimeUnitType = (() => {
280
282
  switch (serviceKey) {
@@ -309,7 +311,7 @@ function deriveTreeseedDesiredUnits({
309
311
  publicBaseUrl: serviceState?.publicBaseUrl ?? null
310
312
  },
311
313
  secrets: {},
312
- metadata: {}
314
+ metadata: { bootstrapSystem: serviceBootstrapSystem }
313
315
  });
314
316
  }
315
317
  return { deployConfig, legacyState, units };
@@ -1,8 +1,10 @@
1
1
  import type { TreeseedObservedUnitState, TreeseedReconcilePlan, TreeseedReconcileResult, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitVerificationResult } from './contracts.ts';
2
- export declare function observeTreeseedUnits({ tenantRoot, target, env, write, }: {
2
+ import { type TreeseedRunnableBootstrapSystem } from './bootstrap-systems.ts';
3
+ export declare function observeTreeseedUnits({ tenantRoot, target, env, systems, write, }: {
3
4
  tenantRoot: string;
4
5
  target: TreeseedReconcileTarget;
5
6
  env?: NodeJS.ProcessEnv;
7
+ systems?: TreeseedRunnableBootstrapSystem[];
6
8
  write?: (line: string) => void;
7
9
  }): Promise<{
8
10
  units: import("./contracts.ts").TreeseedDesiredUnit[];
@@ -180,10 +182,11 @@ export declare function observeTreeseedUnits({ tenantRoot, target, env, write, }
180
182
  };
181
183
  };
182
184
  }>;
183
- export declare function planTreeseedReconciliation({ tenantRoot, target, env, write, }: {
185
+ export declare function planTreeseedReconciliation({ tenantRoot, target, env, systems, write, }: {
184
186
  tenantRoot: string;
185
187
  target: TreeseedReconcileTarget;
186
188
  env?: NodeJS.ProcessEnv;
189
+ systems?: TreeseedRunnableBootstrapSystem[];
187
190
  write?: (line: string) => void;
188
191
  }): Promise<{
189
192
  plans: TreeseedReconcilePlan[];
@@ -362,10 +365,11 @@ export declare function planTreeseedReconciliation({ tenantRoot, target, env, wr
362
365
  };
363
366
  };
364
367
  }>;
365
- export declare function reconcileTreeseedTarget({ tenantRoot, target, env, write, }: {
368
+ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, write, }: {
366
369
  tenantRoot: string;
367
370
  target: TreeseedReconcileTarget;
368
371
  env?: NodeJS.ProcessEnv;
372
+ systems?: TreeseedRunnableBootstrapSystem[];
369
373
  write?: (line: string) => void;
370
374
  }): Promise<{
371
375
  target: TreeseedReconcileTarget;
@@ -383,10 +387,11 @@ export declare function destroyTreeseedTargetUnits({ tenantRoot, target, env, wr
383
387
  target: TreeseedReconcileTarget;
384
388
  results: TreeseedReconcileResult[];
385
389
  }>;
386
- export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env, }: {
390
+ export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env, systems, }: {
387
391
  tenantRoot: string;
388
392
  target: TreeseedReconcileTarget;
389
393
  env?: NodeJS.ProcessEnv;
394
+ systems?: TreeseedRunnableBootstrapSystem[];
390
395
  }): Promise<{
391
396
  target: TreeseedReconcileTarget;
392
397
  ready: boolean;
@@ -2,6 +2,7 @@ import { createTreeseedReconcileRegistry } from "./registry.js";
2
2
  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
+ import { filterTreeseedDesiredUnitsByBootstrapSystems } from "./bootstrap-systems.js";
5
6
  function nowIso() {
6
7
  return (/* @__PURE__ */ new Date()).toISOString();
7
8
  }
@@ -35,6 +36,33 @@ function createRunContext(tenantRoot, target, launchEnv, write) {
35
36
  session: /* @__PURE__ */ new Map()
36
37
  };
37
38
  }
39
+ function dependencyLevels(units) {
40
+ const remaining = new Map(units.map((unit) => [unit.unitId, unit]));
41
+ const completed = /* @__PURE__ */ new Set();
42
+ const levels = [];
43
+ while (remaining.size > 0) {
44
+ const ready = [...remaining.values()].filter(
45
+ (unit) => unit.dependencies.every((dependencyId) => completed.has(dependencyId) || !remaining.has(dependencyId))
46
+ );
47
+ if (ready.length === 0) {
48
+ topologicallySortDesiredUnits(units);
49
+ throw new Error("Treeseed reconcile dependency graph could not be scheduled.");
50
+ }
51
+ for (const unit of ready) {
52
+ remaining.delete(unit.unitId);
53
+ completed.add(unit.unitId);
54
+ }
55
+ levels.push(ready);
56
+ }
57
+ return levels;
58
+ }
59
+ async function runByDependencyLevel(units, action) {
60
+ const results = [];
61
+ for (const level of dependencyLevels(units)) {
62
+ results.push(...await Promise.all(level.map((unit) => action(unit))));
63
+ }
64
+ return results;
65
+ }
38
66
  function persistResult(reconcileState, previous, result) {
39
67
  updateTreeseedPersistedUnitState(reconcileState, {
40
68
  ...previous,
@@ -98,14 +126,17 @@ async function observeTreeseedUnits({
98
126
  tenantRoot,
99
127
  target,
100
128
  env = process.env,
129
+ systems,
101
130
  write
102
131
  }) {
103
- const { units, deployConfig } = deriveTreeseedDesiredUnits({ tenantRoot, target });
132
+ const derived = deriveTreeseedDesiredUnits({ tenantRoot, target });
133
+ const units = filterTreeseedDesiredUnitsByBootstrapSystems(derived.units, systems);
134
+ const deployConfig = derived.deployConfig;
104
135
  const registry = createTreeseedReconcileRegistry(deployConfig);
105
136
  const reconcileState = loadTreeseedReconcileState(tenantRoot, target);
106
137
  const context = createRunContext(tenantRoot, target, env, write);
107
138
  const observations = /* @__PURE__ */ new Map();
108
- for (const unit of topologicallySortDesiredUnits(units)) {
139
+ await runByDependencyLevel(topologicallySortDesiredUnits(units), async (unit) => {
109
140
  write?.(`Observing ${unit.provider}:${unit.unitType}...`);
110
141
  const adapter = registry.get(unit.unitType, unit.provider);
111
142
  const persisted = ensureTreeseedPersistedUnitState(reconcileState, unit);
@@ -120,7 +151,7 @@ async function observeTreeseedUnits({
120
151
  wrapAdapterFailure("observe", unit.provider, unit.unitType, unit.unitId, error);
121
152
  }
122
153
  observations.set(unit.unitId, observed);
123
- }
154
+ });
124
155
  return {
125
156
  units,
126
157
  observations,
@@ -132,13 +163,14 @@ async function planTreeseedReconciliation({
132
163
  tenantRoot,
133
164
  target,
134
165
  env = process.env,
166
+ systems,
135
167
  write
136
168
  }) {
137
- const observed = await observeTreeseedUnits({ tenantRoot, target, env, write });
169
+ const observed = await observeTreeseedUnits({ tenantRoot, target, env, systems, write });
138
170
  const registry = createTreeseedReconcileRegistry(observed.deployConfig);
139
171
  const context = createRunContext(tenantRoot, target, env, write);
140
172
  const plans = [];
141
- for (const unit of topologicallySortDesiredUnits(observed.units)) {
173
+ await runByDependencyLevel(topologicallySortDesiredUnits(observed.units), async (unit) => {
142
174
  const adapter = registry.get(unit.unitType, unit.provider);
143
175
  const persisted = ensureTreeseedPersistedUnitState(observed.state, unit);
144
176
  const observation = observed.observations.get(unit.unitId);
@@ -159,7 +191,7 @@ async function planTreeseedReconciliation({
159
191
  diff,
160
192
  persisted
161
193
  });
162
- }
194
+ });
163
195
  return {
164
196
  ...observed,
165
197
  plans
@@ -169,15 +201,26 @@ async function reconcileTreeseedTarget({
169
201
  tenantRoot,
170
202
  target,
171
203
  env = process.env,
204
+ systems,
172
205
  write
173
206
  }) {
174
- const planned = await planTreeseedReconciliation({ tenantRoot, target, env, write });
207
+ const planned = await planTreeseedReconciliation({ tenantRoot, target, env, systems, write });
175
208
  const registry = createTreeseedReconcileRegistry(planned.deployConfig);
176
209
  const context = createRunContext(tenantRoot, target, env, write);
177
210
  const results = [];
178
211
  const verificationMap = /* @__PURE__ */ new Map();
179
212
  context.session.set("treeseed:verification-results", verificationMap);
180
- for (const plan of planned.plans) {
213
+ const planByUnitId = new Map(planned.plans.map((plan) => [plan.unit.unitId, plan]));
214
+ let persistChain = Promise.resolve();
215
+ const persistVerifiedResult = async (persisted, verifiedResult) => {
216
+ persistChain = persistChain.then(() => {
217
+ persistResult(planned.state, persisted, verifiedResult);
218
+ writeTreeseedReconcileState(tenantRoot, planned.state);
219
+ });
220
+ await persistChain;
221
+ };
222
+ await runByDependencyLevel(topologicallySortDesiredUnits(planned.units), async (unit) => {
223
+ const plan = planByUnitId.get(unit.unitId);
181
224
  write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType}...`);
182
225
  const adapter = registry.get(plan.unit.unitType, plan.unit.provider);
183
226
  const persisted = ensureTreeseedPersistedUnitState(planned.state, plan.unit);
@@ -232,13 +275,12 @@ async function reconcileTreeseedTarget({
232
275
  };
233
276
  verificationMap.set(plan.unit.unitId, verification);
234
277
  if (!verification.verified) {
235
- persistResult(planned.state, persisted, verifiedResult);
236
- writeTreeseedReconcileState(tenantRoot, planned.state);
278
+ await persistVerifiedResult(persisted, verifiedResult);
237
279
  throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
238
280
  }
239
- persistResult(planned.state, persisted, verifiedResult);
281
+ await persistVerifiedResult(persisted, verifiedResult);
240
282
  results.push(verifiedResult);
241
- }
283
+ });
242
284
  writeTreeseedReconcileState(tenantRoot, planned.state);
243
285
  return {
244
286
  target,
@@ -294,9 +336,10 @@ async function destroyTreeseedTargetUnits({
294
336
  async function collectTreeseedReconcileStatus({
295
337
  tenantRoot,
296
338
  target,
297
- env = process.env
339
+ env = process.env,
340
+ systems
298
341
  }) {
299
- const observed = await observeTreeseedUnits({ tenantRoot, target, env });
342
+ const observed = await observeTreeseedUnits({ tenantRoot, target, env, systems });
300
343
  const registry = createTreeseedReconcileRegistry(observed.deployConfig);
301
344
  const context = createRunContext(tenantRoot, target, env);
302
345
  const plans = await Promise.all(observed.units.map(async (unit) => {
@@ -1,4 +1,5 @@
1
1
  export * from './contracts.ts';
2
+ export * from './bootstrap-systems.ts';
2
3
  export * from './desired-state.ts';
3
4
  export * from './engine.ts';
4
5
  export * from './errors.ts';
@@ -1,4 +1,5 @@
1
1
  export * from "./contracts.js";
2
+ export * from "./bootstrap-systems.js";
2
3
  export * from "./desired-state.js";
3
4
  export * from "./engine.js";
4
5
  export * from "./errors.js";
@@ -5,6 +5,9 @@ function parseArgs(argv) {
5
5
  const parsed = {
6
6
  scopes: [],
7
7
  sync: 'all',
8
+ systems: [],
9
+ skipUnavailable: undefined,
10
+ bootstrapExecution: 'parallel',
8
11
  bootstrap: false,
9
12
  rotateMachineKey: false,
10
13
  };
@@ -34,6 +37,30 @@ function parseArgs(argv) {
34
37
  parsed.bootstrap = true;
35
38
  continue;
36
39
  }
40
+ if (current === '--system') {
41
+ parsed.systems.push(rest.shift() ?? '');
42
+ continue;
43
+ }
44
+ if (current.startsWith('--system=')) {
45
+ parsed.systems.push(current.split('=', 2)[1] ?? '');
46
+ continue;
47
+ }
48
+ if (current === '--systems') {
49
+ parsed.systems.push(...String(rest.shift() ?? '').split(','));
50
+ continue;
51
+ }
52
+ if (current.startsWith('--systems=')) {
53
+ parsed.systems.push(...String(current.split('=', 2)[1] ?? '').split(','));
54
+ continue;
55
+ }
56
+ if (current === '--skip-unavailable') {
57
+ parsed.skipUnavailable = true;
58
+ continue;
59
+ }
60
+ if (current === '--bootstrap-sequential') {
61
+ parsed.bootstrapExecution = 'sequential';
62
+ continue;
63
+ }
37
64
  if (current === '--rotate-machine-key') {
38
65
  parsed.rotateMachineKey = true;
39
66
  continue;
@@ -75,6 +102,9 @@ try {
75
102
  tenantRoot,
76
103
  scopes,
77
104
  sync: options.sync,
105
+ systems: options.systems.length > 0 ? options.systems : undefined,
106
+ skipUnavailable: options.skipUnavailable,
107
+ bootstrapExecution: options.bootstrapExecution,
78
108
  env: process.env,
79
109
  });
80
110
  const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
@@ -34,8 +34,6 @@ export function resolveWranglerBin() {
34
34
  export function createProductionBuildEnv(extraEnv = {}) {
35
35
  return {
36
36
  TREESEED_LOCAL_DEV_MODE: 'cloudflare',
37
- TREESEED_PUBLIC_FORMS_LOCAL_BYPASS_TURNSTILE: '',
38
- TREESEED_FORMS_LOCAL_BYPASS_TURNSTILE: '',
39
37
  TREESEED_FORMS_LOCAL_BYPASS_CLOUDFLARE_GUARDS: '',
40
38
  TREESEED_PUBLIC_DEV_WATCH_RELOAD: '',
41
39
  ...extraEnv,