@treeseed/sdk 0.1.2 → 0.3.1

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 (237) hide show
  1. package/README.md +97 -506
  2. package/dist/{src/cli-tools.d.ts → cli-tools.d.ts} +1 -1
  3. package/dist/cli-tools.js +5 -3
  4. package/dist/{src/content-store.d.ts → content-store.d.ts} +3 -2
  5. package/dist/content-store.js +52 -20
  6. package/dist/{src/d1-store.d.ts → d1-store.d.ts} +62 -1
  7. package/dist/d1-store.js +625 -65
  8. package/dist/field-aliases.d.ts +11 -0
  9. package/dist/field-aliases.js +41 -0
  10. package/dist/graph/build.d.ts +19 -0
  11. package/dist/graph/build.js +949 -0
  12. package/dist/graph/dsl.d.ts +2 -0
  13. package/dist/graph/dsl.js +243 -0
  14. package/dist/graph/query.d.ts +47 -0
  15. package/dist/graph/query.js +447 -0
  16. package/dist/graph/ranking.d.ts +3 -0
  17. package/dist/graph/ranking.js +483 -0
  18. package/dist/graph/schema.d.ts +142 -0
  19. package/dist/graph/schema.js +133 -0
  20. package/dist/graph.d.ts +52 -0
  21. package/dist/graph.js +133 -0
  22. package/dist/index.d.ts +28 -0
  23. package/dist/index.js +91 -2
  24. package/dist/model-registry.d.ts +8 -0
  25. package/dist/model-registry.js +351 -25
  26. package/dist/operations/providers/default.d.ts +10 -0
  27. package/dist/operations/providers/default.js +514 -0
  28. package/dist/operations/runtime.d.ts +7 -0
  29. package/dist/operations/runtime.js +60 -0
  30. package/dist/operations/services/config-runtime.d.ts +269 -0
  31. package/dist/operations/services/config-runtime.js +1397 -0
  32. package/dist/operations/services/d1-migration.d.ts +6 -0
  33. package/dist/operations/services/d1-migration.js +89 -0
  34. package/dist/operations/services/deploy.d.ts +371 -0
  35. package/dist/operations/services/deploy.js +981 -0
  36. package/dist/operations/services/git-workflow.d.ts +49 -0
  37. package/dist/operations/services/git-workflow.js +218 -0
  38. package/dist/operations/services/github-automation.d.ts +156 -0
  39. package/dist/operations/services/github-automation.js +256 -0
  40. package/dist/operations/services/local-dev.d.ts +9 -0
  41. package/dist/operations/services/local-dev.js +106 -0
  42. package/dist/operations/services/mailpit-runtime.d.ts +4 -0
  43. package/dist/operations/services/mailpit-runtime.js +59 -0
  44. package/dist/operations/services/railway-deploy.d.ts +53 -0
  45. package/dist/operations/services/railway-deploy.js +123 -0
  46. package/dist/operations/services/runtime-paths.d.ts +19 -0
  47. package/dist/operations/services/runtime-paths.js +54 -0
  48. package/dist/operations/services/runtime-tools.d.ts +117 -0
  49. package/dist/operations/services/runtime-tools.js +358 -0
  50. package/dist/operations/services/save-deploy-preflight.d.ts +34 -0
  51. package/dist/operations/services/save-deploy-preflight.js +76 -0
  52. package/dist/operations/services/template-registry.d.ts +88 -0
  53. package/dist/operations/services/template-registry.js +407 -0
  54. package/dist/operations/services/watch-dev.d.ts +21 -0
  55. package/dist/operations/services/watch-dev.js +284 -0
  56. package/dist/operations/services/workspace-preflight.d.ts +40 -0
  57. package/dist/operations/services/workspace-preflight.js +165 -0
  58. package/dist/operations/services/workspace-save.d.ts +42 -0
  59. package/dist/operations/services/workspace-save.js +235 -0
  60. package/dist/operations/services/workspace-tools.d.ts +16 -0
  61. package/dist/operations/services/workspace-tools.js +270 -0
  62. package/dist/operations-registry.d.ts +5 -0
  63. package/dist/operations-registry.js +68 -0
  64. package/dist/operations-types.d.ts +71 -0
  65. package/dist/operations-types.js +17 -0
  66. package/dist/operations.d.ts +6 -0
  67. package/dist/operations.js +16 -0
  68. package/dist/platform/books-data.d.ts +1 -0
  69. package/dist/platform/books-data.js +1 -0
  70. package/dist/platform/contracts.d.ts +158 -0
  71. package/dist/platform/contracts.js +0 -0
  72. package/dist/platform/deploy/config.d.ts +4 -0
  73. package/dist/platform/deploy/config.js +222 -0
  74. package/dist/platform/deploy-config.d.ts +1 -0
  75. package/dist/platform/deploy-config.js +1 -0
  76. package/dist/platform/deploy-runtime.d.ts +18 -0
  77. package/dist/platform/deploy-runtime.js +78 -0
  78. package/dist/platform/env.yaml +394 -0
  79. package/dist/platform/environment.d.ts +130 -0
  80. package/dist/platform/environment.js +331 -0
  81. package/dist/platform/plugin.d.ts +2 -0
  82. package/dist/platform/plugin.js +4 -0
  83. package/dist/platform/plugins/constants.d.ts +22 -0
  84. package/dist/platform/plugins/constants.js +29 -0
  85. package/dist/platform/plugins/plugin.d.ts +51 -0
  86. package/dist/platform/plugins/plugin.js +6 -0
  87. package/dist/platform/plugins/runtime.d.ts +35 -0
  88. package/dist/platform/plugins/runtime.js +161 -0
  89. package/dist/platform/plugins.d.ts +6 -0
  90. package/dist/platform/plugins.js +38 -0
  91. package/dist/platform/site-config-schema.js +1 -0
  92. package/dist/platform/tenant/config.d.ts +9 -0
  93. package/dist/platform/tenant/config.js +154 -0
  94. package/dist/platform/tenant/runtime-config.d.ts +4 -0
  95. package/dist/platform/tenant/runtime-config.js +20 -0
  96. package/dist/platform/tenant-config.d.ts +1 -0
  97. package/dist/platform/tenant-config.js +1 -0
  98. package/dist/platform/utils/books-data.d.ts +29 -0
  99. package/dist/platform/utils/books-data.js +82 -0
  100. package/dist/platform/utils/site-config-schema.js +321 -0
  101. package/dist/remote.d.ts +175 -0
  102. package/dist/remote.js +202 -0
  103. package/dist/runtime.js +35 -22
  104. package/dist/scripts/aggregate-book.js +121 -0
  105. package/dist/scripts/build-dist.js +54 -13
  106. package/dist/scripts/build-tenant-worker.js +36 -0
  107. package/dist/scripts/cleanup-markdown.js +373 -0
  108. package/dist/scripts/cli-test-fixtures.js +48 -0
  109. package/dist/scripts/config-treeseed.js +95 -0
  110. package/dist/scripts/ensure-mailpit.js +29 -0
  111. package/dist/scripts/local-dev.js +129 -0
  112. package/dist/scripts/logs-mailpit.js +2 -0
  113. package/dist/scripts/patch-starlight-content-path.js +172 -0
  114. package/dist/scripts/release-verify.js +34 -6
  115. package/dist/scripts/run-fixture-astro-command.js +18 -0
  116. package/dist/scripts/scaffold-site.js +65 -0
  117. package/dist/scripts/stop-mailpit.js +5 -0
  118. package/dist/scripts/sync-dev-vars.js +6 -0
  119. package/dist/scripts/sync-template.js +20 -0
  120. package/dist/scripts/template-catalog.test.js +100 -0
  121. package/dist/scripts/template-command.js +31 -0
  122. package/dist/scripts/tenant-astro-command.js +3 -0
  123. package/dist/scripts/tenant-build.js +16 -0
  124. package/dist/scripts/tenant-check.js +7 -0
  125. package/dist/scripts/tenant-d1-migrate-local.js +11 -0
  126. package/dist/scripts/tenant-deploy.js +180 -0
  127. package/dist/scripts/tenant-destroy.js +104 -0
  128. package/dist/scripts/tenant-dev.js +171 -0
  129. package/dist/scripts/tenant-lint.js +4 -0
  130. package/dist/scripts/tenant-test.js +4 -0
  131. package/dist/scripts/test-cloudflare-local.js +212 -0
  132. package/dist/scripts/test-scaffold.js +314 -0
  133. package/dist/scripts/test-smoke.js +71 -13
  134. package/dist/scripts/treeseed-assert-release-tag-version.js +21 -0
  135. package/dist/scripts/treeseed-build-dist.js +134 -0
  136. package/dist/scripts/treeseed-publish-package.js +19 -0
  137. package/dist/scripts/treeseed-release-verify.js +131 -0
  138. package/dist/scripts/treeseed-run-ts.js +45 -0
  139. package/dist/scripts/validate-templates.js +6 -0
  140. package/dist/scripts/verify-driver.js +29 -0
  141. package/dist/scripts/workflow-commands.test.js +39 -0
  142. package/dist/scripts/workspace-close.js +24 -0
  143. package/dist/scripts/workspace-command-e2e.js +718 -0
  144. package/dist/scripts/workspace-lint.js +9 -0
  145. package/dist/scripts/workspace-preflight.js +22 -0
  146. package/dist/scripts/workspace-publish-changed-packages.js +16 -0
  147. package/dist/scripts/workspace-release-verify.js +81 -0
  148. package/dist/scripts/workspace-release.js +42 -0
  149. package/dist/scripts/workspace-save.js +124 -0
  150. package/dist/scripts/workspace-start-warning.js +3 -0
  151. package/dist/scripts/workspace-start.js +71 -0
  152. package/dist/scripts/workspace-test-unit.js +4 -0
  153. package/dist/scripts/workspace-test.js +11 -0
  154. package/dist/sdk-fields.d.ts +11 -0
  155. package/dist/sdk-fields.js +169 -0
  156. package/dist/sdk-filters.d.ts +4 -0
  157. package/dist/sdk-filters.js +12 -15
  158. package/dist/sdk-types.d.ts +796 -0
  159. package/dist/sdk-types.js +7 -1
  160. package/dist/sdk-version.d.ts +2 -0
  161. package/dist/sdk-version.js +42 -0
  162. package/dist/sdk.d.ts +215 -0
  163. package/dist/sdk.js +235 -11
  164. package/dist/stores/cursor-store.js +9 -3
  165. package/dist/stores/lease-store.js +8 -2
  166. package/dist/{src/stores → stores}/message-store.d.ts +1 -1
  167. package/dist/stores/message-store.js +27 -3
  168. package/dist/stores/operational-store.d.ts +24 -0
  169. package/dist/stores/operational-store.js +279 -0
  170. package/dist/stores/run-store.js +8 -1
  171. package/dist/stores/subscription-store.js +7 -5
  172. package/dist/template-catalog.d.ts +13 -0
  173. package/dist/template-catalog.js +141 -0
  174. package/dist/treeseed/services/compose.yml +7 -0
  175. package/dist/treeseed/template-catalog/catalog.fixture.json +55 -0
  176. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +2 -0
  177. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +3 -0
  178. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +32 -0
  179. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +40 -0
  180. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +1 -0
  181. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +11 -0
  182. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +11 -0
  183. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +1 -0
  184. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +3 -0
  185. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +1 -0
  186. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +19 -0
  187. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +26 -0
  188. package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +9 -0
  189. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +90 -0
  190. package/dist/utils/agents/contracts/messages.d.ts +88 -0
  191. package/dist/utils/agents/contracts/messages.js +138 -0
  192. package/dist/utils/agents/contracts/run.d.ts +20 -0
  193. package/dist/utils/agents/contracts/run.js +0 -0
  194. package/dist/utils/agents/runtime-types.d.ts +117 -0
  195. package/dist/utils/agents/runtime-types.js +4 -0
  196. package/dist/verification.d.ts +20 -0
  197. package/dist/verification.js +98 -0
  198. package/dist/workflow/operations.d.ts +396 -0
  199. package/dist/workflow/operations.js +841 -0
  200. package/dist/workflow-state.d.ts +56 -0
  201. package/dist/workflow-state.js +195 -0
  202. package/dist/workflow-support.d.ts +9 -0
  203. package/dist/workflow-support.js +176 -0
  204. package/dist/workflow.d.ts +111 -0
  205. package/dist/workflow.js +97 -0
  206. package/package.json +111 -5
  207. package/scripts/verify-driver.mjs +29 -0
  208. package/dist/scripts/.ts-run-1775630384291-crtqr3izsa.js +0 -22
  209. package/dist/scripts/.ts-run-1775630388025-vnjle0z75a.js +0 -129
  210. package/dist/scripts/assert-release-tag-version.d.ts +0 -1
  211. package/dist/scripts/build-dist.d.ts +0 -1
  212. package/dist/scripts/fixture-tools.d.ts +0 -5
  213. package/dist/scripts/package-tools.d.ts +0 -15
  214. package/dist/scripts/publish-package.d.ts +0 -1
  215. package/dist/scripts/release-verify.d.ts +0 -1
  216. package/dist/scripts/test-smoke.d.ts +0 -1
  217. package/dist/src/index.d.ts +0 -6
  218. package/dist/src/model-registry.d.ts +0 -4
  219. package/dist/src/sdk-filters.d.ts +0 -4
  220. package/dist/src/sdk-types.d.ts +0 -285
  221. package/dist/src/sdk.d.ts +0 -109
  222. package/dist/test/test-fixture.d.ts +0 -1
  223. package/dist/test/utils/envelopes.test.d.ts +0 -1
  224. package/dist/test/utils/sdk.test.d.ts +0 -1
  225. package/dist/vitest.config.d.ts +0 -2
  226. /package/dist/{src/frontmatter.d.ts → frontmatter.d.ts} +0 -0
  227. /package/dist/{src/git-runtime.d.ts → git-runtime.d.ts} +0 -0
  228. /package/dist/{src/runtime.d.ts → runtime.d.ts} +0 -0
  229. /package/dist/{src/stores → stores}/cursor-store.d.ts +0 -0
  230. /package/dist/{src/stores → stores}/envelopes.d.ts +0 -0
  231. /package/dist/{src/stores → stores}/helpers.d.ts +0 -0
  232. /package/dist/{src/stores → stores}/lease-store.d.ts +0 -0
  233. /package/dist/{src/stores → stores}/run-store.d.ts +0 -0
  234. /package/dist/{src/stores → stores}/subscription-store.d.ts +0 -0
  235. /package/dist/{src/types → types}/agents.d.ts +0 -0
  236. /package/dist/{src/types → types}/cloudflare.d.ts +0 -0
  237. /package/dist/{src/wrangler-d1.d.ts → wrangler-d1.d.ts} +0 -0
@@ -0,0 +1,1397 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
+ import {
8
+ getTreeseedEnvironmentSuggestedValues,
9
+ resolveTreeseedEnvironmentRegistry,
10
+ TREESEED_ENVIRONMENT_SCOPES,
11
+ validateTreeseedEnvironmentValues
12
+ } from "../../platform/environment.js";
13
+ import { loadTreeseedManifest } from "../../platform/tenant/config.js";
14
+ import {
15
+ createPersistentDeployTarget,
16
+ ensureGeneratedWranglerConfig,
17
+ markManagedServicesInitialized,
18
+ markDeploymentInitialized,
19
+ provisionCloudflareResources,
20
+ syncCloudflareSecrets
21
+ } from "./deploy.js";
22
+ import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
23
+ import { loadCliDeployConfig, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
24
+ const MACHINE_CONFIG_RELATIVE_PATH = ".treeseed/config/machine.yaml";
25
+ const MACHINE_KEY_HOME_RELATIVE_PATH = ".treeseed/config/machine.key";
26
+ const LEGACY_MACHINE_KEY_RELATIVE_PATH = ".treeseed/config/machine.key";
27
+ const REMOTE_AUTH_RELATIVE_PATH = ".treeseed/config/remote-auth.json";
28
+ const TEMPLATE_CATALOG_CACHE_RELATIVE_PATH = "treeseed/cache/template-catalog.json";
29
+ const TENANT_ENVIRONMENT_OVERLAY_PATH = "src/env.yaml";
30
+ const CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER = "replace-with-cloudflare-account-id";
31
+ const DEFAULT_TREESEED_API_BASE_URL = "https://api.treeseed.ai";
32
+ const DEFAULT_TEMPLATE_CATALOG_URL = "https://api.treeseed.ai/search/templates";
33
+ const TREESEED_TEMPLATE_CATALOG_URL_ENV = "TREESEED_TEMPLATE_CATALOG_URL";
34
+ const TREESEED_API_BASE_URL_ENV = "TREESEED_API_BASE_URL";
35
+ function createDefaultRemoteHost() {
36
+ return {
37
+ id: "official",
38
+ label: "TreeSeed Official API",
39
+ baseUrl: DEFAULT_TREESEED_API_BASE_URL,
40
+ official: true
41
+ };
42
+ }
43
+ function createDefaultRemoteSettings() {
44
+ return {
45
+ activeHostId: "official",
46
+ executionMode: "prefer-local",
47
+ hosts: [createDefaultRemoteHost()]
48
+ };
49
+ }
50
+ function normalizeRemoteSettings(value) {
51
+ const record = value && typeof value === "object" ? value : {};
52
+ const hosts = Array.isArray(record.hosts) ? record.hosts.filter((entry) => entry && typeof entry === "object").map((entry) => ({
53
+ id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : "official",
54
+ label: typeof entry.label === "string" && entry.label.trim() ? entry.label.trim() : void 0,
55
+ baseUrl: typeof entry.baseUrl === "string" && entry.baseUrl.trim() ? entry.baseUrl.trim().replace(/\/$/u, "") : DEFAULT_TREESEED_API_BASE_URL,
56
+ official: entry.official === true
57
+ })) : [createDefaultRemoteHost()];
58
+ return {
59
+ activeHostId: typeof record.activeHostId === "string" && record.activeHostId.trim() ? record.activeHostId.trim() : hosts[0]?.id ?? "official",
60
+ executionMode: record.executionMode === "prefer-remote" || record.executionMode === "remote-only" ? record.executionMode : "prefer-local",
61
+ hosts
62
+ };
63
+ }
64
+ function createDefaultServiceSettings() {
65
+ return {
66
+ railway: {
67
+ projectId: "",
68
+ projectName: "",
69
+ apiServiceId: "",
70
+ apiServiceName: "",
71
+ agentsServiceId: "",
72
+ agentsServiceName: ""
73
+ }
74
+ };
75
+ }
76
+ function normalizeServiceSettings(value) {
77
+ const record = value && typeof value === "object" ? value : {};
78
+ const railway = record.railway && typeof record.railway === "object" ? record.railway : {};
79
+ return {
80
+ railway: {
81
+ projectId: typeof railway.projectId === "string" ? railway.projectId : "",
82
+ projectName: typeof railway.projectName === "string" ? railway.projectName : "",
83
+ apiServiceId: typeof railway.apiServiceId === "string" ? railway.apiServiceId : "",
84
+ apiServiceName: typeof railway.apiServiceName === "string" ? railway.apiServiceName : "",
85
+ agentsServiceId: typeof railway.agentsServiceId === "string" ? railway.agentsServiceId : "",
86
+ agentsServiceName: typeof railway.agentsServiceName === "string" ? railway.agentsServiceName : ""
87
+ }
88
+ };
89
+ }
90
+ function ensureParent(filePath) {
91
+ mkdirSync(dirname(filePath), { recursive: true });
92
+ }
93
+ function parseEnvFile(contents) {
94
+ return contents.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).reduce((acc, line) => {
95
+ const separatorIndex = line.indexOf("=");
96
+ if (separatorIndex === -1) {
97
+ return acc;
98
+ }
99
+ acc[line.slice(0, separatorIndex).trim()] = line.slice(separatorIndex + 1);
100
+ return acc;
101
+ }, {});
102
+ }
103
+ function readEnvFileIfPresent(filePath) {
104
+ if (!existsSync(filePath)) {
105
+ return {};
106
+ }
107
+ return parseEnvFile(readFileSync(filePath, "utf8"));
108
+ }
109
+ function maskValue(value) {
110
+ if (!value) {
111
+ return "(unset)";
112
+ }
113
+ if (value.length <= 8) {
114
+ return "********";
115
+ }
116
+ return `${value.slice(0, 3)}...${value.slice(-3)}`;
117
+ }
118
+ function writeDeploySummary(write, summary) {
119
+ write("Treeseed deployment summary");
120
+ write(` Target: ${summary.target}`);
121
+ write(` Worker: ${summary.workerName}`);
122
+ write(` Site URL: ${summary.siteUrl}`);
123
+ write(` Account ID: ${summary.accountId}`);
124
+ write(` D1: ${summary.siteDataDb.databaseName} (${summary.siteDataDb.databaseId})`);
125
+ write(` KV FORM_GUARD_KV: ${summary.formGuardKv.id}`);
126
+ write(` KV SESSION: ${summary.sessionKv.id}`);
127
+ }
128
+ function syncManagedServiceSettingsFromDeployConfig(tenantRoot) {
129
+ const config = loadTreeseedMachineConfig(tenantRoot);
130
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
131
+ const railway = config.settings.services.railway;
132
+ railway.projectId = deployConfig.services?.api?.railway?.projectId ?? deployConfig.services?.agents?.railway?.projectId ?? railway.projectId;
133
+ railway.projectName = deployConfig.services?.api?.railway?.projectName ?? deployConfig.services?.agents?.railway?.projectName ?? railway.projectName;
134
+ railway.apiServiceId = deployConfig.services?.api?.railway?.serviceId ?? railway.apiServiceId;
135
+ railway.apiServiceName = deployConfig.services?.api?.railway?.serviceName ?? railway.apiServiceName;
136
+ railway.agentsServiceId = deployConfig.services?.agents?.railway?.serviceId ?? railway.agentsServiceId;
137
+ railway.agentsServiceName = deployConfig.services?.agents?.railway?.serviceName ?? railway.agentsServiceName;
138
+ const remote = normalizeRemoteSettings(config.settings.remote);
139
+ const defaultHostBaseUrl = deployConfig.services?.api?.environments?.prod?.baseUrl ?? deployConfig.services?.api?.publicBaseUrl ?? remote.hosts[0]?.baseUrl ?? DEFAULT_TREESEED_API_BASE_URL;
140
+ const officialHost = remote.hosts.find((entry) => entry.id === "official");
141
+ if (officialHost) {
142
+ officialHost.baseUrl = defaultHostBaseUrl.replace(/\/$/u, "");
143
+ } else {
144
+ remote.hosts.unshift({
145
+ id: "official",
146
+ label: "TreeSeed Official API",
147
+ baseUrl: defaultHostBaseUrl.replace(/\/$/u, ""),
148
+ official: true
149
+ });
150
+ }
151
+ config.settings.remote = remote;
152
+ writeTreeseedMachineConfig(tenantRoot, config);
153
+ return config;
154
+ }
155
+ function loadTenantDeployConfig(tenantRoot) {
156
+ return loadCliDeployConfig(tenantRoot);
157
+ }
158
+ function loadOptionalTenantManifest(tenantRoot) {
159
+ try {
160
+ return withProcessCwd(tenantRoot, () => loadTreeseedManifest());
161
+ } catch {
162
+ return void 0;
163
+ }
164
+ }
165
+ function findNearestTreeseedMachineConfig(startRoot = process.cwd()) {
166
+ let current = resolve(startRoot);
167
+ while (true) {
168
+ const configPath = resolve(current, MACHINE_CONFIG_RELATIVE_PATH);
169
+ if (existsSync(configPath)) {
170
+ return configPath;
171
+ }
172
+ const parent = resolve(current, "..");
173
+ if (parent === current) {
174
+ break;
175
+ }
176
+ current = parent;
177
+ }
178
+ return null;
179
+ }
180
+ function getTreeseedMachineConfigPaths(tenantRoot) {
181
+ const homeRoot = process.env.HOME && process.env.HOME.trim().length > 0 ? process.env.HOME : homedir();
182
+ return {
183
+ configPath: resolve(tenantRoot, MACHINE_CONFIG_RELATIVE_PATH),
184
+ authPath: resolve(tenantRoot, REMOTE_AUTH_RELATIVE_PATH),
185
+ keyPath: resolve(homeRoot, MACHINE_KEY_HOME_RELATIVE_PATH),
186
+ legacyKeyPath: resolve(tenantRoot, LEGACY_MACHINE_KEY_RELATIVE_PATH)
187
+ };
188
+ }
189
+ function getTreeseedRemoteAuthPaths(tenantRoot) {
190
+ return {
191
+ authPath: getTreeseedMachineConfigPaths(tenantRoot).authPath
192
+ };
193
+ }
194
+ function createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantConfig }) {
195
+ return {
196
+ version: 1,
197
+ project: {
198
+ tenantRoot,
199
+ tenantId: tenantConfig?.id ?? deployConfig.slug,
200
+ slug: deployConfig.slug,
201
+ name: deployConfig.name,
202
+ siteUrl: deployConfig.siteUrl,
203
+ overlayPath: resolve(tenantRoot, TENANT_ENVIRONMENT_OVERLAY_PATH)
204
+ },
205
+ settings: {
206
+ sync: {
207
+ github: true,
208
+ cloudflare: true
209
+ },
210
+ templates: {
211
+ catalogEndpoint: DEFAULT_TEMPLATE_CATALOG_URL
212
+ },
213
+ remote: createDefaultRemoteSettings(),
214
+ services: createDefaultServiceSettings()
215
+ },
216
+ environments: Object.fromEntries(
217
+ TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
218
+ scope,
219
+ {
220
+ values: {},
221
+ secrets: {}
222
+ }
223
+ ])
224
+ )
225
+ };
226
+ }
227
+ function readMachineKey(keyPath) {
228
+ if (existsSync(keyPath)) {
229
+ return Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
230
+ }
231
+ return null;
232
+ }
233
+ function writeMachineKey(keyPath, key) {
234
+ ensureParent(keyPath);
235
+ writeFileSync(keyPath, `${key.toString("base64")}
236
+ `, { mode: 384 });
237
+ }
238
+ function ensureHomeMachineKey(tenantRoot) {
239
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
240
+ const existing = readMachineKey(keyPath);
241
+ if (existing) {
242
+ return existing;
243
+ }
244
+ const key = randomBytes(32);
245
+ writeMachineKey(keyPath, key);
246
+ return key;
247
+ }
248
+ function loadLegacyMachineKey(tenantRoot) {
249
+ const { legacyKeyPath } = getTreeseedMachineConfigPaths(tenantRoot);
250
+ return readMachineKey(legacyKeyPath);
251
+ }
252
+ function createDefaultRemoteAuthState() {
253
+ return {
254
+ version: 1,
255
+ sessions: {}
256
+ };
257
+ }
258
+ function encryptValue(value, key) {
259
+ const iv = randomBytes(12);
260
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
261
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
262
+ const tag = cipher.getAuthTag();
263
+ return {
264
+ algorithm: "aes-256-gcm",
265
+ iv: iv.toString("base64"),
266
+ tag: tag.toString("base64"),
267
+ ciphertext: ciphertext.toString("base64")
268
+ };
269
+ }
270
+ function decryptValue(payload, key) {
271
+ if (!payload || typeof payload !== "object") {
272
+ return "";
273
+ }
274
+ const decipher = createDecipheriv(
275
+ "aes-256-gcm",
276
+ key,
277
+ Buffer.from(String(payload.iv ?? ""), "base64")
278
+ );
279
+ decipher.setAuthTag(Buffer.from(String(payload.tag ?? ""), "base64"));
280
+ const decrypted = Buffer.concat([
281
+ decipher.update(Buffer.from(String(payload.ciphertext ?? ""), "base64")),
282
+ decipher.final()
283
+ ]);
284
+ return decrypted.toString("utf8");
285
+ }
286
+ function decryptMachineConfigSecrets(config, key) {
287
+ const secrets = {};
288
+ for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
289
+ secrets[scope] = {};
290
+ for (const [entryId, payload] of Object.entries(config.environments?.[scope]?.secrets ?? {})) {
291
+ secrets[scope][entryId] = decryptValue(payload, key);
292
+ }
293
+ }
294
+ return secrets;
295
+ }
296
+ function applyMachineConfigSecrets(config, secrets, key) {
297
+ for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
298
+ const scoped = config.environments?.[scope];
299
+ if (!scoped) {
300
+ continue;
301
+ }
302
+ for (const [entryId, value] of Object.entries(secrets[scope] ?? {})) {
303
+ scoped.secrets[entryId] = encryptValue(value, key);
304
+ }
305
+ }
306
+ return config;
307
+ }
308
+ function decryptRemoteAuthSessions(payload, key) {
309
+ return Object.fromEntries(
310
+ Object.entries(payload.sessions ?? {}).map(([hostId, entry]) => [
311
+ hostId,
312
+ {
313
+ accessToken: decryptValue(entry.accessToken, key),
314
+ refreshToken: decryptValue(entry.refreshToken, key),
315
+ expiresAt: typeof entry.expiresAt === "string" ? entry.expiresAt : "",
316
+ principal: entry.principal ?? null
317
+ }
318
+ ])
319
+ );
320
+ }
321
+ function encryptRemoteAuthSessions(sessions, key) {
322
+ return Object.fromEntries(
323
+ Object.entries(sessions ?? {}).map(([hostId, entry]) => [
324
+ hostId,
325
+ {
326
+ accessToken: entry.accessToken ? encryptValue(entry.accessToken, key) : null,
327
+ refreshToken: entry.refreshToken ? encryptValue(entry.refreshToken, key) : null,
328
+ expiresAt: entry.expiresAt ?? "",
329
+ principal: entry.principal ?? null
330
+ }
331
+ ])
332
+ );
333
+ }
334
+ function loadRemoteAuthPayload(tenantRoot) {
335
+ const { authPath } = getTreeseedRemoteAuthPaths(tenantRoot);
336
+ if (!existsSync(authPath)) {
337
+ return createDefaultRemoteAuthState();
338
+ }
339
+ try {
340
+ const raw = JSON.parse(readFileSync(authPath, "utf8"));
341
+ return raw && typeof raw === "object" ? raw : createDefaultRemoteAuthState();
342
+ } catch {
343
+ return createDefaultRemoteAuthState();
344
+ }
345
+ }
346
+ function writeRemoteAuthPayload(tenantRoot, payload) {
347
+ const { authPath } = getTreeseedRemoteAuthPaths(tenantRoot);
348
+ ensureParent(authPath);
349
+ writeFileSync(authPath, `${JSON.stringify(payload, null, 2)}
350
+ `, "utf8");
351
+ }
352
+ function removeLegacyMachineKeyIfSafe(tenantRoot) {
353
+ const { legacyKeyPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
354
+ if (legacyKeyPath !== keyPath && existsSync(legacyKeyPath)) {
355
+ rmSync(legacyKeyPath, { force: true });
356
+ }
357
+ }
358
+ function reencryptTreeseedEncryptedState(tenantRoot, oldKey, newKey) {
359
+ const machineConfig = loadTreeseedMachineConfig(tenantRoot);
360
+ const machineSecrets = decryptMachineConfigSecrets(machineConfig, oldKey);
361
+ const { authPath } = getTreeseedRemoteAuthPaths(tenantRoot);
362
+ const remoteAuthExists = existsSync(authPath);
363
+ const remoteAuthPayload = loadRemoteAuthPayload(tenantRoot);
364
+ const remoteSessions = decryptRemoteAuthSessions(remoteAuthPayload, oldKey);
365
+ writeTreeseedMachineConfig(tenantRoot, applyMachineConfigSecrets(machineConfig, machineSecrets, newKey));
366
+ if (remoteAuthExists || Object.keys(remoteSessions).length > 0) {
367
+ writeRemoteAuthPayload(tenantRoot, {
368
+ version: 1,
369
+ sessions: encryptRemoteAuthSessions(remoteSessions, newKey)
370
+ });
371
+ }
372
+ }
373
+ function ensureTreeseedMachineKeyMigrated(tenantRoot) {
374
+ const homeKey = ensureHomeMachineKey(tenantRoot);
375
+ const legacyKey = loadLegacyMachineKey(tenantRoot);
376
+ if (!legacyKey) {
377
+ return homeKey;
378
+ }
379
+ try {
380
+ reencryptTreeseedEncryptedState(tenantRoot, legacyKey, homeKey);
381
+ removeLegacyMachineKeyIfSafe(tenantRoot);
382
+ } catch {
383
+ }
384
+ return homeKey;
385
+ }
386
+ function loadMachineKey(tenantRoot) {
387
+ return ensureTreeseedMachineKeyMigrated(tenantRoot);
388
+ }
389
+ function decryptValueWithMachineKey(tenantRoot, payload, key) {
390
+ try {
391
+ return decryptValue(payload, key);
392
+ } catch (error) {
393
+ const legacyKey = loadLegacyMachineKey(tenantRoot);
394
+ if (!legacyKey) {
395
+ throw error;
396
+ }
397
+ const value = decryptValue(payload, legacyKey);
398
+ reencryptTreeseedEncryptedState(tenantRoot, legacyKey, key);
399
+ removeLegacyMachineKeyIfSafe(tenantRoot);
400
+ return value;
401
+ }
402
+ }
403
+ function rotateTreeseedMachineKey(tenantRoot) {
404
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
405
+ const oldKey = loadMachineKey(tenantRoot);
406
+ const newKey = randomBytes(32);
407
+ reencryptTreeseedEncryptedState(tenantRoot, oldKey, newKey);
408
+ writeMachineKey(keyPath, newKey);
409
+ removeLegacyMachineKeyIfSafe(tenantRoot);
410
+ return {
411
+ keyPath,
412
+ rotated: true
413
+ };
414
+ }
415
+ function loadTreeseedRemoteAuthState(tenantRoot) {
416
+ const key = loadMachineKey(tenantRoot);
417
+ const payload = loadRemoteAuthPayload(tenantRoot);
418
+ let sessions;
419
+ try {
420
+ sessions = decryptRemoteAuthSessions(payload, key);
421
+ } catch (error) {
422
+ const legacyKey = loadLegacyMachineKey(tenantRoot);
423
+ if (!legacyKey) {
424
+ throw error;
425
+ }
426
+ sessions = decryptRemoteAuthSessions(payload, legacyKey);
427
+ reencryptTreeseedEncryptedState(tenantRoot, legacyKey, key);
428
+ removeLegacyMachineKeyIfSafe(tenantRoot);
429
+ }
430
+ return {
431
+ version: 1,
432
+ sessions
433
+ };
434
+ }
435
+ function writeTreeseedRemoteAuthState(tenantRoot, state) {
436
+ const key = loadMachineKey(tenantRoot);
437
+ writeRemoteAuthPayload(tenantRoot, {
438
+ version: 1,
439
+ sessions: encryptRemoteAuthSessions(state.sessions, key)
440
+ });
441
+ }
442
+ function setTreeseedRemoteSession(tenantRoot, { hostId, accessToken, refreshToken, expiresAt, principal }) {
443
+ const state = loadTreeseedRemoteAuthState(tenantRoot);
444
+ state.sessions[hostId] = {
445
+ accessToken,
446
+ refreshToken,
447
+ expiresAt,
448
+ principal: principal ?? null
449
+ };
450
+ writeTreeseedRemoteAuthState(tenantRoot, state);
451
+ return state.sessions[hostId];
452
+ }
453
+ function clearTreeseedRemoteSession(tenantRoot, hostId) {
454
+ const state = loadTreeseedRemoteAuthState(tenantRoot);
455
+ if (hostId) {
456
+ delete state.sessions[hostId];
457
+ } else {
458
+ state.sessions = {};
459
+ }
460
+ writeTreeseedRemoteAuthState(tenantRoot, state);
461
+ return state;
462
+ }
463
+ function resolveTreeseedRemoteSession(tenantRoot, hostId) {
464
+ const { authPath } = getTreeseedRemoteAuthPaths(tenantRoot);
465
+ if (!existsSync(authPath)) {
466
+ return null;
467
+ }
468
+ const sessionState = loadTreeseedRemoteAuthState(tenantRoot);
469
+ const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
470
+ const selectedHostId = hostId ?? (existsSync(configPath) ? loadTreeseedMachineConfig(tenantRoot).settings?.remote?.activeHostId : "official") ?? "official";
471
+ return sessionState.sessions?.[selectedHostId] ?? null;
472
+ }
473
+ function loadTreeseedMachineConfig(tenantRoot) {
474
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
475
+ const tenantConfig = loadOptionalTenantManifest(tenantRoot);
476
+ const defaults = createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantConfig });
477
+ const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
478
+ if (!existsSync(configPath)) {
479
+ return defaults;
480
+ }
481
+ const raw = parseYaml(readFileSync(configPath, "utf8")) ?? {};
482
+ const parsed = raw && typeof raw === "object" ? raw : {};
483
+ return {
484
+ ...defaults,
485
+ ...parsed,
486
+ project: {
487
+ ...defaults.project,
488
+ ...parsed.project ?? {}
489
+ },
490
+ settings: {
491
+ ...defaults.settings,
492
+ ...parsed.settings ?? {},
493
+ sync: {
494
+ ...defaults.settings.sync,
495
+ ...parsed.settings?.sync ?? {}
496
+ },
497
+ templates: {
498
+ ...defaults.settings.templates ?? {},
499
+ ...parsed.settings?.templates ?? {}
500
+ },
501
+ remote: normalizeRemoteSettings({
502
+ ...defaults.settings.remote ?? {},
503
+ ...parsed.settings?.remote ?? {}
504
+ }),
505
+ services: normalizeServiceSettings({
506
+ ...defaults.settings.services ?? {},
507
+ ...parsed.settings?.services ?? {}
508
+ })
509
+ },
510
+ environments: Object.fromEntries(
511
+ TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
512
+ scope,
513
+ {
514
+ values: {
515
+ ...defaults.environments?.[scope]?.values ?? {},
516
+ ...parsed.environments?.[scope]?.values ?? {}
517
+ },
518
+ secrets: {
519
+ ...defaults.environments?.[scope]?.secrets ?? {},
520
+ ...parsed.environments?.[scope]?.secrets ?? {}
521
+ }
522
+ }
523
+ ])
524
+ )
525
+ };
526
+ }
527
+ function writeTreeseedMachineConfig(tenantRoot, config) {
528
+ const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
529
+ ensureParent(configPath);
530
+ writeFileSync(configPath, stringifyYaml(config), "utf8");
531
+ }
532
+ function resolveTreeseedRemoteConfig(startRoot = process.cwd(), env = process.env) {
533
+ const machineConfigPath = findNearestTreeseedMachineConfig(startRoot);
534
+ const tenantRoot = machineConfigPath ? resolve(dirname(dirname(machineConfigPath)), "..") : startRoot;
535
+ const deployConfig = existsSync(resolve(tenantRoot, "treeseed.site.yaml")) ? loadTenantDeployConfig(tenantRoot) : null;
536
+ const machineConfig = machineConfigPath ? loadTreeseedMachineConfig(tenantRoot) : createDefaultTreeseedMachineConfig({
537
+ tenantRoot: startRoot,
538
+ deployConfig: {
539
+ name: "TreeSeed",
540
+ slug: "treeseed",
541
+ siteUrl: DEFAULT_TREESEED_API_BASE_URL,
542
+ contactEmail: "hello@treeseed.ai"
543
+ },
544
+ tenantConfig: void 0
545
+ });
546
+ const settings = normalizeRemoteSettings(machineConfig.settings?.remote);
547
+ const deployBaseUrl = deployConfig?.services?.api?.environments?.prod?.baseUrl ?? deployConfig?.services?.api?.publicBaseUrl ?? null;
548
+ if (deployBaseUrl) {
549
+ const officialHost = settings.hosts.find((entry) => entry.id === "official");
550
+ if (officialHost) {
551
+ officialHost.baseUrl = deployBaseUrl.replace(/\/$/u, "");
552
+ }
553
+ }
554
+ const envBaseUrl = env[TREESEED_API_BASE_URL_ENV];
555
+ const hosts = envBaseUrl && envBaseUrl.trim().length > 0 ? [
556
+ {
557
+ id: "env",
558
+ label: "Environment override",
559
+ baseUrl: envBaseUrl.trim().replace(/\/$/u, "")
560
+ },
561
+ ...settings.hosts.filter((entry) => entry.id !== "env")
562
+ ] : settings.hosts;
563
+ const activeHostId = envBaseUrl && envBaseUrl.trim().length > 0 ? "env" : settings.activeHostId;
564
+ const auth = resolveTreeseedRemoteSession(tenantRoot, activeHostId);
565
+ return {
566
+ hosts,
567
+ activeHostId,
568
+ executionMode: settings.executionMode,
569
+ auth: auth ? {
570
+ accessToken: auth.accessToken,
571
+ refreshToken: auth.refreshToken,
572
+ expiresAt: auth.expiresAt,
573
+ principal: auth.principal ?? null
574
+ } : void 0
575
+ };
576
+ }
577
+ function resolveTreeseedTemplateCatalogEndpoint(startRoot = process.cwd(), env = process.env) {
578
+ const envValue = env[TREESEED_TEMPLATE_CATALOG_URL_ENV];
579
+ if (typeof envValue === "string" && envValue.trim().length > 0) {
580
+ return envValue.trim();
581
+ }
582
+ const machineConfigPath = findNearestTreeseedMachineConfig(startRoot);
583
+ if (!machineConfigPath) {
584
+ return DEFAULT_TEMPLATE_CATALOG_URL;
585
+ }
586
+ const raw = parseYaml(readFileSync(machineConfigPath, "utf8")) ?? {};
587
+ const parsed = raw && typeof raw === "object" ? raw : {};
588
+ const configuredEndpoint = parsed.settings?.templates?.catalogEndpoint;
589
+ return typeof configuredEndpoint === "string" && configuredEndpoint.trim().length > 0 ? configuredEndpoint.trim() : DEFAULT_TEMPLATE_CATALOG_URL;
590
+ }
591
+ function resolveTreeseedTemplateCatalogCachePath(startRoot = process.cwd()) {
592
+ const machineConfigPath = findNearestTreeseedMachineConfig(startRoot);
593
+ if (machineConfigPath) {
594
+ return resolve(dirname(dirname(machineConfigPath)), "cache", "template-catalog.json");
595
+ }
596
+ return resolve(tmpdir(), TEMPLATE_CATALOG_CACHE_RELATIVE_PATH);
597
+ }
598
+ function ensureTreeseedGitignoreEntries(tenantRoot) {
599
+ const gitignorePath = resolve(tenantRoot, ".gitignore");
600
+ const requiredEntries = [".env.local", ".dev.vars", ".treeseed/"];
601
+ const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
602
+ const lines = current.split(/\r?\n/);
603
+ let changed = false;
604
+ for (const entry of requiredEntries) {
605
+ if (!lines.includes(entry)) {
606
+ lines.push(entry);
607
+ changed = true;
608
+ }
609
+ }
610
+ if (changed || !existsSync(gitignorePath)) {
611
+ writeFileSync(gitignorePath, `${lines.filter(Boolean).join("\n")}
612
+ `, "utf8");
613
+ }
614
+ return gitignorePath;
615
+ }
616
+ function resolveTreeseedMachineEnvironmentValues(tenantRoot, scope) {
617
+ const key = loadMachineKey(tenantRoot);
618
+ const config = loadTreeseedMachineConfig(tenantRoot);
619
+ const values = {
620
+ ...config.environments?.[scope]?.values ?? {}
621
+ };
622
+ for (const [entryId, payload] of Object.entries(config.environments?.[scope]?.secrets ?? {})) {
623
+ values[entryId] = decryptValueWithMachineKey(tenantRoot, payload, key);
624
+ }
625
+ return values;
626
+ }
627
+ function setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, value) {
628
+ const key = loadMachineKey(tenantRoot);
629
+ const config = loadTreeseedMachineConfig(tenantRoot);
630
+ const scoped = config.environments[scope];
631
+ if (entry.sensitivity === "secret") {
632
+ delete scoped.values[entry.id];
633
+ if (value) {
634
+ scoped.secrets[entry.id] = encryptValue(value, key);
635
+ } else {
636
+ delete scoped.secrets[entry.id];
637
+ }
638
+ } else {
639
+ delete scoped.secrets[entry.id];
640
+ if (value) {
641
+ scoped.values[entry.id] = value;
642
+ } else {
643
+ delete scoped.values[entry.id];
644
+ }
645
+ }
646
+ writeTreeseedMachineConfig(tenantRoot, config);
647
+ return config;
648
+ }
649
+ function collectTreeseedEnvironmentContext(tenantRoot) {
650
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
651
+ const tenantConfig = loadOptionalTenantManifest(tenantRoot);
652
+ return resolveTreeseedEnvironmentRegistry({
653
+ deployConfig,
654
+ tenantConfig
655
+ });
656
+ }
657
+ function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
658
+ return {
659
+ ...readEnvFileIfPresent(resolve(tenantRoot, ".env.local")),
660
+ ...readEnvFileIfPresent(resolve(tenantRoot, ".dev.vars")),
661
+ ...Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0])),
662
+ ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope)
663
+ };
664
+ }
665
+ function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
666
+ const values = {};
667
+ const sources = {};
668
+ const merge = (source, entries) => {
669
+ for (const [key, value] of Object.entries(entries)) {
670
+ if (typeof value !== "string" || value.length === 0) {
671
+ continue;
672
+ }
673
+ values[key] = value;
674
+ sources[key] = source;
675
+ }
676
+ };
677
+ merge(".env.local", readEnvFileIfPresent(resolve(tenantRoot, ".env.local")));
678
+ merge(".dev.vars", readEnvFileIfPresent(resolve(tenantRoot, ".dev.vars")));
679
+ merge("process.env", Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0])));
680
+ merge("machine-config", resolveTreeseedMachineEnvironmentValues(tenantRoot, scope));
681
+ return { values, sources };
682
+ }
683
+ function formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env = process.env, revealSecrets = false }) {
684
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
685
+ const { values, sources } = collectTreeseedConfigSeedValueSources(tenantRoot, scope, env);
686
+ const lines = [
687
+ formatConfigSectionTitle(`Resolved environment values for ${scope}`),
688
+ revealSecrets ? "Secrets are shown because --show-secrets was provided." : "Secret values are masked. Re-run with --show-secrets to print full values."
689
+ ];
690
+ for (const entry of registry.entries.filter((candidate) => candidate.scopes.includes(scope))) {
691
+ const value = values[entry.id];
692
+ const displayValue = typeof value === "string" && value.length > 0 ? entry.sensitivity === "secret" && !revealSecrets ? maskValue(value) : value : "(unset)";
693
+ lines.push(`${entry.id}=${displayValue} (${sources[entry.id] ?? "unset"})`);
694
+ }
695
+ return lines.join("\n");
696
+ }
697
+ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false }) {
698
+ const resolvedValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
699
+ for (const [key, value] of Object.entries(resolvedValues)) {
700
+ const currentValue = process.env[key] ?? "";
701
+ const shouldReplacePlaceholder = key === "CLOUDFLARE_ACCOUNT_ID" && currentValue === CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER;
702
+ if ((override || currentValue.length === 0 || shouldReplacePlaceholder) && typeof value === "string" && value.length > 0) {
703
+ process.env[key] = value;
704
+ }
705
+ }
706
+ return resolvedValues;
707
+ }
708
+ function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
709
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
710
+ const machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
711
+ const values = {
712
+ ...machineValues,
713
+ ...Object.fromEntries(Object.entries(process.env).map(([key, value]) => [key, value ?? void 0]))
714
+ };
715
+ const validation = validateTreeseedEnvironmentValues({
716
+ values,
717
+ scope,
718
+ purpose,
719
+ deployConfig: registry.context.deployConfig,
720
+ tenantConfig: registry.context.tenantConfig,
721
+ plugins: registry.context.plugins
722
+ });
723
+ return {
724
+ registry,
725
+ values,
726
+ validation
727
+ };
728
+ }
729
+ function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
730
+ const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
731
+ if (report.validation.ok) {
732
+ return report;
733
+ }
734
+ const lines = [
735
+ `Treeseed environment is not ready for ${purpose} (${scope}).`,
736
+ "Run `treeseed config` to fill in the missing values, or export them in the current shell."
737
+ ];
738
+ for (const problem of [...report.validation.missing, ...report.validation.invalid]) {
739
+ lines.push(`- ${problem.message}`);
740
+ }
741
+ const error = new Error(lines.join("\n"));
742
+ error.kind = report.validation.missing.length > 0 ? "missing_config" : "invalid_config";
743
+ error.details = report.validation;
744
+ throw error;
745
+ }
746
+ function renderEnvEntries(entries, values) {
747
+ return entries.map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0).map(([key, value]) => `${key}=${value}`).join("\n");
748
+ }
749
+ function writeTreeseedLocalEnvironmentFiles(tenantRoot) {
750
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
751
+ const scope = "local";
752
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
753
+ const envEntries = registry.entries.filter(
754
+ (entry) => entry.scopes.includes(scope) && entry.targets.includes("local-file")
755
+ );
756
+ const devVarsEntries = registry.entries.filter(
757
+ (entry) => entry.scopes.includes(scope) && entry.targets.includes("wrangler-dev-vars")
758
+ );
759
+ writeFileSync(resolve(tenantRoot, ".env.local"), `${renderEnvEntries(envEntries, values)}
760
+ `, "utf8");
761
+ writeFileSync(resolve(tenantRoot, ".dev.vars"), `${renderEnvEntries(devVarsEntries, values)}
762
+ `, "utf8");
763
+ return {
764
+ envLocalPath: resolve(tenantRoot, ".env.local"),
765
+ devVarsPath: resolve(tenantRoot, ".dev.vars")
766
+ };
767
+ }
768
+ function runGh(args, { cwd, dryRun = false, input } = {}) {
769
+ if (dryRun) {
770
+ return { status: 0, stdout: "", stderr: "" };
771
+ }
772
+ const result = spawnSync("gh", args, {
773
+ cwd,
774
+ stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
775
+ encoding: "utf8",
776
+ input
777
+ });
778
+ if (result.status !== 0) {
779
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `gh ${args.join(" ")} failed`);
780
+ }
781
+ return result;
782
+ }
783
+ function runRailway(args, { cwd, dryRun = false, input } = {}) {
784
+ if (dryRun) {
785
+ return { status: 0, stdout: "", stderr: "" };
786
+ }
787
+ const result = spawnSync("railway", args, {
788
+ cwd,
789
+ stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
790
+ encoding: "utf8",
791
+ input
792
+ });
793
+ if (result.status !== 0) {
794
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${args.join(" ")} failed`);
795
+ }
796
+ return result;
797
+ }
798
+ function commandAvailable(command) {
799
+ const result = spawnSync("bash", ["-lc", `command -v ${command}`], {
800
+ stdio: "pipe",
801
+ encoding: "utf8"
802
+ });
803
+ return result.status === 0;
804
+ }
805
+ function checkCommand(command, args, { cwd, env } = {}) {
806
+ const result = spawnSync(command, args, {
807
+ cwd,
808
+ stdio: "pipe",
809
+ encoding: "utf8",
810
+ env: { ...process.env, ...env ?? {} }
811
+ });
812
+ return {
813
+ ok: result.status === 0,
814
+ status: result.status ?? 1,
815
+ stdout: result.stdout?.trim() ?? "",
816
+ stderr: result.stderr?.trim() ?? "",
817
+ detail: `${result.stderr ?? ""}
818
+ ${result.stdout ?? ""}`.trim()
819
+ };
820
+ }
821
+ function toolStatus(name, available, detail, extra = {}) {
822
+ return {
823
+ name,
824
+ available,
825
+ detail,
826
+ ...extra
827
+ };
828
+ }
829
+ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), installIfMissing = true, env = process.env, write } = {}) {
830
+ const githubCli = !commandAvailable("gh") ? toolStatus("githubCli", false, "GitHub CLI `gh` is not installed.") : (() => {
831
+ const check = checkCommand("gh", ["--version"], { cwd: tenantRoot, env });
832
+ return toolStatus("githubCli", check.ok, check.ok ? check.stdout.split("\n")[0] ?? "GitHub CLI detected." : check.detail || "GitHub CLI check failed.");
833
+ })();
834
+ let ghActExtension = toolStatus("ghActExtension", false, "GitHub CLI extension `gh-act` is not installed.", {
835
+ attemptedInstall: false,
836
+ installedDuringConfig: false
837
+ });
838
+ if (githubCli.available) {
839
+ const check = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
840
+ if (check.ok) {
841
+ ghActExtension = toolStatus("ghActExtension", true, check.stdout.split("\n")[0] ?? "gh-act is installed.", {
842
+ attemptedInstall: false,
843
+ installedDuringConfig: false
844
+ });
845
+ } else if (installIfMissing) {
846
+ write?.("Installing GitHub CLI extension `gh-act`...");
847
+ const install = checkCommand("gh", ["extension", "install", "https://github.com/nektos/gh-act"], { cwd: tenantRoot, env });
848
+ const postInstall = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
849
+ ghActExtension = toolStatus(
850
+ "ghActExtension",
851
+ postInstall.ok,
852
+ postInstall.ok ? postInstall.stdout.split("\n")[0] ?? "gh-act is installed." : install.detail || postInstall.detail || "Unable to install the gh-act extension.",
853
+ {
854
+ attemptedInstall: true,
855
+ installedDuringConfig: postInstall.ok,
856
+ installStatus: install.status
857
+ }
858
+ );
859
+ } else {
860
+ ghActExtension = toolStatus("ghActExtension", false, check.detail || "GitHub CLI extension `gh-act` is not installed.", {
861
+ attemptedInstall: false,
862
+ installedDuringConfig: false
863
+ });
864
+ }
865
+ }
866
+ const dockerCheck = checkCommand("docker", ["info"], { cwd: tenantRoot, env });
867
+ const dockerDaemon = toolStatus(
868
+ "dockerDaemon",
869
+ dockerCheck.ok,
870
+ dockerCheck.ok ? dockerCheck.stdout.split("\n")[0] ?? "Docker daemon is available." : dockerCheck.detail || "Docker daemon is unavailable."
871
+ );
872
+ const remediation = [];
873
+ if (!githubCli.available) {
874
+ remediation.push("Install GitHub CLI from https://cli.github.com/ and rerun `treeseed config`.");
875
+ }
876
+ if (githubCli.available && !ghActExtension.available) {
877
+ remediation.push("Run `gh extension install https://github.com/nektos/gh-act` and rerun `treeseed config`.");
878
+ }
879
+ if (!dockerDaemon.available) {
880
+ remediation.push("Start Docker Desktop or another local Docker daemon, then rerun `treeseed config`.");
881
+ }
882
+ return {
883
+ githubCli,
884
+ ghActExtension,
885
+ dockerDaemon,
886
+ actVerificationReady: githubCli.available && ghActExtension.available && dockerDaemon.available,
887
+ remediation
888
+ };
889
+ }
890
+ function formatCheckOutput(result) {
891
+ return `${result.stderr ?? ""}
892
+ ${result.stdout ?? ""}`.trim();
893
+ }
894
+ function providerConnectionResult(provider, ready, detail, extra = {}) {
895
+ return {
896
+ provider,
897
+ ready,
898
+ detail,
899
+ ...extra
900
+ };
901
+ }
902
+ function checkGitHubConnection({ tenantRoot, env }) {
903
+ if (!env.GH_TOKEN) {
904
+ return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
905
+ }
906
+ if (!commandAvailable("gh")) {
907
+ return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
908
+ }
909
+ const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
910
+ const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
911
+ const result = spawnSync("gh", args, {
912
+ cwd: tenantRoot,
913
+ stdio: "pipe",
914
+ encoding: "utf8",
915
+ env: { ...process.env, ...env }
916
+ });
917
+ if (result.status !== 0) {
918
+ return providerConnectionResult("github", false, formatCheckOutput(result) || "GitHub API check failed.");
919
+ }
920
+ const resolved = result.stdout.trim();
921
+ return providerConnectionResult(
922
+ "github",
923
+ true,
924
+ repository ? `GitHub token can access ${resolved || repository}.` : resolved ? `Authenticated as ${resolved}.` : "GitHub API check succeeded."
925
+ );
926
+ }
927
+ function checkCloudflareConnection({ tenantRoot, env }) {
928
+ if (!env.CLOUDFLARE_API_TOKEN) {
929
+ return providerConnectionResult("cloudflare", false, "CLOUDFLARE_API_TOKEN is not configured.", { skipped: true });
930
+ }
931
+ try {
932
+ const result = spawnSync(process.execPath, [resolveWranglerBin(), "whoami"], {
933
+ cwd: tenantRoot,
934
+ stdio: "pipe",
935
+ encoding: "utf8",
936
+ env: { ...process.env, ...env }
937
+ });
938
+ if (result.status !== 0) {
939
+ return providerConnectionResult("cloudflare", false, formatCheckOutput(result) || "Cloudflare Wrangler check failed.");
940
+ }
941
+ return providerConnectionResult("cloudflare", true, "Wrangler authenticated with CLOUDFLARE_API_TOKEN.");
942
+ } catch (error) {
943
+ return providerConnectionResult("cloudflare", false, error instanceof Error ? error.message : "Cloudflare Wrangler check failed.");
944
+ }
945
+ }
946
+ function checkRailwayConnection({ tenantRoot, env }) {
947
+ if (!env.RAILWAY_API_TOKEN && !env.RAILWAY_TOKEN) {
948
+ return providerConnectionResult("railway", false, "RAILWAY_API_TOKEN or RAILWAY_TOKEN is not configured.", { skipped: true });
949
+ }
950
+ if (!commandAvailable("railway")) {
951
+ return providerConnectionResult("railway", false, "Railway CLI `railway` is not installed.");
952
+ }
953
+ const result = spawnSync("railway", ["whoami"], {
954
+ cwd: tenantRoot,
955
+ stdio: "pipe",
956
+ encoding: "utf8",
957
+ env: { ...process.env, ...env }
958
+ });
959
+ if (result.status !== 0) {
960
+ return providerConnectionResult("railway", false, formatCheckOutput(result) || "Railway CLI check failed.");
961
+ }
962
+ return providerConnectionResult("railway", true, result.stdout.trim() || "Railway CLI check succeeded.");
963
+ }
964
+ function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = process.env } = {}) {
965
+ const values = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
966
+ const commandEnv = {
967
+ GH_TOKEN: values.GH_TOKEN,
968
+ CLOUDFLARE_API_TOKEN: values.CLOUDFLARE_API_TOKEN,
969
+ CLOUDFLARE_ACCOUNT_ID: values.CLOUDFLARE_ACCOUNT_ID,
970
+ RAILWAY_API_TOKEN: values.RAILWAY_API_TOKEN,
971
+ RAILWAY_TOKEN: values.RAILWAY_TOKEN
972
+ };
973
+ const checks = [
974
+ checkGitHubConnection({ tenantRoot, env: commandEnv }),
975
+ checkCloudflareConnection({ tenantRoot, env: commandEnv }),
976
+ checkRailwayConnection({ tenantRoot, env: commandEnv })
977
+ ];
978
+ return {
979
+ scope,
980
+ ok: checks.every((check) => check.ready || check.skipped),
981
+ checks
982
+ };
983
+ }
984
+ function formatTreeseedProviderConnectionReport(report) {
985
+ const lines = [formatConfigSectionTitle(`Provider connection checks for ${report.scope}`)];
986
+ for (const check of report.checks) {
987
+ const label = check.provider[0].toUpperCase() + check.provider.slice(1);
988
+ const status = check.ready ? colorize("ready", "32") : check.skipped ? colorize("skipped", "33") : colorize("failed", "31");
989
+ lines.push(`${label}: ${status} - ${check.detail}`);
990
+ }
991
+ return lines.join("\n");
992
+ }
993
+ function writeProviderConnectionReport(write, report) {
994
+ write(formatTreeseedProviderConnectionReport(report));
995
+ }
996
+ function listGitHubNames(command, repository, tenantRoot) {
997
+ const result = runGh([command, "list", "--repo", repository, "--json", "name"], { cwd: tenantRoot });
998
+ return new Set(JSON.parse(result.stdout || "[]").map((entry) => entry?.name).filter(Boolean));
999
+ }
1000
+ function syncTreeseedGitHubEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1001
+ const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1002
+ if (!repository) {
1003
+ throw new Error("Unable to determine the GitHub repository from the origin remote.");
1004
+ }
1005
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1006
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1007
+ const relevant = registry.entries.filter((entry) => entry.scopes.includes(scope));
1008
+ const secretNames = listGitHubNames("secret", repository, tenantRoot);
1009
+ const variableNames = listGitHubNames("variable", repository, tenantRoot);
1010
+ const synced = {
1011
+ secrets: [],
1012
+ variables: []
1013
+ };
1014
+ for (const entry of relevant) {
1015
+ const value = values[entry.id];
1016
+ if (!value) {
1017
+ continue;
1018
+ }
1019
+ if (entry.targets.includes("github-secret")) {
1020
+ runGh(["secret", "set", entry.id, "--repo", repository, "--body", value], { cwd: tenantRoot, dryRun });
1021
+ synced.secrets.push({ name: entry.id, existed: secretNames.has(entry.id) });
1022
+ }
1023
+ if (entry.targets.includes("github-variable")) {
1024
+ runGh(["variable", "set", entry.id, "--repo", repository, "--body", value], { cwd: tenantRoot, dryRun });
1025
+ synced.variables.push({ name: entry.id, existed: variableNames.has(entry.id) });
1026
+ }
1027
+ }
1028
+ return {
1029
+ repository,
1030
+ scope,
1031
+ ...synced
1032
+ };
1033
+ }
1034
+ function syncTreeseedCloudflareEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1035
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1036
+ const target = createPersistentDeployTarget(scope);
1037
+ for (const [key, value] of Object.entries(values)) {
1038
+ if (typeof value === "string" && value.length > 0) {
1039
+ process.env[key] = value;
1040
+ }
1041
+ }
1042
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
1043
+ const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
1044
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1045
+ const cloudflareVars = registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("cloudflare-var")).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
1046
+ return {
1047
+ scope,
1048
+ target,
1049
+ wranglerPath,
1050
+ secrets: syncedSecrets,
1051
+ varsManagedByWranglerConfig: cloudflareVars
1052
+ };
1053
+ }
1054
+ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1055
+ const config = syncManagedServiceSettingsFromDeployConfig(tenantRoot);
1056
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
1057
+ const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1058
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1059
+ const railwaySecretNames = registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("railway-secret")).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
1060
+ const services = ["api", "agents", "manager", "worker", "workdayStart", "workdayReport"].map((serviceKey) => {
1061
+ const service = deployConfig.services?.[serviceKey];
1062
+ if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
1063
+ return null;
1064
+ }
1065
+ const environment = service.environments?.[scope];
1066
+ const fallbackServiceName = serviceKey === "api" ? config.settings.services.railway.apiServiceName : serviceKey === "agents" ? config.settings.services.railway.agentsServiceName : "";
1067
+ const defaultRootDir = serviceKey === "api" ? "packages/api" : "packages/agent";
1068
+ return {
1069
+ service: serviceKey,
1070
+ projectName: service.railway?.projectName ?? config.settings.services.railway.projectName,
1071
+ serviceName: service.railway?.serviceName ?? fallbackServiceName,
1072
+ serviceId: service.railway?.serviceId ?? "",
1073
+ rootDir: resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir),
1074
+ baseUrl: environment?.baseUrl ?? service.publicBaseUrl ?? "(unset)",
1075
+ environmentName: environment?.railwayEnvironment ?? scope,
1076
+ secrets: railwaySecretNames,
1077
+ dryRun
1078
+ };
1079
+ }).filter(Boolean);
1080
+ for (const service of services) {
1081
+ for (const key of service.secrets) {
1082
+ runRailway(
1083
+ ["variable", "set", "--service", service.serviceName || service.serviceId, "--environment", service.environmentName, "--stdin", "--skip-deploys", key],
1084
+ { cwd: service.rootDir, dryRun, input: values[key] }
1085
+ );
1086
+ }
1087
+ }
1088
+ return {
1089
+ scope,
1090
+ services
1091
+ };
1092
+ }
1093
+ function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
1094
+ const normalizedScope = scope === "prod" ? "prod" : scope;
1095
+ const target = createPersistentDeployTarget(normalizedScope);
1096
+ const summary = provisionCloudflareResources(tenantRoot, { dryRun, target });
1097
+ ensureGeneratedWranglerConfig(tenantRoot, { target });
1098
+ const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
1099
+ if (!dryRun) {
1100
+ markDeploymentInitialized(tenantRoot, { target });
1101
+ }
1102
+ return {
1103
+ scope: normalizedScope,
1104
+ target,
1105
+ summary,
1106
+ secrets: syncedSecrets
1107
+ };
1108
+ }
1109
+ function colorize(value, code) {
1110
+ return `\x1B[${code}m${value}\x1B[0m`;
1111
+ }
1112
+ function formatConfigSectionTitle(label) {
1113
+ return colorize(`
1114
+ == ${label}`, "1;36");
1115
+ }
1116
+ function formatConfigFieldPrompt(entry, currentValue) {
1117
+ const current = entry.sensitivity === "secret" ? maskValue(currentValue) : currentValue ?? "(unset)";
1118
+ return [
1119
+ colorize(`
1120
+ -- ${entry.label}`, "1;37"),
1121
+ colorize(` ${entry.id}`, "36"),
1122
+ ` ${entry.description}`,
1123
+ ` How to get it: ${entry.howToGet}`,
1124
+ ` Used for: ${entry.purposes.join(", ")}`,
1125
+ ` Targets: ${entry.targets.join(", ")}`,
1126
+ ` Current: ${current}`,
1127
+ colorize(' Enter value, press Enter to keep current/default, or "-" to clear', "90")
1128
+ ].join("\n");
1129
+ }
1130
+ function hasConfigValue(values, key) {
1131
+ return typeof values[key] === "string" && values[key].trim().length > 0;
1132
+ }
1133
+ function createConfigAuthStatus(values) {
1134
+ const ghReady = hasConfigValue(values, "GH_TOKEN");
1135
+ return {
1136
+ gh: {
1137
+ authenticated: ghReady
1138
+ },
1139
+ wrangler: {
1140
+ authenticated: hasConfigValue(values, "CLOUDFLARE_API_TOKEN")
1141
+ },
1142
+ railway: {
1143
+ authenticated: hasConfigValue(values, "RAILWAY_API_TOKEN")
1144
+ },
1145
+ copilot: {
1146
+ configured: ghReady
1147
+ }
1148
+ };
1149
+ }
1150
+ async function renderInkConfigFrame({ scope, tenantName, tenantSlug, group, entry, currentValue, authStatus }) {
1151
+ if (!process.stdout.isTTY) {
1152
+ return false;
1153
+ }
1154
+ try {
1155
+ const [{ render, Box, Text }, React] = await Promise.all([
1156
+ import("ink"),
1157
+ import("react")
1158
+ ]);
1159
+ const h = React.createElement;
1160
+ const status = (ready) => ready ? "ready" : "missing";
1161
+ const current = entry.sensitivity === "secret" ? maskValue(currentValue) : currentValue ?? "(unset)";
1162
+ const permissionDetails = {
1163
+ GH_TOKEN: {
1164
+ title: "Required GitHub permissions",
1165
+ items: [
1166
+ "Scoped to TreeSeed repository",
1167
+ "Contents: read/write",
1168
+ "Environments: read/write",
1169
+ "Secrets and variables: read/write",
1170
+ "Actions and workflows: read/write",
1171
+ "Pull requests: read/write",
1172
+ "Issues: read/write"
1173
+ ]
1174
+ },
1175
+ CLOUDFLARE_API_TOKEN: {
1176
+ title: "Required Cloudflare permissions",
1177
+ items: [
1178
+ "Scoped to domain and account",
1179
+ "Account Cloudflare Pages: edit",
1180
+ "Account Workers Scripts: edit",
1181
+ "Account Workers KV Storage: edit",
1182
+ "Account D1: edit",
1183
+ "Account Queues: edit",
1184
+ "Zone DNS: edit"
1185
+ ]
1186
+ }
1187
+ }[entry.id];
1188
+ const frame = h(
1189
+ Box,
1190
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 },
1191
+ h(Text, { color: "cyan", bold: true }, `Treeseed Config - ${scope}`),
1192
+ h(Text, null, `${tenantName} (${tenantSlug})`),
1193
+ authStatus ? h(
1194
+ Text,
1195
+ null,
1196
+ `GitHub ${status(authStatus.gh?.authenticated)} - Cloudflare ${status(authStatus.wrangler?.authenticated)} - Railway ${status(authStatus.railway?.authenticated)}`
1197
+ ) : null,
1198
+ h(Text, { color: "yellow" }, `[${group}] ${entry.label}`),
1199
+ h(Text, { color: "gray" }, entry.id),
1200
+ h(Text, null, entry.description),
1201
+ h(Text, null, `How: ${entry.howToGet}`),
1202
+ permissionDetails ? h(
1203
+ Box,
1204
+ { flexDirection: "column", marginTop: 1 },
1205
+ h(Text, { color: "green", bold: true }, permissionDetails.title),
1206
+ ...permissionDetails.items.map((permission) => h(Text, { key: permission }, `- ${permission}`))
1207
+ ) : null,
1208
+ h(Text, null, `Used for: ${entry.purposes.join(", ")}`),
1209
+ h(Text, null, `Targets: ${entry.targets.join(", ")}`),
1210
+ h(Text, { color: currentValue ? "green" : "red" }, `Current: ${current}`)
1211
+ );
1212
+ const instance = render(frame, { exitOnCtrlC: false });
1213
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 20));
1214
+ instance.unmount();
1215
+ return true;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+ async function runTreeseedConfigWizard({
1221
+ tenantRoot,
1222
+ scopes = ["local", "staging", "prod"],
1223
+ sync = "all",
1224
+ prompt,
1225
+ authStatus,
1226
+ write = console.log,
1227
+ env = process.env,
1228
+ useInk = false,
1229
+ printEnv = false,
1230
+ revealSecrets = false,
1231
+ checkConnections = true
1232
+ }) {
1233
+ ensureTreeseedGitignoreEntries(tenantRoot);
1234
+ const registry = collectTreeseedEnvironmentContext(tenantRoot);
1235
+ const groups = ["auth", "local-development", "forms", "smtp", "cloudflare"];
1236
+ const summary = {
1237
+ scopes,
1238
+ updated: [],
1239
+ synced: {},
1240
+ initialized: [],
1241
+ connectionChecks: []
1242
+ };
1243
+ for (const scope of scopes) {
1244
+ const existingValues = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
1245
+ const configAuthStatus = createConfigAuthStatus(existingValues);
1246
+ const suggested = getTreeseedEnvironmentSuggestedValues({
1247
+ scope,
1248
+ deployConfig: registry.context.deployConfig,
1249
+ tenantConfig: registry.context.tenantConfig,
1250
+ plugins: registry.context.plugins
1251
+ });
1252
+ write(formatConfigSectionTitle(`Treeseed configuration for ${scope}`));
1253
+ write(`Tenant: ${registry.context.deployConfig.name} (${registry.context.deployConfig.slug})`);
1254
+ if (authStatus) {
1255
+ write(`GitHub token: ${configAuthStatus.gh.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
1256
+ write(`Cloudflare token: ${configAuthStatus.wrangler.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
1257
+ write(`Railway token: ${configAuthStatus.railway.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
1258
+ }
1259
+ for (const group of groups) {
1260
+ const groupEntries = registry.entries.filter(
1261
+ (entry) => entry.group === group && entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config"))
1262
+ );
1263
+ if (groupEntries.length === 0) {
1264
+ continue;
1265
+ }
1266
+ write(formatConfigSectionTitle(group));
1267
+ for (const entry of groupEntries) {
1268
+ const currentValue = existingValues[entry.id];
1269
+ const suggestedValue = suggested[entry.id];
1270
+ const displayValue = currentValue ?? suggestedValue ?? "";
1271
+ const entryAuthStatus = createConfigAuthStatus(existingValues);
1272
+ const renderedInk = useInk && await renderInkConfigFrame({
1273
+ scope,
1274
+ tenantName: registry.context.deployConfig.name,
1275
+ tenantSlug: registry.context.deployConfig.slug,
1276
+ group,
1277
+ entry,
1278
+ currentValue,
1279
+ authStatus: entryAuthStatus
1280
+ });
1281
+ if (!renderedInk) {
1282
+ write(formatConfigFieldPrompt(entry, currentValue));
1283
+ }
1284
+ const answer = (await prompt(
1285
+ `${entry.id}${displayValue ? ` [${entry.sensitivity === "secret" ? "keep current" : displayValue}]` : ""}: `
1286
+ )).trim();
1287
+ if (answer === "" && displayValue) {
1288
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, displayValue);
1289
+ existingValues[entry.id] = displayValue;
1290
+ summary.updated.push({ scope, id: entry.id, reused: true });
1291
+ continue;
1292
+ }
1293
+ if (answer === "" && !displayValue) {
1294
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, "");
1295
+ existingValues[entry.id] = "";
1296
+ continue;
1297
+ }
1298
+ if (answer === "-") {
1299
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, "");
1300
+ existingValues[entry.id] = "";
1301
+ summary.updated.push({ scope, id: entry.id, cleared: true });
1302
+ continue;
1303
+ }
1304
+ setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, answer);
1305
+ existingValues[entry.id] = answer;
1306
+ summary.updated.push({ scope, id: entry.id, reused: false });
1307
+ }
1308
+ }
1309
+ const validation = validateTreeseedEnvironmentValues({
1310
+ values: resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
1311
+ scope,
1312
+ purpose: "config",
1313
+ deployConfig: registry.context.deployConfig,
1314
+ tenantConfig: registry.context.tenantConfig,
1315
+ plugins: registry.context.plugins
1316
+ });
1317
+ if (!validation.ok) {
1318
+ const details = [...validation.missing, ...validation.invalid].map((problem) => `- ${problem.message}`).join("\n");
1319
+ throw new Error(`Treeseed config validation failed for ${scope}:
1320
+ ${details}`);
1321
+ }
1322
+ if (printEnv) {
1323
+ write(formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env, revealSecrets }));
1324
+ }
1325
+ if (checkConnections) {
1326
+ const connectionReport = checkTreeseedProviderConnections({ tenantRoot, scope, env });
1327
+ summary.connectionChecks.push(connectionReport);
1328
+ writeProviderConnectionReport(write, connectionReport);
1329
+ }
1330
+ }
1331
+ writeTreeseedLocalEnvironmentFiles(tenantRoot);
1332
+ syncManagedServiceSettingsFromDeployConfig(tenantRoot);
1333
+ for (const scope of scopes) {
1334
+ if (scope === "local") {
1335
+ continue;
1336
+ }
1337
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
1338
+ const initialized = initializeTreeseedPersistentEnvironment({ tenantRoot, scope });
1339
+ if (write) {
1340
+ writeDeploySummary(write, initialized.summary);
1341
+ }
1342
+ summary.initialized.push({
1343
+ scope,
1344
+ secrets: initialized.secrets.length,
1345
+ target: initialized.summary.target
1346
+ });
1347
+ markManagedServicesInitialized(tenantRoot, { scope });
1348
+ }
1349
+ if (sync === "github" || sync === "all") {
1350
+ summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1351
+ }
1352
+ if (sync === "cloudflare" || sync === "all") {
1353
+ summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1354
+ }
1355
+ if (sync === "railway" || sync === "all") {
1356
+ summary.synced.railway = syncTreeseedRailwayEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1357
+ }
1358
+ return summary;
1359
+ }
1360
+ export {
1361
+ DEFAULT_TEMPLATE_CATALOG_URL,
1362
+ DEFAULT_TREESEED_API_BASE_URL,
1363
+ TREESEED_API_BASE_URL_ENV,
1364
+ TREESEED_TEMPLATE_CATALOG_URL_ENV,
1365
+ applyTreeseedEnvironmentToProcess,
1366
+ assertTreeseedCommandEnvironment,
1367
+ checkTreeseedProviderConnections,
1368
+ clearTreeseedRemoteSession,
1369
+ collectTreeseedConfigSeedValues,
1370
+ collectTreeseedEnvironmentContext,
1371
+ createDefaultTreeseedMachineConfig,
1372
+ ensureTreeseedActVerificationTooling,
1373
+ ensureTreeseedGitignoreEntries,
1374
+ formatTreeseedConfigEnvironmentReport,
1375
+ formatTreeseedProviderConnectionReport,
1376
+ getTreeseedMachineConfigPaths,
1377
+ getTreeseedRemoteAuthPaths,
1378
+ initializeTreeseedPersistentEnvironment,
1379
+ loadTreeseedMachineConfig,
1380
+ loadTreeseedRemoteAuthState,
1381
+ resolveTreeseedMachineEnvironmentValues,
1382
+ resolveTreeseedRemoteConfig,
1383
+ resolveTreeseedRemoteSession,
1384
+ resolveTreeseedTemplateCatalogCachePath,
1385
+ resolveTreeseedTemplateCatalogEndpoint,
1386
+ rotateTreeseedMachineKey,
1387
+ runTreeseedConfigWizard,
1388
+ setTreeseedMachineEnvironmentValue,
1389
+ setTreeseedRemoteSession,
1390
+ syncTreeseedCloudflareEnvironment,
1391
+ syncTreeseedGitHubEnvironment,
1392
+ syncTreeseedRailwayEnvironment,
1393
+ validateTreeseedCommandEnvironment,
1394
+ writeTreeseedLocalEnvironmentFiles,
1395
+ writeTreeseedMachineConfig,
1396
+ writeTreeseedRemoteAuthState
1397
+ };