@treeseed/sdk 0.10.23 → 0.10.25

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 (49) hide show
  1. package/dist/index.d.ts +12 -2
  2. package/dist/index.js +42 -1
  3. package/dist/market-client.d.ts +23 -0
  4. package/dist/market-client.js +30 -0
  5. package/dist/operations/providers/default.js +103 -10
  6. package/dist/operations/repository-operations.d.ts +6 -1
  7. package/dist/operations/repository-operations.js +44 -0
  8. package/dist/operations/services/bootstrap-runner.d.ts +5 -1
  9. package/dist/operations/services/bootstrap-runner.js +34 -5
  10. package/dist/operations/services/config-runtime.d.ts +25 -9
  11. package/dist/operations/services/config-runtime.js +60 -12
  12. package/dist/operations/services/deploy.js +6 -1
  13. package/dist/operations/services/hub-launch.js +1 -0
  14. package/dist/operations/services/hub-provider-launch.d.ts +11 -1
  15. package/dist/operations/services/hub-provider-launch.js +81 -8
  16. package/dist/operations/services/project-host-operations.d.ts +153 -0
  17. package/dist/operations/services/project-host-operations.js +365 -0
  18. package/dist/operations/services/project-platform.d.ts +207 -177
  19. package/dist/operations/services/project-platform.js +96 -29
  20. package/dist/operations/services/railway-deploy.d.ts +33 -1
  21. package/dist/operations/services/railway-deploy.js +153 -44
  22. package/dist/operations/services/release-candidate.js +8 -2
  23. package/dist/operations/services/template-host-bindings.d.ts +68 -0
  24. package/dist/operations/services/template-host-bindings.js +400 -0
  25. package/dist/operations/services/template-registry.d.ts +22 -2
  26. package/dist/operations/services/template-registry.js +93 -6
  27. package/dist/operations/services/template-secret-sync.d.ts +97 -0
  28. package/dist/operations/services/template-secret-sync.js +292 -0
  29. package/dist/platform/contracts.d.ts +1 -0
  30. package/dist/platform/deploy-config.js +8 -1
  31. package/dist/platform/deploy-runtime.js +1 -0
  32. package/dist/platform/environment.d.ts +3 -0
  33. package/dist/project-workflow.d.ts +7 -1
  34. package/dist/reconcile/engine.d.ts +2 -0
  35. package/dist/reconcile/engine.js +58 -3
  36. package/dist/scripts/scaffold-site.js +3 -2
  37. package/dist/scripts/test-scaffold.js +2 -1
  38. package/dist/sdk-types.d.ts +87 -0
  39. package/dist/sdk-types.js +29 -0
  40. package/dist/template-catalog.js +3 -1
  41. package/dist/template-launch-requirements.d.ts +118 -0
  42. package/dist/template-launch-requirements.js +759 -0
  43. package/dist/template-launch-ui.d.ts +85 -0
  44. package/dist/template-launch-ui.js +189 -0
  45. package/dist/timing.d.ts +20 -0
  46. package/dist/timing.js +73 -0
  47. package/dist/treeseed/template-catalog/catalog.fixture.json +477 -0
  48. package/package.json +13 -1
  49. package/templates/github/deploy-web.workflow.yml +4 -0
@@ -0,0 +1,97 @@
1
+ import { syncTreeseedCloudflareEnvironment, syncTreeseedGitHubEnvironment, syncTreeseedRailwayEnvironment, type TreeseedConfigScope } from './config-runtime.ts';
2
+ import type { ProjectLaunchResolvedHostBinding, ProjectLaunchSecretDeploymentPlanItem } from '../../template-launch-requirements.ts';
3
+ import type { ProjectLaunchRequirementKind } from '../../sdk-types.ts';
4
+ export type ProjectLaunchSecretSyncProvider = 'github' | 'cloudflare' | 'railway';
5
+ export type ProjectLaunchSecretSyncStatus = 'synced' | 'skipped' | 'failed';
6
+ export type ProjectLaunchSecretSyncProviderStatus = 'running' | 'completed' | 'failed';
7
+ export type ProjectLaunchSecretSyncTargetKind = 'github-secret' | 'github-variable' | 'cloudflare-secret' | 'cloudflare-var' | 'railway-secret' | 'railway-var';
8
+ export interface ProjectLaunchSecretValueDiagnostic {
9
+ code: 'missing_value' | 'unsupported_target';
10
+ requirementKey: string;
11
+ requirementKind: ProjectLaunchRequirementKind;
12
+ env: string;
13
+ source: string;
14
+ targets: string[];
15
+ scopes: TreeseedConfigScope[];
16
+ message: string;
17
+ }
18
+ export interface ProjectLaunchResolvedSecretValueItem {
19
+ requirementKey: string;
20
+ requirementKind: ProjectLaunchRequirementKind;
21
+ env: string;
22
+ source: string;
23
+ targets: ProjectLaunchSecretSyncTargetKind[];
24
+ scopes: TreeseedConfigScope[];
25
+ sensitivity: string;
26
+ sourceHostId?: string | null;
27
+ resolved: boolean;
28
+ }
29
+ export interface ProjectLaunchSecretValueOverlayResult {
30
+ valuesOverlay: Record<string, string>;
31
+ items: ProjectLaunchResolvedSecretValueItem[];
32
+ diagnostics: ProjectLaunchSecretValueDiagnostic[];
33
+ }
34
+ export interface ProjectLaunchSecretSyncSummaryItem {
35
+ provider: ProjectLaunchSecretSyncProvider;
36
+ scope: TreeseedConfigScope;
37
+ target: ProjectLaunchSecretSyncTargetKind;
38
+ env: string;
39
+ requirementKey: string;
40
+ requirementKind: ProjectLaunchRequirementKind;
41
+ sensitivity: string;
42
+ status: ProjectLaunchSecretSyncStatus;
43
+ error?: {
44
+ message: string;
45
+ };
46
+ }
47
+ export interface ProjectLaunchSecretSyncProviderSummary {
48
+ provider: ProjectLaunchSecretSyncProvider;
49
+ scope: TreeseedConfigScope;
50
+ entryIds: string[];
51
+ status: Exclude<ProjectLaunchSecretSyncProviderStatus, 'running'>;
52
+ error?: {
53
+ message: string;
54
+ };
55
+ }
56
+ export interface ProjectLaunchSecretSyncProgressEvent {
57
+ provider: ProjectLaunchSecretSyncProvider;
58
+ scope: TreeseedConfigScope;
59
+ status: ProjectLaunchSecretSyncProviderStatus;
60
+ entryIds: string[];
61
+ message: string;
62
+ }
63
+ export interface ProjectLaunchSecretSyncResult {
64
+ ok: boolean;
65
+ items: ProjectLaunchSecretSyncSummaryItem[];
66
+ providers: ProjectLaunchSecretSyncProviderSummary[];
67
+ diagnostics: ProjectLaunchSecretValueDiagnostic[];
68
+ }
69
+ export interface ProjectLaunchSecretSyncAdapters {
70
+ github?: typeof syncTreeseedGitHubEnvironment;
71
+ cloudflare?: typeof syncTreeseedCloudflareEnvironment;
72
+ railway?: typeof syncTreeseedRailwayEnvironment;
73
+ }
74
+ export interface ResolveProjectLaunchSecretValueOverlayOptions {
75
+ hostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
76
+ secretDeploymentPlan?: {
77
+ items?: ProjectLaunchSecretDeploymentPlanItem[] | null;
78
+ } | null;
79
+ valuesOverlay?: Record<string, string | undefined> | null;
80
+ valuesByScope?: Partial<Record<TreeseedConfigScope, Record<string, string | undefined> | null>> | null;
81
+ processEnv?: Record<string, string | undefined>;
82
+ scopes?: TreeseedConfigScope[];
83
+ }
84
+ export interface SyncProjectLaunchHostBindingSecretsOptions extends ResolveProjectLaunchSecretValueOverlayOptions {
85
+ projectRoot: string;
86
+ repository?: string | null;
87
+ dryRun?: boolean;
88
+ providers?: ProjectLaunchSecretSyncProvider[];
89
+ onProgress?: (event: ProjectLaunchSecretSyncProgressEvent) => void | Promise<void>;
90
+ adapters?: ProjectLaunchSecretSyncAdapters;
91
+ }
92
+ export declare class ProjectLaunchSecretSyncError extends Error {
93
+ readonly result: ProjectLaunchSecretSyncResult;
94
+ constructor(message: string, result: ProjectLaunchSecretSyncResult);
95
+ }
96
+ export declare function resolveProjectLaunchSecretValueOverlay(options: ResolveProjectLaunchSecretValueOverlayOptions): ProjectLaunchSecretValueOverlayResult;
97
+ export declare function syncProjectLaunchHostBindingSecrets(options: SyncProjectLaunchHostBindingSecretsOptions): Promise<ProjectLaunchSecretSyncResult>;
@@ -0,0 +1,292 @@
1
+ import {
2
+ syncTreeseedCloudflareEnvironment,
3
+ syncTreeseedGitHubEnvironment,
4
+ syncTreeseedRailwayEnvironment
5
+ } from "./config-runtime.js";
6
+ const PROVIDER_TARGETS = {
7
+ github: ["github-secret", "github-variable"],
8
+ cloudflare: ["cloudflare-secret", "cloudflare-var"],
9
+ railway: ["railway-secret", "railway-var"]
10
+ };
11
+ const DEFAULT_SCOPES = ["staging", "prod"];
12
+ const NON_PROVIDER_TARGETS = /* @__PURE__ */ new Set(["local-runtime", "local-cloudflare", "config-file"]);
13
+ const SECRET_TOKEN_PATTERN = /(?:gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{16,}|[A-Za-z0-9+/=]{32,})/gu;
14
+ class ProjectLaunchSecretSyncError extends Error {
15
+ result;
16
+ constructor(message, result) {
17
+ super(message);
18
+ this.name = "ProjectLaunchSecretSyncError";
19
+ this.result = result;
20
+ }
21
+ }
22
+ function stringValue(value) {
23
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : "";
24
+ }
25
+ function normalizeSecretTargets(targets) {
26
+ const allowed = new Set(Object.values(PROVIDER_TARGETS).flat());
27
+ return targets.map((target) => String(target ?? "").trim()).filter((target) => allowed.has(target));
28
+ }
29
+ function normalizeScopes(items, requested) {
30
+ const scopes = new Set(requested?.length ? requested : DEFAULT_SCOPES);
31
+ for (const item of items) {
32
+ for (const scope of item.scopes ?? []) {
33
+ if (scope === "staging" || scope === "prod") {
34
+ scopes.add(scope);
35
+ }
36
+ }
37
+ }
38
+ return [...scopes];
39
+ }
40
+ function valueCandidatesForSource(source) {
41
+ const value = String(source ?? "").trim();
42
+ if (!value) return [];
43
+ const [, suffix = ""] = /^(?:selectedHost|host)\.(?:secret|config|environment|env):(.+)$/u.exec(value) ?? [];
44
+ if (suffix) {
45
+ return [suffix, suffix.toUpperCase(), `TREESEED_${suffix.toUpperCase()}`];
46
+ }
47
+ const [, dotted = ""] = /^(?:selectedHost|host)\.([A-Za-z0-9_]+)$/u.exec(value) ?? [];
48
+ if (dotted) {
49
+ return [dotted, dotted.toUpperCase(), `TREESEED_${dotted.toUpperCase()}`];
50
+ }
51
+ return [];
52
+ }
53
+ function scopedOverlay(options, scope) {
54
+ return {
55
+ ...options.processEnv ?? process.env,
56
+ ...options.valuesOverlay ?? {},
57
+ ...options.valuesByScope?.[scope] ?? {}
58
+ };
59
+ }
60
+ function bindingValue(binding, key) {
61
+ if (!binding || !key) return "";
62
+ const record = binding;
63
+ for (const containerKey of ["environmentValues", "configValues", "secrets", "secretValues", "metadata"]) {
64
+ const container = record[containerKey];
65
+ if (container && typeof container === "object") {
66
+ const value = stringValue(container[key]) || stringValue(container[key.toUpperCase()]);
67
+ if (value) return value;
68
+ }
69
+ }
70
+ return stringValue(record[key]) || stringValue(record[key.toUpperCase()]);
71
+ }
72
+ function resolvePlanItemValue(item, scope, options) {
73
+ const values = scopedOverlay(options, scope);
74
+ const direct = stringValue(values[item.env]);
75
+ if (direct) return direct;
76
+ const binding = item.sourceHostId ? options.hostBindings?.[item.requirementKey] : void 0;
77
+ for (const candidate of valueCandidatesForSource(item.source)) {
78
+ const overlayValue = stringValue(values[candidate]);
79
+ if (overlayValue) return overlayValue;
80
+ const hostValue = bindingValue(binding, candidate);
81
+ if (hostValue) return hostValue;
82
+ }
83
+ return "";
84
+ }
85
+ function providerForTarget(target) {
86
+ if (target.startsWith("github-")) return "github";
87
+ if (target.startsWith("cloudflare-")) return "cloudflare";
88
+ return "railway";
89
+ }
90
+ function redactSecretSyncMessage(message) {
91
+ return String(message instanceof Error ? message.message : message ?? "Secret sync failed.").replace(SECRET_TOKEN_PATTERN, "[redacted]").replace(/(token|password|secret|key)=([^,\s]+)/giu, "$1=[redacted]");
92
+ }
93
+ function resolveProjectLaunchSecretValueOverlay(options) {
94
+ const planItems = options.secretDeploymentPlan?.items?.filter(Boolean) ?? [];
95
+ const scopes = normalizeScopes(planItems, options.scopes);
96
+ const valuesOverlay = {};
97
+ const resolvedItems = [];
98
+ const diagnostics = [];
99
+ for (const item of planItems) {
100
+ const targets = normalizeSecretTargets(item.targets ?? []);
101
+ const itemScopes = (item.scopes ?? []).filter((scope) => scope === "local" || scope === "staging" || scope === "prod");
102
+ if (targets.length === 0) {
103
+ if ((item.targets ?? []).every((target) => NON_PROVIDER_TARGETS.has(String(target)))) {
104
+ continue;
105
+ }
106
+ diagnostics.push({
107
+ code: "unsupported_target",
108
+ requirementKey: item.requirementKey,
109
+ requirementKind: item.requirementKind,
110
+ env: item.env,
111
+ source: item.source,
112
+ targets: item.targets ?? [],
113
+ scopes: itemScopes,
114
+ message: `Secret deployment target is not supported for ${item.env}.`
115
+ });
116
+ continue;
117
+ }
118
+ const activeScopes = itemScopes.filter((scope) => scopes.includes(scope));
119
+ let resolved = false;
120
+ for (const scope of activeScopes) {
121
+ const value = resolvePlanItemValue(item, scope, options);
122
+ if (value) {
123
+ valuesOverlay[item.env] = value;
124
+ resolved = true;
125
+ break;
126
+ }
127
+ }
128
+ if (!resolved) {
129
+ diagnostics.push({
130
+ code: "missing_value",
131
+ requirementKey: item.requirementKey,
132
+ requirementKind: item.requirementKind,
133
+ env: item.env,
134
+ source: item.source,
135
+ targets,
136
+ scopes: activeScopes,
137
+ message: `No launch secret value was available for ${item.env}.`
138
+ });
139
+ }
140
+ resolvedItems.push({
141
+ requirementKey: item.requirementKey,
142
+ requirementKind: item.requirementKind,
143
+ env: item.env,
144
+ source: item.source,
145
+ targets,
146
+ scopes: activeScopes,
147
+ sensitivity: item.sensitivity,
148
+ sourceHostId: item.sourceHostId ?? null,
149
+ resolved
150
+ });
151
+ }
152
+ return { valuesOverlay, items: resolvedItems, diagnostics };
153
+ }
154
+ function itemsForProviderScope(items, provider, scope) {
155
+ const targets = new Set(PROVIDER_TARGETS[provider]);
156
+ return items.filter((item) => item.resolved && item.scopes.includes(scope) && item.targets.some((target) => targets.has(target)));
157
+ }
158
+ function summaryItemsFor(items, provider, scope, status, error) {
159
+ const targets = new Set(PROVIDER_TARGETS[provider]);
160
+ return items.flatMap((item) => item.targets.filter((target) => targets.has(target)).map((target) => ({
161
+ provider,
162
+ scope,
163
+ target,
164
+ env: item.env,
165
+ requirementKey: item.requirementKey,
166
+ requirementKind: item.requirementKind,
167
+ sensitivity: item.sensitivity,
168
+ status,
169
+ ...error ? { error: { message: redactSecretSyncMessage(error) } } : {}
170
+ })));
171
+ }
172
+ async function syncProjectLaunchHostBindingSecrets(options) {
173
+ const planItems = options.secretDeploymentPlan?.items?.filter(Boolean) ?? [];
174
+ const scopes = normalizeScopes(planItems, options.scopes);
175
+ const providers = options.providers?.length ? options.providers : Object.keys(PROVIDER_TARGETS);
176
+ const overlay = resolveProjectLaunchSecretValueOverlay({ ...options, scopes });
177
+ const relevantDiagnostics = overlay.diagnostics.filter((diagnostic) => diagnostic.scopes.some((scope) => scopes.includes(scope)) && diagnostic.targets.some((target) => providers.includes(providerForTarget(target))));
178
+ const result = {
179
+ ok: relevantDiagnostics.length === 0,
180
+ items: [],
181
+ providers: [],
182
+ diagnostics: overlay.diagnostics
183
+ };
184
+ if (relevantDiagnostics.length > 0) {
185
+ for (const diagnostic of relevantDiagnostics) {
186
+ for (const target of normalizeSecretTargets(diagnostic.targets)) {
187
+ const provider = providerForTarget(target);
188
+ for (const scope of diagnostic.scopes.filter((entry) => scopes.includes(entry))) {
189
+ result.items.push({
190
+ provider,
191
+ scope,
192
+ target,
193
+ env: diagnostic.env,
194
+ requirementKey: diagnostic.requirementKey,
195
+ requirementKind: diagnostic.requirementKind,
196
+ sensitivity: "secret",
197
+ status: "failed",
198
+ error: { message: diagnostic.message }
199
+ });
200
+ }
201
+ }
202
+ }
203
+ throw new ProjectLaunchSecretSyncError("Host-bound secret sync could not resolve every required value.", result);
204
+ }
205
+ const adapters = {
206
+ github: options.adapters?.github ?? syncTreeseedGitHubEnvironment,
207
+ cloudflare: options.adapters?.cloudflare ?? syncTreeseedCloudflareEnvironment,
208
+ railway: options.adapters?.railway ?? syncTreeseedRailwayEnvironment
209
+ };
210
+ for (const provider of providers) {
211
+ for (const scope of scopes) {
212
+ if (scope === "local") continue;
213
+ const scopedItems = itemsForProviderScope(overlay.items, provider, scope);
214
+ if (scopedItems.length === 0) continue;
215
+ const entryIds = [...new Set(scopedItems.map((item) => item.env))];
216
+ await options.onProgress?.({
217
+ provider,
218
+ scope,
219
+ entryIds,
220
+ status: "running",
221
+ message: `Syncing ${entryIds.length} host-bound ${provider} entr${entryIds.length === 1 ? "y" : "ies"} for ${scope}.`
222
+ });
223
+ try {
224
+ const valuesOverlay = {
225
+ ...overlay.valuesOverlay,
226
+ ...options.valuesByScope?.[scope] ?? {}
227
+ };
228
+ if (provider === "github") {
229
+ await adapters.github({
230
+ tenantRoot: options.projectRoot,
231
+ scope,
232
+ dryRun: options.dryRun,
233
+ repository: options.repository ?? null,
234
+ valuesOverlay,
235
+ entryIds,
236
+ execution: "sequential"
237
+ });
238
+ } else if (provider === "cloudflare") {
239
+ adapters.cloudflare({
240
+ tenantRoot: options.projectRoot,
241
+ scope,
242
+ dryRun: options.dryRun,
243
+ valuesOverlay,
244
+ entryIds
245
+ });
246
+ } else {
247
+ adapters.railway({
248
+ tenantRoot: options.projectRoot,
249
+ scope,
250
+ dryRun: options.dryRun,
251
+ valuesOverlay,
252
+ entryIds
253
+ });
254
+ }
255
+ result.items.push(...summaryItemsFor(scopedItems, provider, scope, "synced"));
256
+ result.providers.push({ provider, scope, entryIds, status: "completed" });
257
+ await options.onProgress?.({
258
+ provider,
259
+ scope,
260
+ entryIds,
261
+ status: "completed",
262
+ message: `Synced host-bound ${provider} entries for ${scope}.`
263
+ });
264
+ } catch (error) {
265
+ result.ok = false;
266
+ result.items.push(...summaryItemsFor(scopedItems, provider, scope, "failed", error));
267
+ result.providers.push({
268
+ provider,
269
+ scope,
270
+ entryIds,
271
+ status: "failed",
272
+ error: { message: redactSecretSyncMessage(error) }
273
+ });
274
+ await options.onProgress?.({
275
+ provider,
276
+ scope,
277
+ entryIds,
278
+ status: "failed",
279
+ message: redactSecretSyncMessage(error)
280
+ });
281
+ throw new ProjectLaunchSecretSyncError(redactSecretSyncMessage(error), result);
282
+ }
283
+ }
284
+ }
285
+ result.ok = result.providers.every((provider) => provider.status === "completed") && result.diagnostics.length === 0;
286
+ return result;
287
+ }
288
+ export {
289
+ ProjectLaunchSecretSyncError,
290
+ resolveProjectLaunchSecretValueOverlay,
291
+ syncProjectLaunchHostBindingSecrets
292
+ };
@@ -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"
@@ -86,6 +86,9 @@ export type TreeseedEnvironmentEntry = {
86
86
  localDefaultValue?: TreeseedEnvironmentValueResolver;
87
87
  isRelevant?: (context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, purpose?: TreeseedEnvironmentPurpose) => boolean;
88
88
  requiredWhen?: (context: TreeseedEnvironmentContext, scope: TreeseedEnvironmentScope, purpose?: TreeseedEnvironmentPurpose) => boolean;
89
+ sourceRequirement?: string;
90
+ sourceHostType?: string | null;
91
+ sourceProvider?: string | null;
89
92
  };
90
93
  export type TreeseedEnvironmentEntryYaml = Omit<TreeseedEnvironmentEntry, 'id' | 'defaultValue' | 'localDefaultValue' | 'isRelevant' | 'requiredWhen'> & {
91
94
  cluster?: string;
@@ -1,4 +1,4 @@
1
- import type { ProjectConnection, RemoteJobStatus } from './sdk-types.ts';
1
+ import type { ProjectConnection, ProjectLaunchHostBindingInput, RemoteJobStatus } from './sdk-types.ts';
2
2
  export declare const PROJECT_TEAM_CAPABILITIES: readonly ["launch_projects", "edit_direct", "manage_workstreams", "stage_releases", "publish_releases", "publish_market_listings", "manage_products", "manage_billing", "approve_remote_execution"];
3
3
  export declare const PROJECT_JOB_STATUSES: readonly ["queued", "running", "waiting_for_approval", "failed", "completed", "rolled_back", "cancelled"];
4
4
  export declare const WORKSTREAM_STATES: readonly ["drafting", "active_local", "verifying", "saved_remote", "in_staging", "archived"];
@@ -201,9 +201,15 @@ export interface LaunchProjectRequest {
201
201
  sourceKind: 'blank' | 'template' | 'knowledge_pack';
202
202
  sourceRef?: string | null;
203
203
  hostingMode: 'managed' | 'hybrid' | 'self_hosted';
204
+ repositoryHostId?: string | null;
205
+ repositoryHostConfig?: Record<string, unknown> | null;
204
206
  cloudflareHostId?: string | null;
205
207
  cloudflareHostMode?: 'team_owned' | 'treeseed_managed' | null;
206
208
  cloudflareHostConfig?: Record<string, unknown> | null;
209
+ emailHostId?: string | null;
210
+ emailHostMode?: 'team_owned' | 'treeseed_managed' | null;
211
+ emailHostConfig?: Record<string, unknown> | null;
212
+ hostBindings?: Record<string, ProjectLaunchHostBindingInput>;
207
213
  targetEnvironments?: Array<'local' | 'staging' | 'prod'>;
208
214
  publicSite?: boolean;
209
215
  repoProvider?: 'github';
@@ -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({
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from 'node:path';
3
3
  import { scaffoldTemplateProject } from '../operations/services/template-registry.js';
4
+ import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from '../sdk-types.js';
4
5
  function parseArgs(argv) {
5
6
  const args = {
6
7
  target: null,
7
- template: 'starter-basic',
8
+ template: TREESEED_DEFAULT_STARTER_TEMPLATE_ID,
8
9
  name: null,
9
10
  slug: null,
10
11
  siteUrl: null,
@@ -59,7 +60,7 @@ console.log(`Created Treeseed tenant from ${definition.id} at ${targetRoot}`);
59
60
  console.log('Next steps:');
60
61
  console.log(` cd ${options.target}`);
61
62
  console.log(' npm install');
62
- console.log(' treeseed template show starter-basic');
63
+ console.log(` treeseed template show ${options.template}`);
63
64
  console.log(' treeseed sync --check');
64
65
  console.log(' treeseed config --environment local');
65
66
  console.log(' treeseed dev');
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { corePackageRoot, packageRoot, packageScriptPath, sdkPackageRoot } from '../operations/services/runtime-tools.js';
7
7
  import { listTemplateProducts, validateTemplateProduct } from '../operations/services/template-registry.js';
8
+ import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from '../sdk-types.js';
8
9
  const npmCacheDir = process.env.TREESEED_SCAFFOLD_NPM_CACHE_DIR
9
10
  ? resolve(process.env.TREESEED_SCAFFOLD_NPM_CACHE_DIR)
10
11
  : resolve(tmpdir(), 'treeseed-npm-cache');
@@ -204,7 +205,7 @@ async function scaffoldSite(siteRoot) {
204
205
  for (const definition of await listTemplateProducts({ writeWarning: (message) => console.warn(message) })) {
205
206
  await validateTemplateProduct(definition, { writeWarning: (message) => console.warn(message) });
206
207
  }
207
- runStep(process.execPath, [packageScriptPath('scaffold-site'), siteRoot, '--template', 'starter-basic', '--name', 'Smoke Site', '--site-url', 'https://smoke.example.com', '--contact-email', 'hello@example.com']);
208
+ runStep(process.execPath, [packageScriptPath('scaffold-site'), siteRoot, '--template', TREESEED_DEFAULT_STARTER_TEMPLATE_ID, '--name', 'Smoke Site', '--site-url', 'https://smoke.example.com', '--contact-email', 'hello@example.com']);
208
209
  }
209
210
  function installScaffold(siteRoot, { coreTarballPath, sdkTarballPath, cliTarballPath }) {
210
211
  if (coreTarballPath && sdkTarballPath && cliTarballPath) {