@treeseed/sdk 0.8.0 → 0.8.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.
@@ -0,0 +1,642 @@
1
+ import net from "node:net";
2
+ import tls from "node:tls";
3
+ import {
4
+ getTreeseedEnvironmentSuggestedValues,
5
+ validateTreeseedEnvironmentValues
6
+ } from "../../platform/environment.js";
7
+ import {
8
+ collectTreeseedConfigSeedValues,
9
+ collectTreeseedEnvironmentContext,
10
+ checkTreeseedProviderConnections
11
+ } from "./config-runtime.js";
12
+ import {
13
+ buildProvisioningSummary,
14
+ createBranchPreviewDeployTarget,
15
+ createPersistentDeployTarget,
16
+ loadDeployState
17
+ } from "./deploy.js";
18
+ import {
19
+ currentManagedBranch,
20
+ PRODUCTION_BRANCH,
21
+ STAGING_BRANCH
22
+ } from "./git-workflow.js";
23
+ import { loadCliDeployConfig } from "./runtime-tools.js";
24
+ import {
25
+ collectTreeseedReconcileStatus,
26
+ reconcileTreeseedTarget
27
+ } from "../../reconcile/index.js";
28
+ const HOST_KINDS = ["repository", "web", "processing", "email"];
29
+ const HOST_GROUPS = {
30
+ repository: /* @__PURE__ */ new Set(["auth", "github"]),
31
+ web: /* @__PURE__ */ new Set(["auth", "cloudflare", "hosting"]),
32
+ processing: /* @__PURE__ */ new Set(["auth", "railway", "hosting"]),
33
+ email: /* @__PURE__ */ new Set(["smtp"])
34
+ };
35
+ function hasValue(value) {
36
+ return typeof value === "string" && value.trim().length > 0;
37
+ }
38
+ function firstValue(values, keys) {
39
+ for (const key of keys) {
40
+ const value = values[key];
41
+ if (hasValue(value)) {
42
+ return value;
43
+ }
44
+ }
45
+ return void 0;
46
+ }
47
+ function nonEmptyEnvironmentValues(env = process.env) {
48
+ return Object.fromEntries(
49
+ Object.entries(env).filter(([, value]) => typeof value === "string" && value.trim().length > 0).map(([key, value]) => [key, String(value)])
50
+ );
51
+ }
52
+ function normalizeHostKinds(hostKinds) {
53
+ const selected = Array.isArray(hostKinds) && hostKinds.length > 0 ? hostKinds : HOST_KINDS;
54
+ const normalized = selected.map((kind) => String(kind).trim()).filter((kind) => HOST_KINDS.includes(kind));
55
+ return normalized.length > 0 ? [...new Set(normalized)] : HOST_KINDS;
56
+ }
57
+ function targetLabel(target) {
58
+ return target.kind === "branch" ? `preview:${target.branchName}` : target.scope;
59
+ }
60
+ function serializeTarget(target) {
61
+ return {
62
+ kind: target.kind,
63
+ ...target.kind === "branch" ? { branchName: target.branchName } : { scope: target.scope },
64
+ label: targetLabel(target)
65
+ };
66
+ }
67
+ function resolveTreeseedHostingAuditTarget({
68
+ tenantRoot,
69
+ environment = "current"
70
+ }) {
71
+ if (environment === "local") {
72
+ return {
73
+ environment: "local",
74
+ scope: "local",
75
+ target: createPersistentDeployTarget("staging"),
76
+ branchName: null
77
+ };
78
+ }
79
+ if (environment === "staging") {
80
+ return {
81
+ environment: "staging",
82
+ scope: "staging",
83
+ target: createPersistentDeployTarget("staging"),
84
+ branchName: null
85
+ };
86
+ }
87
+ if (environment === "prod") {
88
+ return {
89
+ environment: "prod",
90
+ scope: "prod",
91
+ target: createPersistentDeployTarget("prod"),
92
+ branchName: null
93
+ };
94
+ }
95
+ const branchName = currentManagedBranch(tenantRoot);
96
+ if (branchName === PRODUCTION_BRANCH) {
97
+ return {
98
+ environment: "prod",
99
+ scope: "prod",
100
+ target: createPersistentDeployTarget("prod"),
101
+ branchName
102
+ };
103
+ }
104
+ if (branchName === STAGING_BRANCH) {
105
+ return {
106
+ environment: "staging",
107
+ scope: "staging",
108
+ target: createPersistentDeployTarget("staging"),
109
+ branchName
110
+ };
111
+ }
112
+ if (branchName) {
113
+ try {
114
+ const deployConfig = loadCliDeployConfig(tenantRoot);
115
+ const previewTarget = createBranchPreviewDeployTarget(branchName);
116
+ const previewState = loadDeployState(tenantRoot, deployConfig, { target: previewTarget });
117
+ if (previewState?.previewEnabled === true || previewState?.readiness?.initialized === true || hasValue(previewState?.lastDeployedUrl) || hasValue(previewState?.workerName)) {
118
+ return {
119
+ environment: "preview",
120
+ scope: "staging",
121
+ target: previewTarget,
122
+ branchName
123
+ };
124
+ }
125
+ } catch {
126
+ }
127
+ }
128
+ return {
129
+ environment: "staging",
130
+ scope: "staging",
131
+ target: createPersistentDeployTarget("staging"),
132
+ branchName
133
+ };
134
+ }
135
+ function normalizeAuditValues(values) {
136
+ const normalized = { ...values };
137
+ const githubToken = normalized.TREESEED_HOSTED_HUBS_GITHUB_TOKEN;
138
+ if (githubToken) {
139
+ normalized.GH_TOKEN = githubToken;
140
+ normalized.GITHUB_TOKEN = githubToken;
141
+ }
142
+ const cloudflareToken = normalized.CLOUDFLARE_API_TOKEN;
143
+ if (cloudflareToken) {
144
+ normalized.CLOUDFLARE_API_TOKEN = cloudflareToken;
145
+ }
146
+ const cloudflareAccount = normalized.CLOUDFLARE_ACCOUNT_ID;
147
+ if (cloudflareAccount) {
148
+ normalized.CLOUDFLARE_ACCOUNT_ID = cloudflareAccount;
149
+ }
150
+ const railwayToken = normalized.RAILWAY_API_TOKEN;
151
+ if (railwayToken) {
152
+ normalized.RAILWAY_API_TOKEN = railwayToken;
153
+ }
154
+ const railwayWorkspace = normalized.TREESEED_RAILWAY_WORKSPACE;
155
+ if (railwayWorkspace) {
156
+ normalized.TREESEED_RAILWAY_WORKSPACE = railwayWorkspace;
157
+ }
158
+ return normalized;
159
+ }
160
+ function configCheck({
161
+ id,
162
+ hostType,
163
+ provider,
164
+ status,
165
+ severity,
166
+ summary,
167
+ detail,
168
+ remediation
169
+ }) {
170
+ return {
171
+ id,
172
+ hostType,
173
+ provider,
174
+ category: "config",
175
+ status,
176
+ severity,
177
+ summary,
178
+ ...detail ? { detail } : {},
179
+ ...remediation ? { remediation } : {}
180
+ };
181
+ }
182
+ function requiredKeyCheck(checks, values, {
183
+ id,
184
+ hostType,
185
+ provider,
186
+ keys,
187
+ label,
188
+ remediation
189
+ }) {
190
+ const configured = firstValue(values, keys);
191
+ checks.push(configCheck({
192
+ id,
193
+ hostType,
194
+ provider,
195
+ status: configured ? "passed" : "failed",
196
+ severity: configured ? "info" : "critical",
197
+ summary: configured ? `${label} is configured.` : `${label} is missing.`,
198
+ detail: configured ? void 0 : `Expected one of: ${keys.join(", ")}.`,
199
+ remediation
200
+ }));
201
+ }
202
+ function appendManualConfigChecks(checks, values, hostKinds) {
203
+ if (hostKinds.includes("repository")) {
204
+ requiredKeyCheck(checks, values, {
205
+ id: "repository.github.owner",
206
+ hostType: "repository",
207
+ provider: "github",
208
+ keys: ["TREESEED_HOSTED_HUBS_GITHUB_OWNER"],
209
+ label: "Repository owner or organization",
210
+ remediation: "Set TREESEED_HOSTED_HUBS_GITHUB_OWNER for TreeSeed-managed hosted repositories."
211
+ });
212
+ requiredKeyCheck(checks, values, {
213
+ id: "repository.github.token",
214
+ hostType: "repository",
215
+ provider: "github",
216
+ keys: ["TREESEED_HOSTED_HUBS_GITHUB_TOKEN"],
217
+ label: "Repository provider token",
218
+ remediation: "Set TREESEED_HOSTED_HUBS_GITHUB_TOKEN for TreeSeed-managed hosted repositories, or provide a team-owned Repository Host session."
219
+ });
220
+ }
221
+ if (hostKinds.includes("web")) {
222
+ requiredKeyCheck(checks, values, {
223
+ id: "web.cloudflare.token",
224
+ hostType: "web",
225
+ provider: "cloudflare",
226
+ keys: ["CLOUDFLARE_API_TOKEN"],
227
+ label: "Web provider token",
228
+ remediation: "Set CLOUDFLARE_API_TOKEN for TreeSeed-managed Web hosting."
229
+ });
230
+ requiredKeyCheck(checks, values, {
231
+ id: "web.cloudflare.account",
232
+ hostType: "web",
233
+ provider: "cloudflare",
234
+ keys: ["CLOUDFLARE_ACCOUNT_ID"],
235
+ label: "Web provider account",
236
+ remediation: "Set CLOUDFLARE_ACCOUNT_ID for TreeSeed-managed Web hosting."
237
+ });
238
+ }
239
+ if (hostKinds.includes("processing")) {
240
+ requiredKeyCheck(checks, values, {
241
+ id: "processing.railway.token",
242
+ hostType: "processing",
243
+ provider: "railway",
244
+ keys: ["RAILWAY_API_TOKEN"],
245
+ label: "Processing provider token",
246
+ remediation: "Set RAILWAY_API_TOKEN for TreeSeed-managed Processing hosting."
247
+ });
248
+ requiredKeyCheck(checks, values, {
249
+ id: "processing.railway.workspace",
250
+ hostType: "processing",
251
+ provider: "railway",
252
+ keys: ["TREESEED_RAILWAY_WORKSPACE"],
253
+ label: "Processing provider workspace",
254
+ remediation: "Set TREESEED_RAILWAY_WORKSPACE for TreeSeed-managed Processing hosting."
255
+ });
256
+ }
257
+ if (hostKinds.includes("email")) {
258
+ requiredKeyCheck(checks, values, {
259
+ id: "email.smtp.host",
260
+ hostType: "email",
261
+ provider: "smtp",
262
+ keys: ["TREESEED_SMTP_HOST"],
263
+ label: "Email provider host",
264
+ remediation: "Set TREESEED_SMTP_HOST or configure a team-owned Email Host session."
265
+ });
266
+ requiredKeyCheck(checks, values, {
267
+ id: "email.smtp.port",
268
+ hostType: "email",
269
+ provider: "smtp",
270
+ keys: ["TREESEED_SMTP_PORT"],
271
+ label: "Email provider port",
272
+ remediation: "Set TREESEED_SMTP_PORT or configure a team-owned Email Host session."
273
+ });
274
+ requiredKeyCheck(checks, values, {
275
+ id: "email.smtp.from",
276
+ hostType: "email",
277
+ provider: "smtp",
278
+ keys: ["TREESEED_SMTP_FROM"],
279
+ label: "Email sender address",
280
+ remediation: "Set TREESEED_SMTP_FROM to a verified sender address."
281
+ });
282
+ }
283
+ }
284
+ function appendRegistryConfigChecks({
285
+ checks,
286
+ tenantRoot,
287
+ scope,
288
+ values,
289
+ hostKinds
290
+ }) {
291
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
292
+ const selectedGroups = new Set(hostKinds.flatMap((kind) => [...HOST_GROUPS[kind]]));
293
+ const validation = validateTreeseedEnvironmentValues({
294
+ values,
295
+ scope,
296
+ purpose: "config",
297
+ deployConfig: registry.context.deployConfig,
298
+ tenantConfig: registry.context.tenantConfig,
299
+ plugins: registry.context.plugins
300
+ });
301
+ const selectedProblems = [...validation.missing, ...validation.invalid].filter((problem) => selectedGroups.has(problem.entry.group));
302
+ for (const problem of selectedProblems) {
303
+ const hostType = hostKinds.find((kind) => HOST_GROUPS[kind].has(problem.entry.group)) ?? "platform";
304
+ checks.push(configCheck({
305
+ id: `config.${problem.id}`,
306
+ hostType,
307
+ provider: problem.entry.group,
308
+ status: "failed",
309
+ severity: problem.entry.requirement === "optional" ? "warning" : "critical",
310
+ summary: `${problem.label} is ${problem.reason}.`,
311
+ detail: problem.message,
312
+ remediation: problem.entry.howToGet
313
+ }));
314
+ }
315
+ if (selectedProblems.length === 0) {
316
+ checks.push(configCheck({
317
+ id: "config.registry",
318
+ hostType: "platform",
319
+ provider: "treeseed",
320
+ status: "passed",
321
+ severity: "info",
322
+ summary: "Registered hosting environment values are complete."
323
+ }));
324
+ }
325
+ }
326
+ function providerConnectionChecks(report, hostKinds) {
327
+ const allowedProviders = /* @__PURE__ */ new Set();
328
+ if (hostKinds.includes("repository")) allowedProviders.add("github");
329
+ if (hostKinds.includes("web")) allowedProviders.add("cloudflare");
330
+ if (hostKinds.includes("processing")) allowedProviders.add("railway");
331
+ return report.checks.filter((check) => allowedProviders.has(check.provider)).map((check) => {
332
+ const hostType = check.provider === "github" ? "repository" : check.provider === "railway" ? "processing" : "web";
333
+ return {
334
+ id: `identity.${check.provider}`,
335
+ hostType,
336
+ provider: check.provider,
337
+ category: "identity",
338
+ status: check.ready ? "passed" : check.skipped ? "skipped" : "failed",
339
+ severity: check.ready || check.skipped ? "info" : "critical",
340
+ summary: check.ready ? `${check.provider} identity check passed.` : check.skipped ? `${check.provider} identity check skipped.` : `${check.provider} identity check failed.`,
341
+ detail: check.detail,
342
+ repairAvailable: false,
343
+ remediation: check.ready ? void 0 : `Confirm ${check.provider} credentials and provider CLI access for this environment.`
344
+ };
345
+ });
346
+ }
347
+ function reconcileSystemsForHostKinds(hostKinds) {
348
+ const systems = /* @__PURE__ */ new Set();
349
+ if (hostKinds.includes("repository")) systems.add("github");
350
+ if (hostKinds.includes("web")) {
351
+ systems.add("data");
352
+ systems.add("web");
353
+ }
354
+ if (hostKinds.includes("processing")) {
355
+ systems.add("api");
356
+ systems.add("agents");
357
+ }
358
+ return [...systems];
359
+ }
360
+ function reconcileStatusChecks(status, repairedIds = /* @__PURE__ */ new Set()) {
361
+ const checks = [];
362
+ for (const unit of status.units) {
363
+ const hostType = unit.provider === "github" ? "repository" : unit.provider === "railway" ? "processing" : "web";
364
+ const verified = unit.verification?.verified === true;
365
+ const id = `resource.${unit.provider}.${unit.unitType}.${unit.unitId}`;
366
+ const repaired = repairedIds.has(id);
367
+ checks.push({
368
+ id,
369
+ hostType,
370
+ provider: unit.provider,
371
+ category: "resource",
372
+ status: repaired ? "repaired" : verified ? "passed" : "failed",
373
+ severity: verified || repaired ? "info" : "critical",
374
+ summary: `${unit.provider}:${unit.unitType} ${verified || repaired ? "is ready" : "is not ready"}.`,
375
+ detail: [
376
+ unit.exists === false ? "Resource is missing." : null,
377
+ unit.verification?.missing?.length ? `missing: ${unit.verification.missing.join(", ")}` : null,
378
+ unit.verification?.drifted?.length ? `drifted: ${unit.verification.drifted.join(", ")}` : null,
379
+ unit.warnings?.length ? `warnings: ${unit.warnings.join("; ")}` : null
380
+ ].filter(Boolean).join(" ") || void 0,
381
+ resourceRef: unit.locators?.length ? unit.locators.join(", ") : unit.unitId,
382
+ repairAvailable: true,
383
+ repaired,
384
+ remediation: verified || repaired ? void 0 : "Run trsd audit hosting --repair to reconcile platform resources."
385
+ });
386
+ }
387
+ return checks;
388
+ }
389
+ async function checkSmtpReachability(values) {
390
+ const host = values.TREESEED_SMTP_HOST;
391
+ const port = Number(values.TREESEED_SMTP_PORT ?? 0);
392
+ const secure = /^(true|1|tls|ssl|465)$/iu.test(String(values.TREESEED_SMTP_SECURE ?? ""));
393
+ if (!host || !Number.isFinite(port) || port <= 0) {
394
+ return {
395
+ id: "connectivity.smtp",
396
+ hostType: "email",
397
+ provider: "smtp",
398
+ category: "connectivity",
399
+ status: "skipped",
400
+ severity: "warning",
401
+ summary: "Email connectivity check skipped.",
402
+ detail: "SMTP host and port are required before TreeSeed can test connectivity.",
403
+ remediation: "Configure SMTP host and port, then rerun the hosting audit."
404
+ };
405
+ }
406
+ return new Promise((resolve) => {
407
+ const socket = secure ? tls.connect({ host, port, servername: host, timeout: 5e3 }) : net.connect({ host, port, timeout: 5e3 });
408
+ let settled = false;
409
+ const finish = (check) => {
410
+ if (settled) return;
411
+ settled = true;
412
+ socket.destroy();
413
+ resolve(check);
414
+ };
415
+ socket.once("connect", () => finish({
416
+ id: "connectivity.smtp",
417
+ hostType: "email",
418
+ provider: "smtp",
419
+ category: "connectivity",
420
+ status: "passed",
421
+ severity: "info",
422
+ summary: "Email provider accepts a TCP connection.",
423
+ resourceRef: `${host}:${port}`
424
+ }));
425
+ socket.once("secureConnect", () => finish({
426
+ id: "connectivity.smtp",
427
+ hostType: "email",
428
+ provider: "smtp",
429
+ category: "connectivity",
430
+ status: "passed",
431
+ severity: "info",
432
+ summary: "Email provider accepts a TLS connection.",
433
+ resourceRef: `${host}:${port}`
434
+ }));
435
+ socket.once("timeout", () => finish({
436
+ id: "connectivity.smtp",
437
+ hostType: "email",
438
+ provider: "smtp",
439
+ category: "connectivity",
440
+ status: "failed",
441
+ severity: "critical",
442
+ summary: "Email provider connection timed out.",
443
+ resourceRef: `${host}:${port}`,
444
+ remediation: "Confirm SMTP host, port, firewall, and TLS mode."
445
+ }));
446
+ socket.once("error", (error) => finish({
447
+ id: "connectivity.smtp",
448
+ hostType: "email",
449
+ provider: "smtp",
450
+ category: "connectivity",
451
+ status: "failed",
452
+ severity: "critical",
453
+ summary: "Email provider connection failed.",
454
+ detail: error instanceof Error ? error.message : String(error),
455
+ resourceRef: `${host}:${port}`,
456
+ remediation: "Confirm SMTP host, port, firewall, and TLS mode."
457
+ }));
458
+ });
459
+ }
460
+ function summarizeReport(checks) {
461
+ const blockers = checks.filter((check) => check.status === "failed" && check.severity === "critical").map((check) => check.detail ? `${check.summary} ${check.detail}` : check.summary);
462
+ const warnings = checks.filter((check) => check.status === "warning" || check.status === "failed" && check.severity === "warning").map((check) => check.detail ? `${check.summary} ${check.detail}` : check.summary);
463
+ const missingConfig = checks.filter((check) => check.category === "config" && check.status === "failed").flatMap((check) => {
464
+ const detail = check.detail ?? "";
465
+ const match = detail.match(/Expected one of: ([^.]+)\./u);
466
+ const keys = match?.[1]?.split(",").map((key) => key.trim()).filter(Boolean) ?? [];
467
+ return keys.length > 0 ? keys.map((key) => ({
468
+ key,
469
+ hostType: check.hostType,
470
+ severity: check.severity,
471
+ summary: check.summary
472
+ })) : [{
473
+ key: check.id.replace(/^config\./u, ""),
474
+ hostType: check.hostType,
475
+ severity: check.severity,
476
+ summary: check.summary
477
+ }];
478
+ });
479
+ const nextActions = blockers.length > 0 ? [...new Set(checks.filter((check) => check.status === "failed").map((check) => check.remediation).filter((value) => typeof value === "string" && value.trim().length > 0))] : ["Hosting setup is ready for host saving and project launch."];
480
+ return { blockers, warnings, missingConfig, nextActions };
481
+ }
482
+ async function runTreeseedHostingAudit({
483
+ tenantRoot,
484
+ environment = "current",
485
+ repair = false,
486
+ env = process.env,
487
+ valuesOverlay = {},
488
+ hostKinds: requestedHostKinds,
489
+ write
490
+ }) {
491
+ const resolved = resolveTreeseedHostingAuditTarget({ tenantRoot, environment });
492
+ const hostKinds = normalizeHostKinds(requestedHostKinds);
493
+ const deployConfig = loadCliDeployConfig(tenantRoot);
494
+ const seedValues = collectTreeseedConfigSeedValues(tenantRoot, resolved.scope, env, valuesOverlay);
495
+ const suggestedValues = getTreeseedEnvironmentSuggestedValues({
496
+ scope: resolved.scope,
497
+ purpose: "config",
498
+ deployConfig,
499
+ values: {
500
+ ...seedValues,
501
+ ...nonEmptyEnvironmentValues(env),
502
+ ...nonEmptyEnvironmentValues(valuesOverlay)
503
+ }
504
+ });
505
+ const values = normalizeAuditValues({
506
+ ...suggestedValues,
507
+ ...seedValues,
508
+ ...nonEmptyEnvironmentValues(env),
509
+ ...nonEmptyEnvironmentValues(valuesOverlay)
510
+ });
511
+ const checks = [];
512
+ appendManualConfigChecks(checks, values, hostKinds);
513
+ appendRegistryConfigChecks({ checks, tenantRoot, scope: resolved.scope, values, hostKinds });
514
+ const connectionReport = await checkTreeseedProviderConnections({
515
+ tenantRoot,
516
+ scope: resolved.scope,
517
+ env: values,
518
+ valuesOverlay: values
519
+ });
520
+ checks.push(...providerConnectionChecks(connectionReport, hostKinds));
521
+ if (hostKinds.includes("email")) {
522
+ checks.push(await checkSmtpReachability(values));
523
+ }
524
+ const systems = reconcileSystemsForHostKinds(hostKinds);
525
+ let resources = {};
526
+ let repaired = false;
527
+ if (resolved.environment !== "local" && systems.length > 0) {
528
+ try {
529
+ const state = loadDeployState(tenantRoot, deployConfig, { target: resolved.target });
530
+ resources = buildProvisioningSummary(deployConfig, state, resolved.target);
531
+ } catch (error) {
532
+ checks.push({
533
+ id: "resources.summary",
534
+ hostType: "platform",
535
+ provider: "treeseed",
536
+ category: "resource",
537
+ status: "warning",
538
+ severity: "warning",
539
+ summary: "Unable to load provisioning summary.",
540
+ detail: error instanceof Error ? error.message : String(error)
541
+ });
542
+ }
543
+ try {
544
+ const beforeStatus = await collectTreeseedReconcileStatus({
545
+ tenantRoot,
546
+ target: resolved.target,
547
+ env: values,
548
+ systems
549
+ });
550
+ let repairedIds = /* @__PURE__ */ new Set();
551
+ if (repair && beforeStatus.ready !== true) {
552
+ write?.("[audit][hosting] Repair mode enabled; reconciling platform provider resources.");
553
+ const repairResult = await reconcileTreeseedTarget({
554
+ tenantRoot,
555
+ target: resolved.target,
556
+ env: values,
557
+ systems,
558
+ write
559
+ });
560
+ repaired = repairResult.results.some((result) => result.action !== "none");
561
+ const afterStatus = await collectTreeseedReconcileStatus({
562
+ tenantRoot,
563
+ target: resolved.target,
564
+ env: values,
565
+ systems
566
+ });
567
+ repairedIds = new Set(afterStatus.units.filter((unit) => unit.verification?.verified === true).map((unit) => `resource.${unit.provider}.${unit.unitType}.${unit.unitId}`));
568
+ checks.push(...reconcileStatusChecks(afterStatus, repairedIds));
569
+ } else {
570
+ checks.push(...reconcileStatusChecks(beforeStatus));
571
+ }
572
+ } catch (error) {
573
+ checks.push({
574
+ id: "resources.reconcile-status",
575
+ hostType: "platform",
576
+ provider: "treeseed",
577
+ category: "resource",
578
+ status: "failed",
579
+ severity: "critical",
580
+ summary: repair ? "Hosting resource repair failed." : "Hosting resource readiness check failed.",
581
+ detail: error instanceof Error ? error.message : String(error),
582
+ repairAvailable: !repair,
583
+ remediation: repair ? "Inspect provider credentials and rerun the audit." : "Run trsd audit hosting --repair after fixing provider credentials."
584
+ });
585
+ }
586
+ } else if (resolved.environment === "local") {
587
+ checks.push({
588
+ id: "resources.local",
589
+ hostType: "platform",
590
+ provider: "treeseed",
591
+ category: "resource",
592
+ status: "skipped",
593
+ severity: "info",
594
+ summary: "Hosted provider resource checks are skipped for local audits."
595
+ });
596
+ }
597
+ const summary = summarizeReport(checks);
598
+ return {
599
+ ok: summary.blockers.length === 0,
600
+ environment: resolved.environment,
601
+ requestedEnvironment: environment,
602
+ repairMode: repair,
603
+ repaired,
604
+ target: serializeTarget(resolved.target),
605
+ hostKinds,
606
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
607
+ checks,
608
+ missingConfig: summary.missingConfig,
609
+ resources,
610
+ warnings: summary.warnings,
611
+ blockers: summary.blockers,
612
+ nextActions: summary.nextActions
613
+ };
614
+ }
615
+ function formatTreeseedHostingAuditReport(report) {
616
+ const lines = [
617
+ `Treeseed hosting audit (${report.environment}, ${report.repairMode ? "repair" : "read-only"})`,
618
+ `Status: ${report.ok ? "ready" : "blocked"}`,
619
+ `Target: ${report.target.label}`,
620
+ "",
621
+ "Checks:",
622
+ ...report.checks.map((check) => {
623
+ const status = check.status.toUpperCase();
624
+ const resource = check.resourceRef ? ` [${check.resourceRef}]` : "";
625
+ const detail = check.detail ? ` ${check.detail}` : "";
626
+ return ` - ${status} ${check.hostType}/${check.provider}/${check.category}: ${check.summary}${resource}${detail}`;
627
+ })
628
+ ];
629
+ if (report.blockers.length > 0) {
630
+ lines.push("", "Blockers:", ...report.blockers.map((blocker) => ` - ${blocker}`));
631
+ }
632
+ if (report.warnings.length > 0) {
633
+ lines.push("", "Warnings:", ...report.warnings.map((warning) => ` - ${warning}`));
634
+ }
635
+ lines.push("", "Next actions:", ...report.nextActions.map((action) => ` - ${action}`));
636
+ return lines.join("\n");
637
+ }
638
+ export {
639
+ formatTreeseedHostingAuditReport,
640
+ resolveTreeseedHostingAuditTarget,
641
+ runTreeseedHostingAudit
642
+ };
@@ -63,7 +63,7 @@ function defaultHubContentResolutionPolicy() {
63
63
  function planKnowledgeHubRepositories(intent, host) {
64
64
  const normalized = normalizeKnowledgeHubLaunchIntent(intent);
65
65
  const hubSlug = normalized.hub.slug;
66
- const owner = normalized.repository?.owner ?? host?.organizationOrOwner ?? process.env.TREESEED_HOSTED_HUBS_GITHUB_OWNER ?? process.env.TREESEED_REPOSITORY_HOST_GITHUB_OWNER ?? process.env.GITHUB_REPOSITORY_OWNER ?? "treeseed-sites";
66
+ const owner = normalized.repository?.owner ?? host?.organizationOrOwner ?? process.env.TREESEED_HOSTED_HUBS_GITHUB_OWNER ?? "treeseed-sites";
67
67
  const topology = normalized.repository?.topology ?? "split_software_content";
68
68
  const visibility = normalized.repository?.visibility ?? host?.defaultVisibility ?? "private";
69
69
  if (topology === "combined_compatibility") {
@@ -154,7 +154,7 @@ function validateRepositoryHost(host) {
154
154
  };
155
155
  }
156
156
  async function createKnowledgeHubRepositories(input) {
157
- const githubToken = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || process.env.TREESEED_REPOSITORY_HOST_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
157
+ const githubToken = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || "";
158
158
  const githubEnv = githubToken ? { ...process.env, GH_TOKEN: githubToken, GITHUB_TOKEN: githubToken } : process.env;
159
159
  const created = [];
160
160
  for (const repository of input.plan.repositories) {
@@ -239,7 +239,7 @@ function applyManagedProjectDefaults(projectRoot, input) {
239
239
  const marketBaseUrl = normalizeBaseUrl(input.marketBaseUrl ?? envOrNull("TREESEED_MARKET_API_BASE_URL") ?? "https://knowledge.coop");
240
240
  const siteUrl = resolveManagedWebUrl(slug);
241
241
  const projectApiBaseUrl = normalizeBaseUrl(input.projectApiBaseUrl ?? resolveManagedApiUrl(slug));
242
- const cloudflareAccountId = envOrNull("TREESEED_CLOUDFLARE_ACCOUNT_ID") ?? envOrNull("CLOUDFLARE_ACCOUNT_ID") ?? "replace-with-cloudflare-account-id";
242
+ const cloudflareAccountId = envOrNull("CLOUDFLARE_ACCOUNT_ID") ?? "replace-with-cloudflare-account-id";
243
243
  const runtimeMode = input.hostingMode === "managed" ? "treeseed_managed" : input.hostingMode === "hybrid" ? "byo_attached" : "none";
244
244
  const runtimeRegistration = input.hostingMode === "managed" ? "required" : input.hostingMode === "hybrid" ? "optional" : "none";
245
245
  const hubMode = input.hostingMode === "self_hosted" ? "customer_hosted" : "treeseed_hosted";
@@ -545,7 +545,7 @@ function buildCloudflareHostEnvironmentOverlay(input, scope) {
545
545
  for (const [key, value] of Object.entries(environmentConfig)) {
546
546
  overlayValue(overlay, key, value);
547
547
  }
548
- overlay.CLOUDFLARE_ACCOUNT_ID = overlay.CLOUDFLARE_ACCOUNT_ID || overlay.TREESEED_CLOUDFLARE_ACCOUNT_ID || "";
548
+ overlay.CLOUDFLARE_ACCOUNT_ID = overlay.CLOUDFLARE_ACCOUNT_ID || "";
549
549
  overlayValue(overlay, "TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME", overlay.TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME || projectSlug);
550
550
  overlayValue(overlay, "TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME", overlay.TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME || `${projectSlug}-staging`);
551
551
  overlayValue(overlay, "TREESEED_CONTENT_BUCKET_NAME", overlay.TREESEED_CONTENT_BUCKET_NAME || `${projectSlug}-content`);
@@ -600,7 +600,7 @@ function scaffoldKnowledgeCoopSource(projectRoot, input) {
600
600
  });
601
601
  }
602
602
  function repositoryHostGitHubEnvOverlay() {
603
- const token = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || process.env.TREESEED_REPOSITORY_HOST_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
603
+ const token = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || "";
604
604
  return token ? { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token } : process.env;
605
605
  }
606
606
  function prepareKnowledgeHubContentRepositoryRoot(sourceRoot, contentRoot, input) {
@@ -656,7 +656,7 @@ async function validateKnowledgeHubProviderLaunchPrerequisites(tenantRoot = proc
656
656
  ["TREESEED_API_WEB_SERVICE_ID"],
657
657
  ["TREESEED_API_WEB_SERVICE_SECRET"],
658
658
  ["TREESEED_API_WEB_ASSERTION_SECRET"],
659
- ["CLOUDFLARE_ACCOUNT_ID", "TREESEED_CLOUDFLARE_ACCOUNT_ID"]
659
+ ["CLOUDFLARE_ACCOUNT_ID"]
660
660
  ];
661
661
  const missingConfig = requiredConfig.filter((group) => !group.some((name) => {
662
662
  const value = values[name];