@treeseed/sdk 0.4.13 → 0.5.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 (83) hide show
  1. package/dist/control-plane-client.d.ts +60 -1
  2. package/dist/control-plane-client.js +59 -0
  3. package/dist/control-plane.d.ts +1 -1
  4. package/dist/control-plane.js +11 -4
  5. package/dist/d1-store.d.ts +58 -0
  6. package/dist/d1-store.js +64 -0
  7. package/dist/dispatch.js +6 -0
  8. package/dist/graph/schema.js +4 -0
  9. package/dist/index.d.ts +5 -1
  10. package/dist/index.js +32 -0
  11. package/dist/knowledge-coop.d.ts +223 -0
  12. package/dist/knowledge-coop.js +82 -0
  13. package/dist/model-registry.js +79 -0
  14. package/dist/operations/providers/default.js +126 -7
  15. package/dist/operations/services/config-runtime.d.ts +102 -24
  16. package/dist/operations/services/config-runtime.js +896 -160
  17. package/dist/operations/services/deploy.d.ts +223 -15
  18. package/dist/operations/services/deploy.js +626 -55
  19. package/dist/operations/services/github-automation.d.ts +60 -0
  20. package/dist/operations/services/github-automation.js +138 -0
  21. package/dist/operations/services/key-agent.d.ts +118 -0
  22. package/dist/operations/services/key-agent.js +476 -0
  23. package/dist/operations/services/knowledge-coop-launch.d.ts +90 -0
  24. package/dist/operations/services/knowledge-coop-launch.js +753 -0
  25. package/dist/operations/services/knowledge-coop-packaging.d.ts +59 -0
  26. package/dist/operations/services/knowledge-coop-packaging.js +234 -0
  27. package/dist/operations/services/local-dev.d.ts +0 -1
  28. package/dist/operations/services/local-dev.js +1 -14
  29. package/dist/operations/services/project-platform.d.ts +42 -182
  30. package/dist/operations/services/project-platform.js +162 -59
  31. package/dist/operations/services/railway-deploy.d.ts +1 -0
  32. package/dist/operations/services/railway-deploy.js +31 -13
  33. package/dist/operations/services/runtime-tools.d.ts +52 -5
  34. package/dist/operations/services/runtime-tools.js +186 -26
  35. package/dist/operations/services/watch-dev.js +2 -4
  36. package/dist/operations/services/workspace-preflight.d.ts +4 -4
  37. package/dist/operations/services/workspace-preflight.js +22 -20
  38. package/dist/operations-registry.js +7 -2
  39. package/dist/platform/contracts.d.ts +39 -3
  40. package/dist/platform/deploy-config.d.ts +12 -1
  41. package/dist/platform/deploy-config.js +214 -15
  42. package/dist/platform/deploy-runtime.d.ts +1 -0
  43. package/dist/platform/deploy-runtime.js +10 -2
  44. package/dist/platform/env.yaml +93 -61
  45. package/dist/platform/environment.d.ts +13 -2
  46. package/dist/platform/environment.js +90 -20
  47. package/dist/platform/plugins/constants.d.ts +1 -0
  48. package/dist/platform/plugins/constants.js +7 -6
  49. package/dist/platform/tenant/runtime-config.js +8 -1
  50. package/dist/platform/tenant-config.js +4 -0
  51. package/dist/platform/utils/site-config-schema.js +18 -0
  52. package/dist/plugin-default.js +2 -2
  53. package/dist/scripts/key-agent.js +165 -0
  54. package/dist/scripts/tenant-build.js +4 -1
  55. package/dist/scripts/tenant-check.js +4 -1
  56. package/dist/scripts/tenant-deploy.js +43 -4
  57. package/dist/scripts/tenant-dev.js +0 -1
  58. package/dist/sdk-types.d.ts +2 -2
  59. package/dist/sdk-types.js +2 -0
  60. package/dist/sdk.d.ts +13 -0
  61. package/dist/sdk.js +40 -0
  62. package/dist/stores/knowledge-coop-store.d.ts +56 -0
  63. package/dist/stores/knowledge-coop-store.js +482 -0
  64. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +6 -2
  65. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +4 -0
  66. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +25 -0
  67. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +22 -0
  68. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +11 -0
  69. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +17 -0
  70. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +17 -10
  71. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +69 -7
  72. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +1 -0
  73. package/dist/verification.js +90 -2
  74. package/dist/workflow/operations.d.ts +98 -0
  75. package/dist/workflow/operations.js +229 -7
  76. package/dist/workflow-state.d.ts +54 -2
  77. package/dist/workflow-state.js +170 -24
  78. package/dist/workflow-support.d.ts +1 -1
  79. package/dist/workflow-support.js +32 -2
  80. package/dist/workflow.d.ts +29 -0
  81. package/package.json +1 -1
  82. package/templates/github/deploy.workflow.yml +11 -1
  83. package/dist/scripts/sync-dev-vars.js +0 -6
@@ -1,8 +1,8 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
- import { spawnSync } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
6
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
7
  import {
8
8
  getTreeseedEnvironmentSuggestedValues,
@@ -16,7 +16,6 @@ import {
16
16
  createPersistentDeployTarget,
17
17
  ensureGeneratedWranglerConfig,
18
18
  loadDeployState,
19
- markManagedServicesInitialized,
20
19
  markDeploymentInitialized,
21
20
  provisionCloudflareResources,
22
21
  syncCloudflareSecrets,
@@ -24,18 +23,38 @@ import {
24
23
  } from "./deploy.js";
25
24
  import { maybeResolveGitHubRepositorySlug } from "./github-automation.js";
26
25
  import { validateRailwayDeployPrerequisites } from "./railway-deploy.js";
27
- import { loadCliDeployConfig, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
26
+ import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
27
+ import {
28
+ assertTreeseedKeyAgentResponse,
29
+ getTreeseedKeyAgentPaths,
30
+ readWrappedMachineKeyFile,
31
+ replaceWrappedMachineKey,
32
+ rotateWrappedMachineKeyPassphrase,
33
+ TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS,
34
+ TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
35
+ TreeseedKeyAgentError,
36
+ unwrapMachineKey
37
+ } from "./key-agent.js";
38
+ import { TREESEED_MACHINE_KEY_PASSPHRASE_ENV as TREESEED_MACHINE_KEY_PASSPHRASE_ENV2, TreeseedKeyAgentError as TreeseedKeyAgentError2 } from "./key-agent.js";
28
39
  const MACHINE_CONFIG_RELATIVE_PATH = ".treeseed/config/machine.yaml";
40
+ const LEGACY_ENVIRONMENT_ALIASES = {
41
+ RAILWAY_API_KEY: "RAILWAY_API_TOKEN"
42
+ };
29
43
  const MACHINE_KEY_HOME_RELATIVE_PATH = ".treeseed/config/machine.key";
30
44
  const LEGACY_MACHINE_KEY_RELATIVE_PATH = ".treeseed/config/machine.key";
31
45
  const REMOTE_AUTH_RELATIVE_PATH = ".treeseed/config/remote-auth.json";
32
46
  const TEMPLATE_CATALOG_CACHE_RELATIVE_PATH = "treeseed/cache/template-catalog.json";
33
47
  const TENANT_ENVIRONMENT_OVERLAY_PATH = "src/env.yaml";
34
48
  const CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER = "replace-with-cloudflare-account-id";
49
+ const TREESEED_KEY_AGENT_AUTOPROMPT_ENV = "TREESEED_KEY_AGENT_AUTOPROMPT";
35
50
  const DEFAULT_TREESEED_API_BASE_URL = "https://api.treeseed.ai";
36
51
  const DEFAULT_TEMPLATE_CATALOG_URL = "https://api.treeseed.ai/search/templates";
37
52
  const TREESEED_TEMPLATE_CATALOG_URL_ENV = "TREESEED_TEMPLATE_CATALOG_URL";
38
53
  const TREESEED_API_BASE_URL_ENV = "TREESEED_API_BASE_URL";
54
+ const CLI_CHECK_TIMEOUT_MS = 5e3;
55
+ const DEPRECATED_LOCAL_ENV_FILES = [".env.local", ".dev.vars"];
56
+ const warnedDeprecatedLocalEnvRoots = /* @__PURE__ */ new Set();
57
+ const inlineTreeseedSecretSessions = /* @__PURE__ */ new Map();
39
58
  function createDefaultRemoteHost() {
40
59
  return {
41
60
  id: "official",
@@ -71,9 +90,7 @@ function createDefaultServiceSettings() {
71
90
  projectId: "",
72
91
  projectName: "",
73
92
  apiServiceId: "",
74
- apiServiceName: "",
75
- agentsServiceId: "",
76
- agentsServiceName: ""
93
+ apiServiceName: ""
77
94
  }
78
95
  };
79
96
  }
@@ -85,30 +102,26 @@ function normalizeServiceSettings(value) {
85
102
  projectId: typeof railway.projectId === "string" ? railway.projectId : "",
86
103
  projectName: typeof railway.projectName === "string" ? railway.projectName : "",
87
104
  apiServiceId: typeof railway.apiServiceId === "string" ? railway.apiServiceId : "",
88
- apiServiceName: typeof railway.apiServiceName === "string" ? railway.apiServiceName : "",
89
- agentsServiceId: typeof railway.agentsServiceId === "string" ? railway.agentsServiceId : "",
90
- agentsServiceName: typeof railway.agentsServiceName === "string" ? railway.agentsServiceName : ""
105
+ apiServiceName: typeof railway.apiServiceName === "string" ? railway.apiServiceName : ""
91
106
  }
92
107
  };
93
108
  }
94
109
  function ensureParent(filePath) {
95
110
  mkdirSync(dirname(filePath), { recursive: true });
96
111
  }
97
- function parseEnvFile(contents) {
98
- return contents.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).reduce((acc, line) => {
99
- const separatorIndex = line.indexOf("=");
100
- if (separatorIndex === -1) {
101
- return acc;
102
- }
103
- acc[line.slice(0, separatorIndex).trim()] = line.slice(separatorIndex + 1);
104
- return acc;
105
- }, {});
112
+ function listDeprecatedTreeseedLocalEnvFiles(tenantRoot) {
113
+ return DEPRECATED_LOCAL_ENV_FILES.map((fileName) => resolve(tenantRoot, fileName)).filter((filePath) => existsSync(filePath));
106
114
  }
107
- function readEnvFileIfPresent(filePath) {
108
- if (!existsSync(filePath)) {
109
- return {};
115
+ function warnDeprecatedTreeseedLocalEnvFiles(tenantRoot, write = (line) => console.warn(line)) {
116
+ const existing = listDeprecatedTreeseedLocalEnvFiles(tenantRoot);
117
+ if (existing.length === 0 || warnedDeprecatedLocalEnvRoots.has(tenantRoot)) {
118
+ return existing;
110
119
  }
111
- return parseEnvFile(readFileSync(filePath, "utf8"));
120
+ warnedDeprecatedLocalEnvRoots.add(tenantRoot);
121
+ write(
122
+ `Treeseed ignores deprecated local env files: ${existing.map((filePath) => filePath.replace(`${tenantRoot}/`, "")).join(", ")}. Delete them and rely on .treeseed/config/machine.yaml plus Treeseed-launched commands.`
123
+ );
124
+ return existing;
112
125
  }
113
126
  function maskValue(value) {
114
127
  if (!value) {
@@ -133,14 +146,12 @@ function syncManagedServiceSettingsFromDeployConfig(tenantRoot) {
133
146
  const config = loadTreeseedMachineConfig(tenantRoot);
134
147
  const deployConfig = loadTenantDeployConfig(tenantRoot);
135
148
  const railway = config.settings.services.railway;
136
- railway.projectId = deployConfig.services?.api?.railway?.projectId ?? deployConfig.services?.agents?.railway?.projectId ?? railway.projectId;
137
- railway.projectName = deployConfig.services?.api?.railway?.projectName ?? deployConfig.services?.agents?.railway?.projectName ?? railway.projectName;
149
+ railway.projectId = deployConfig.services?.api?.railway?.projectId ?? railway.projectId;
150
+ railway.projectName = deployConfig.services?.api?.railway?.projectName ?? railway.projectName;
138
151
  railway.apiServiceId = deployConfig.services?.api?.railway?.serviceId ?? railway.apiServiceId;
139
152
  railway.apiServiceName = deployConfig.services?.api?.railway?.serviceName ?? railway.apiServiceName;
140
- railway.agentsServiceId = deployConfig.services?.agents?.railway?.serviceId ?? railway.agentsServiceId;
141
- railway.agentsServiceName = deployConfig.services?.agents?.railway?.serviceName ?? railway.agentsServiceName;
142
153
  const remote = normalizeRemoteSettings(config.settings.remote);
143
- const defaultHostBaseUrl = deployConfig.services?.api?.environments?.prod?.baseUrl ?? deployConfig.services?.api?.publicBaseUrl ?? remote.hosts[0]?.baseUrl ?? DEFAULT_TREESEED_API_BASE_URL;
154
+ const defaultHostBaseUrl = process.env[TREESEED_API_BASE_URL_ENV] ?? deployConfig.services?.api?.environments?.prod?.baseUrl ?? deployConfig.services?.api?.publicBaseUrl ?? remote.hosts[0]?.baseUrl ?? DEFAULT_TREESEED_API_BASE_URL;
144
155
  const officialHost = remote.hosts.find((entry) => entry.id === "official");
145
156
  if (officialHost) {
146
157
  officialHost.baseUrl = defaultHostBaseUrl.replace(/\/$/u, "");
@@ -190,6 +201,448 @@ function getTreeseedMachineConfigPaths(tenantRoot) {
190
201
  legacyKeyPath: resolve(tenantRoot, LEGACY_MACHINE_KEY_RELATIVE_PATH)
191
202
  };
192
203
  }
204
+ function keyAgentScriptPath() {
205
+ return packageScriptPath("key-agent.ts");
206
+ }
207
+ function keyAgentRunTsPath() {
208
+ return packageScriptPath("run-ts.mjs");
209
+ }
210
+ function keyAgentScriptCwd() {
211
+ return dirname(dirname(keyAgentRunTsPath()));
212
+ }
213
+ function sleepMs(milliseconds) {
214
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
215
+ }
216
+ function shellQuote(value) {
217
+ return `'${value.replace(/'/gu, `'\\''`)}'`;
218
+ }
219
+ function keyAgentAutoPromptEnabled() {
220
+ const value = String(process.env[TREESEED_KEY_AGENT_AUTOPROMPT_ENV] ?? "").trim().toLowerCase();
221
+ if (value === "0" || value === "false" || value === "off") {
222
+ return false;
223
+ }
224
+ return process.stdin.isTTY && process.stdout.isTTY;
225
+ }
226
+ function useInlineKeyAgentTransport() {
227
+ return process.env.VITEST === "true" || process.env.TREESEED_KEY_AGENT_TRANSPORT === "inline";
228
+ }
229
+ function withTreeseedKeyAgentAutopromptDisabled(action) {
230
+ const previous = process.env[TREESEED_KEY_AGENT_AUTOPROMPT_ENV];
231
+ process.env[TREESEED_KEY_AGENT_AUTOPROMPT_ENV] = "0";
232
+ try {
233
+ return action();
234
+ } finally {
235
+ if (previous === void 0) {
236
+ delete process.env[TREESEED_KEY_AGENT_AUTOPROMPT_ENV];
237
+ } else {
238
+ process.env[TREESEED_KEY_AGENT_AUTOPROMPT_ENV] = previous;
239
+ }
240
+ }
241
+ }
242
+ function startTreeseedKeyAgentDaemon(tenantRoot) {
243
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
244
+ const { socketPath } = getTreeseedKeyAgentPaths();
245
+ const command = [
246
+ shellQuote(process.execPath),
247
+ shellQuote(keyAgentRunTsPath()),
248
+ shellQuote(keyAgentScriptPath()),
249
+ "serve",
250
+ "--key-path",
251
+ shellQuote(keyPath),
252
+ "--socket-path",
253
+ shellQuote(socketPath),
254
+ "--idle-timeout-ms",
255
+ shellQuote(String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS)),
256
+ ">/dev/null",
257
+ "2>/dev/null",
258
+ "&"
259
+ ].join(" ");
260
+ const child = spawn("bash", ["-lc", command], {
261
+ cwd: keyAgentScriptCwd(),
262
+ stdio: "ignore"
263
+ });
264
+ child.unref();
265
+ }
266
+ function runTreeseedKeyAgentCommand(args, options = {}) {
267
+ const result = spawnSync(process.execPath, [
268
+ keyAgentRunTsPath(),
269
+ keyAgentScriptPath(),
270
+ ...args
271
+ ], {
272
+ cwd: keyAgentScriptCwd(),
273
+ encoding: "utf8",
274
+ env: {
275
+ ...process.env,
276
+ ...options.env ?? {}
277
+ },
278
+ stdio: options.input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
279
+ input: options.input
280
+ });
281
+ if (result.status !== 0 && (!result.stdout || result.stdout.trim().length === 0)) {
282
+ return {
283
+ ok: false,
284
+ code: "daemon_unavailable",
285
+ message: result.stderr?.trim() || "Treeseed key-agent command failed."
286
+ };
287
+ }
288
+ try {
289
+ return JSON.parse(result.stdout.trim() || "{}");
290
+ } catch {
291
+ throw new TreeseedKeyAgentError(
292
+ "daemon_unavailable",
293
+ result.stderr?.trim() || "Treeseed key-agent command returned an invalid response."
294
+ );
295
+ }
296
+ }
297
+ function requestTreeseedKeyAgent(tenantRoot, payload, { ensureRunning = false, env } = {}) {
298
+ const invoke = () => runTreeseedKeyAgentCommand([
299
+ "request",
300
+ JSON.stringify(payload)
301
+ ], { env });
302
+ let response = invoke();
303
+ if (response.code !== "daemon_unavailable" || !ensureRunning) {
304
+ return response;
305
+ }
306
+ startTreeseedKeyAgentDaemon(tenantRoot);
307
+ for (let attempt = 0; attempt < 20; attempt += 1) {
308
+ response = invoke();
309
+ if (response.code !== "daemon_unavailable") {
310
+ return response;
311
+ }
312
+ sleepMs(25);
313
+ }
314
+ return response;
315
+ }
316
+ function inspectTreeseedKeyAgentStatus(tenantRoot) {
317
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
318
+ const { socketPath } = getTreeseedKeyAgentPaths();
319
+ const wrapped = readWrappedMachineKeyFile(keyPath);
320
+ if (useInlineKeyAgentTransport()) {
321
+ const session = inlineTreeseedSecretSessions.get(keyPath) ?? { machineKey: null, lastTouchedAt: 0, idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS };
322
+ const idleRemainingMs = session.machineKey ? Math.max(0, session.idleTimeoutMs - (Date.now() - session.lastTouchedAt)) : 0;
323
+ if (idleRemainingMs === 0) {
324
+ session.machineKey = null;
325
+ }
326
+ inlineTreeseedSecretSessions.set(keyPath, session);
327
+ return {
328
+ running: true,
329
+ unlocked: Boolean(session.machineKey) && idleRemainingMs > 0,
330
+ wrappedKeyPresent: wrapped.exists && Boolean(wrapped.wrapped),
331
+ migrationRequired: wrapped.migrationRequired,
332
+ keyPath,
333
+ socketPath,
334
+ idleTimeoutMs: session.idleTimeoutMs,
335
+ idleRemainingMs
336
+ };
337
+ }
338
+ const response = requestTreeseedKeyAgent(tenantRoot, {
339
+ command: "status",
340
+ keyPath,
341
+ socketPath,
342
+ idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS
343
+ });
344
+ if (response.ok && response.status) {
345
+ return response.status;
346
+ }
347
+ return {
348
+ running: false,
349
+ unlocked: false,
350
+ wrappedKeyPresent: wrapped.exists && Boolean(wrapped.wrapped),
351
+ migrationRequired: wrapped.migrationRequired,
352
+ keyPath,
353
+ socketPath,
354
+ idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS,
355
+ idleRemainingMs: 0
356
+ };
357
+ }
358
+ function unlockTreeseedSecretSessionInteractive(tenantRoot) {
359
+ if (useInlineKeyAgentTransport()) {
360
+ throw new TreeseedKeyAgentError("interactive_required", "Inline test transport does not support interactive unlock.");
361
+ }
362
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
363
+ const { socketPath } = getTreeseedKeyAgentPaths();
364
+ startTreeseedKeyAgentDaemon(tenantRoot);
365
+ let response = { ok: false, code: "daemon_unavailable", message: "Treeseed key agent is unavailable." };
366
+ for (let attempt = 0; attempt < 20; attempt += 1) {
367
+ response = runTreeseedKeyAgentCommand([
368
+ "unlock-interactive",
369
+ "--key-path",
370
+ keyPath,
371
+ "--socket-path",
372
+ socketPath,
373
+ "--idle-timeout-ms",
374
+ String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
375
+ "--allow-migration",
376
+ "--create-if-missing"
377
+ ]);
378
+ if (response.code !== "daemon_unavailable") {
379
+ break;
380
+ }
381
+ sleepMs(25);
382
+ }
383
+ assertTreeseedKeyAgentResponse(response, "Unable to unlock the Treeseed secret session.");
384
+ return response.status;
385
+ }
386
+ function unlockTreeseedSecretSessionFromEnv(tenantRoot, options = {}) {
387
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
388
+ const { socketPath } = getTreeseedKeyAgentPaths();
389
+ if (useInlineKeyAgentTransport()) {
390
+ const passphrase = String(process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] ?? "").trim();
391
+ if (!passphrase) {
392
+ throw new TreeseedKeyAgentError(
393
+ "interactive_required",
394
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before unlocking the Treeseed secret session.`
395
+ );
396
+ }
397
+ const wrapped = readWrappedMachineKeyFile(keyPath);
398
+ const machineKey = wrapped.wrapped ? unwrapMachineKey(wrapped.wrapped, passphrase) : wrapped.plaintextLegacy ? (() => {
399
+ if (options.allowMigration === false) {
400
+ throw new TreeseedKeyAgentError("wrapped_key_migration_required", "Wrap the legacy machine key before unlocking it.");
401
+ }
402
+ replaceWrappedMachineKey(keyPath, wrapped.plaintextLegacy, passphrase);
403
+ return wrapped.plaintextLegacy;
404
+ })() : (() => {
405
+ if (options.createIfMissing === false) {
406
+ throw new TreeseedKeyAgentError("wrapped_key_missing", "No wrapped Treeseed machine key exists yet.");
407
+ }
408
+ const createdKey = randomBytes(32);
409
+ replaceWrappedMachineKey(keyPath, createdKey, passphrase);
410
+ return createdKey;
411
+ })();
412
+ inlineTreeseedSecretSessions.set(keyPath, {
413
+ machineKey,
414
+ lastTouchedAt: Date.now(),
415
+ idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS
416
+ });
417
+ return inspectTreeseedKeyAgentStatus(tenantRoot);
418
+ }
419
+ startTreeseedKeyAgentDaemon(tenantRoot);
420
+ let response = { ok: false, code: "daemon_unavailable", message: "Treeseed key agent is unavailable." };
421
+ for (let attempt = 0; attempt < 20; attempt += 1) {
422
+ response = runTreeseedKeyAgentCommand([
423
+ "unlock-from-env",
424
+ "--key-path",
425
+ keyPath,
426
+ "--socket-path",
427
+ socketPath,
428
+ "--idle-timeout-ms",
429
+ String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
430
+ ...options.allowMigration === false ? [] : ["--allow-migration"],
431
+ ...options.createIfMissing === false ? [] : ["--create-if-missing"]
432
+ ]);
433
+ if (response.code !== "daemon_unavailable") {
434
+ break;
435
+ }
436
+ sleepMs(25);
437
+ }
438
+ assertTreeseedKeyAgentResponse(
439
+ response,
440
+ `Unable to unlock the Treeseed secret session from ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV}.`
441
+ );
442
+ return response.status;
443
+ }
444
+ function unlockTreeseedSecretSessionWithPassphrase(tenantRoot, passphrase, options = {}) {
445
+ const normalizedPassphrase = String(passphrase ?? "").trim();
446
+ if (!normalizedPassphrase) {
447
+ throw new TreeseedKeyAgentError(
448
+ "interactive_required",
449
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before unlocking the Treeseed secret session.`
450
+ );
451
+ }
452
+ const previousPassphrase = process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV];
453
+ process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] = normalizedPassphrase;
454
+ try {
455
+ if (useInlineKeyAgentTransport()) {
456
+ return unlockTreeseedSecretSessionFromEnv(tenantRoot, options);
457
+ }
458
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
459
+ const { socketPath } = getTreeseedKeyAgentPaths();
460
+ startTreeseedKeyAgentDaemon(tenantRoot);
461
+ let response = { ok: false, code: "daemon_unavailable", message: "Treeseed key agent is unavailable." };
462
+ for (let attempt = 0; attempt < 20; attempt += 1) {
463
+ response = runTreeseedKeyAgentCommand([
464
+ "unlock-from-env",
465
+ "--key-path",
466
+ keyPath,
467
+ "--socket-path",
468
+ socketPath,
469
+ "--idle-timeout-ms",
470
+ String(TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS),
471
+ ...options.allowMigration === false ? [] : ["--allow-migration"],
472
+ ...options.createIfMissing === false ? [] : ["--create-if-missing"]
473
+ ], {
474
+ env: {
475
+ [TREESEED_MACHINE_KEY_PASSPHRASE_ENV]: normalizedPassphrase
476
+ }
477
+ });
478
+ if (response.code !== "daemon_unavailable") {
479
+ break;
480
+ }
481
+ sleepMs(25);
482
+ }
483
+ assertTreeseedKeyAgentResponse(
484
+ response,
485
+ `Unable to unlock the Treeseed secret session from ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV}.`
486
+ );
487
+ return response.status;
488
+ } finally {
489
+ if (previousPassphrase === void 0) {
490
+ delete process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV];
491
+ } else {
492
+ process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] = previousPassphrase;
493
+ }
494
+ }
495
+ }
496
+ async function ensureTreeseedSecretSessionForConfig({
497
+ tenantRoot,
498
+ interactive = false,
499
+ env = process.env,
500
+ createIfMissing = true,
501
+ allowMigration = true,
502
+ promptForPassphrase,
503
+ promptForNewPassphrase
504
+ }) {
505
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
506
+ if (status.unlocked) {
507
+ return {
508
+ status,
509
+ createdWrappedKey: false,
510
+ migratedWrappedKey: false,
511
+ unlockSource: "existing-session"
512
+ };
513
+ }
514
+ const wrappedBefore = readWrappedMachineKeyFile(status.keyPath);
515
+ const envPassphrase = String(env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] ?? "").trim();
516
+ let unlockSource = "existing-session";
517
+ let nextStatus;
518
+ if (envPassphrase) {
519
+ nextStatus = unlockTreeseedSecretSessionWithPassphrase(tenantRoot, envPassphrase, {
520
+ createIfMissing,
521
+ allowMigration
522
+ });
523
+ unlockSource = "env";
524
+ } else if (interactive && status.migrationRequired) {
525
+ if (!promptForNewPassphrase) {
526
+ throw new TreeseedKeyAgentError("interactive_required", "A passphrase prompt is required to migrate the Treeseed machine key.");
527
+ }
528
+ nextStatus = unlockTreeseedSecretSessionWithPassphrase(tenantRoot, await promptForNewPassphrase(), {
529
+ createIfMissing: false,
530
+ allowMigration: true
531
+ });
532
+ unlockSource = "interactive";
533
+ } else if (interactive && !status.wrappedKeyPresent) {
534
+ if (!promptForNewPassphrase) {
535
+ throw new TreeseedKeyAgentError("interactive_required", "A passphrase prompt is required to create the Treeseed machine key.");
536
+ }
537
+ nextStatus = unlockTreeseedSecretSessionWithPassphrase(tenantRoot, await promptForNewPassphrase(), {
538
+ createIfMissing: true,
539
+ allowMigration: false
540
+ });
541
+ unlockSource = "interactive";
542
+ } else if (interactive) {
543
+ if (!promptForPassphrase) {
544
+ throw new TreeseedKeyAgentError("interactive_required", "A passphrase prompt is required to unlock the Treeseed machine key.");
545
+ }
546
+ nextStatus = unlockTreeseedSecretSessionWithPassphrase(tenantRoot, await promptForPassphrase(), {
547
+ createIfMissing: false,
548
+ allowMigration: false
549
+ });
550
+ unlockSource = "interactive";
551
+ } else if (status.migrationRequired) {
552
+ throw new TreeseedKeyAgentError(
553
+ "wrapped_key_migration_required",
554
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before running treeseed config non-interactively so Treeseed can wrap the legacy machine key.`,
555
+ { keyPath: status.keyPath }
556
+ );
557
+ } else if (!status.wrappedKeyPresent) {
558
+ throw new TreeseedKeyAgentError(
559
+ "wrapped_key_missing",
560
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before running treeseed config non-interactively so Treeseed can create the wrapped machine key.`,
561
+ { keyPath: status.keyPath }
562
+ );
563
+ } else {
564
+ throw new TreeseedKeyAgentError(
565
+ "locked",
566
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before running treeseed config non-interactively so Treeseed can unlock the wrapped machine key.`,
567
+ { keyPath: status.keyPath }
568
+ );
569
+ }
570
+ const wrappedAfter = readWrappedMachineKeyFile(status.keyPath);
571
+ return {
572
+ status: nextStatus,
573
+ createdWrappedKey: !wrappedBefore.wrapped && Boolean(wrappedAfter.wrapped) && !wrappedBefore.migrationRequired,
574
+ migratedWrappedKey: wrappedBefore.migrationRequired && Boolean(wrappedAfter.wrapped),
575
+ unlockSource
576
+ };
577
+ }
578
+ function lockTreeseedSecretSession(tenantRoot) {
579
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
580
+ if (useInlineKeyAgentTransport()) {
581
+ inlineTreeseedSecretSessions.set(keyPath, {
582
+ machineKey: null,
583
+ lastTouchedAt: 0,
584
+ idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS
585
+ });
586
+ return inspectTreeseedKeyAgentStatus(tenantRoot);
587
+ }
588
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
589
+ if (!status.running) {
590
+ return status;
591
+ }
592
+ const response = requestTreeseedKeyAgent(tenantRoot, {
593
+ command: "lock",
594
+ keyPath: status.keyPath,
595
+ socketPath: status.socketPath,
596
+ idleTimeoutMs: status.idleTimeoutMs
597
+ });
598
+ assertTreeseedKeyAgentResponse(response, "Unable to lock the Treeseed secret session.");
599
+ return response.status;
600
+ }
601
+ function resolveUnlockedMachineKey(tenantRoot) {
602
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
603
+ if (!status.unlocked) {
604
+ const envPassphrase = String(process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] ?? "").trim();
605
+ if (envPassphrase) {
606
+ unlockTreeseedSecretSessionFromEnv(tenantRoot);
607
+ } else if (keyAgentAutoPromptEnabled()) {
608
+ unlockTreeseedSecretSessionInteractive(tenantRoot);
609
+ } else if (status.migrationRequired) {
610
+ throw new TreeseedKeyAgentError(
611
+ "wrapped_key_migration_required",
612
+ "The Treeseed machine key is still stored in the legacy plaintext format. Run `treeseed secrets:migrate-key` or unlock it from an interactive session first.",
613
+ { keyPath: status.keyPath }
614
+ );
615
+ } else if (!status.wrappedKeyPresent) {
616
+ throw new TreeseedKeyAgentError(
617
+ "wrapped_key_missing",
618
+ `No wrapped Treeseed machine key exists yet. Run \`treeseed config\` or \`treeseed secrets:unlock\` from an interactive shell, or set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} for the startup unlock path.`,
619
+ { keyPath: status.keyPath }
620
+ );
621
+ } else {
622
+ throw new TreeseedKeyAgentError(
623
+ "locked",
624
+ `Treeseed secrets are locked. Run \`treeseed secrets:unlock\`, unlock from an interactive session, or set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} for the startup unlock path before using secret-backed commands.`,
625
+ { keyPath: status.keyPath }
626
+ );
627
+ }
628
+ }
629
+ if (useInlineKeyAgentTransport()) {
630
+ const session = inlineTreeseedSecretSessions.get(status.keyPath);
631
+ if (!session?.machineKey) {
632
+ throw new TreeseedKeyAgentError("locked", "Treeseed secrets are locked.");
633
+ }
634
+ session.lastTouchedAt = Date.now();
635
+ return session.machineKey;
636
+ }
637
+ const response = requestTreeseedKeyAgent(tenantRoot, {
638
+ command: "get-machine-key",
639
+ keyPath: status.keyPath,
640
+ socketPath: status.socketPath,
641
+ idleTimeoutMs: status.idleTimeoutMs
642
+ });
643
+ assertTreeseedKeyAgentResponse(response, "Unable to resolve the Treeseed machine key from the local key agent.");
644
+ return Buffer.from(String(response.machineKey ?? ""), "base64");
645
+ }
193
646
  function getTreeseedRemoteAuthPaths(tenantRoot) {
194
647
  return {
195
648
  authPath: getTreeseedMachineConfigPaths(tenantRoot).authPath
@@ -232,30 +685,16 @@ function createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantCo
232
685
  )
233
686
  };
234
687
  }
235
- function readMachineKey(keyPath) {
236
- if (existsSync(keyPath)) {
237
- return Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
238
- }
239
- return null;
240
- }
241
- function writeMachineKey(keyPath, key) {
242
- ensureParent(keyPath);
243
- writeFileSync(keyPath, `${key.toString("base64")}
244
- `, { mode: 384 });
245
- }
246
- function ensureHomeMachineKey(tenantRoot) {
247
- const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
248
- const existing = readMachineKey(keyPath);
249
- if (existing) {
250
- return existing;
251
- }
252
- const key = randomBytes(32);
253
- writeMachineKey(keyPath, key);
254
- return key;
255
- }
256
688
  function loadLegacyMachineKey(tenantRoot) {
257
689
  const { legacyKeyPath } = getTreeseedMachineConfigPaths(tenantRoot);
258
- return readMachineKey(legacyKeyPath);
690
+ if (!existsSync(legacyKeyPath)) {
691
+ return null;
692
+ }
693
+ try {
694
+ return Buffer.from(readFileSync(legacyKeyPath, "utf8").trim(), "base64");
695
+ } catch {
696
+ return null;
697
+ }
259
698
  }
260
699
  function createDefaultRemoteAuthState() {
261
700
  return {
@@ -386,21 +825,8 @@ function reencryptTreeseedEncryptedState(tenantRoot, oldKey, newKey) {
386
825
  });
387
826
  }
388
827
  }
389
- function ensureTreeseedMachineKeyMigrated(tenantRoot) {
390
- const homeKey = ensureHomeMachineKey(tenantRoot);
391
- const legacyKey = loadLegacyMachineKey(tenantRoot);
392
- if (!legacyKey) {
393
- return homeKey;
394
- }
395
- try {
396
- reencryptTreeseedEncryptedState(tenantRoot, legacyKey, homeKey);
397
- removeLegacyMachineKeyIfSafe(tenantRoot);
398
- } catch {
399
- }
400
- return homeKey;
401
- }
402
828
  function loadMachineKey(tenantRoot) {
403
- return ensureTreeseedMachineKeyMigrated(tenantRoot);
829
+ return resolveUnlockedMachineKey(tenantRoot);
404
830
  }
405
831
  function decryptValueWithMachineKey(tenantRoot, payload, key) {
406
832
  try {
@@ -421,13 +847,73 @@ function rotateTreeseedMachineKey(tenantRoot) {
421
847
  const oldKey = loadMachineKey(tenantRoot);
422
848
  const newKey = randomBytes(32);
423
849
  reencryptTreeseedEncryptedState(tenantRoot, oldKey, newKey);
424
- writeMachineKey(keyPath, newKey);
850
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
851
+ if (!status.unlocked) {
852
+ throw new TreeseedKeyAgentError("locked", "Treeseed secrets must be unlocked before rotating the machine key.", { keyPath });
853
+ }
854
+ const wrapped = readWrappedMachineKeyFile(keyPath);
855
+ if (!wrapped.wrapped) {
856
+ throw new TreeseedKeyAgentError(
857
+ wrapped.migrationRequired ? "wrapped_key_migration_required" : "wrapped_key_missing",
858
+ wrapped.migrationRequired ? "Wrap the Treeseed machine key before rotating it." : "Create and unlock the Treeseed machine key before rotating it.",
859
+ { keyPath }
860
+ );
861
+ }
862
+ const passphrase = String(process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV] ?? "").trim();
863
+ if (!passphrase) {
864
+ throw new TreeseedKeyAgentError(
865
+ "interactive_required",
866
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} when rotating the machine key non-interactively, or use \`treeseed secrets:rotate-machine-key\` from an interactive shell.`,
867
+ { keyPath }
868
+ );
869
+ }
870
+ replaceWrappedMachineKey(keyPath, newKey, passphrase);
871
+ unlockTreeseedSecretSessionFromEnv(tenantRoot, { allowMigration: false, createIfMissing: false });
425
872
  removeLegacyMachineKeyIfSafe(tenantRoot);
426
873
  return {
427
874
  keyPath,
428
875
  rotated: true
429
876
  };
430
877
  }
878
+ function rotateTreeseedMachineKeyPassphrase(tenantRoot, passphrase) {
879
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
880
+ const machineKey = loadMachineKey(tenantRoot);
881
+ rotateWrappedMachineKeyPassphrase(keyPath, machineKey, passphrase);
882
+ return {
883
+ keyPath,
884
+ rotated: true
885
+ };
886
+ }
887
+ function migrateTreeseedMachineKeyToWrapped(tenantRoot, passphrase) {
888
+ const { keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
889
+ const wrapped = readWrappedMachineKeyFile(keyPath);
890
+ if (wrapped.wrapped) {
891
+ return {
892
+ keyPath,
893
+ migrated: false,
894
+ alreadyWrapped: true
895
+ };
896
+ }
897
+ if (wrapped.plaintextLegacy) {
898
+ replaceWrappedMachineKey(keyPath, wrapped.plaintextLegacy, passphrase);
899
+ } else {
900
+ const legacyKey = loadLegacyMachineKey(tenantRoot);
901
+ if (!legacyKey) {
902
+ throw new TreeseedKeyAgentError(
903
+ "wrapped_key_missing",
904
+ "No existing machine key was found to migrate.",
905
+ { keyPath }
906
+ );
907
+ }
908
+ replaceWrappedMachineKey(keyPath, legacyKey, passphrase);
909
+ removeLegacyMachineKeyIfSafe(tenantRoot);
910
+ }
911
+ return {
912
+ keyPath,
913
+ migrated: true,
914
+ alreadyWrapped: false
915
+ };
916
+ }
431
917
  function loadTreeseedRemoteAuthState(tenantRoot) {
432
918
  const key = loadMachineKey(tenantRoot);
433
919
  const payload = loadRemoteAuthPayload(tenantRoot);
@@ -555,6 +1041,24 @@ function writeTreeseedMachineConfig(tenantRoot, config) {
555
1041
  ensureParent(configPath);
556
1042
  writeFileSync(configPath, stringifyYaml(config), "utf8");
557
1043
  }
1044
+ function updateTreeseedDeployConfigFeatureToggles(tenantRoot, toggles) {
1045
+ const configPath = resolve(tenantRoot, "treeseed.site.yaml");
1046
+ const current = parseYaml(readFileSync(configPath, "utf8")) ?? {};
1047
+ const next = { ...current };
1048
+ if ("smtp" in toggles) {
1049
+ next.smtp = {
1050
+ ...current.smtp ?? {},
1051
+ enabled: toggles.smtp === true
1052
+ };
1053
+ }
1054
+ if ("turnstile" in toggles) {
1055
+ next.turnstile = {
1056
+ ...current.turnstile ?? {},
1057
+ enabled: toggles.turnstile === true
1058
+ };
1059
+ }
1060
+ writeFileSync(configPath, stringifyYaml(next), "utf8");
1061
+ }
558
1062
  function resolveTreeseedRemoteConfig(startRoot = process.cwd(), env = process.env) {
559
1063
  const machineConfigPath = findNearestTreeseedMachineConfig(startRoot);
560
1064
  const tenantRoot = machineConfigPath ? resolve(dirname(dirname(machineConfigPath)), "..") : startRoot;
@@ -570,7 +1074,7 @@ function resolveTreeseedRemoteConfig(startRoot = process.cwd(), env = process.en
570
1074
  tenantConfig: void 0
571
1075
  });
572
1076
  const settings = normalizeRemoteSettings(machineConfig.settings?.remote);
573
- const deployBaseUrl = deployConfig?.services?.api?.environments?.prod?.baseUrl ?? deployConfig?.services?.api?.publicBaseUrl ?? null;
1077
+ const deployBaseUrl = env[TREESEED_API_BASE_URL_ENV] ?? deployConfig?.services?.api?.environments?.prod?.baseUrl ?? deployConfig?.services?.api?.publicBaseUrl ?? null;
574
1078
  if (deployBaseUrl) {
575
1079
  const officialHost = settings.hosts.find((entry) => entry.id === "official");
576
1080
  if (officialHost) {
@@ -623,7 +1127,7 @@ function resolveTreeseedTemplateCatalogCachePath(startRoot = process.cwd()) {
623
1127
  }
624
1128
  function ensureTreeseedGitignoreEntries(tenantRoot) {
625
1129
  const gitignorePath = resolve(tenantRoot, ".gitignore");
626
- const requiredEntries = [".env.local", ".dev.vars", ".treeseed/"];
1130
+ const requiredEntries = [".treeseed/"];
627
1131
  const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
628
1132
  const lines = current.split(/\r?\n/);
629
1133
  let changed = false;
@@ -653,11 +1157,9 @@ function applyTreeseedSafeRepairs(tenantRoot) {
653
1157
  const actions = [];
654
1158
  ensureTreeseedGitignoreEntries(tenantRoot);
655
1159
  actions.push({ id: "gitignore", detail: "Ensured Treeseed gitignore entries are present." });
656
- const envLocalPath = resolve(tenantRoot, ".env.local");
657
- const envLocalExamplePath = resolve(tenantRoot, ".env.local.example");
658
- if (!existsSync(envLocalPath) && existsSync(envLocalExamplePath)) {
659
- copyFileSync(envLocalExamplePath, envLocalPath);
660
- actions.push({ id: "env-local", detail: "Created .env.local from .env.local.example." });
1160
+ const deprecatedFiles = warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1161
+ if (deprecatedFiles.length > 0) {
1162
+ actions.push({ id: "deprecated-local-env", detail: "Detected deprecated .env.local/.dev.vars files that Treeseed now ignores." });
661
1163
  }
662
1164
  const deployConfig = loadTenantDeployConfig(tenantRoot);
663
1165
  const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
@@ -670,12 +1172,14 @@ function applyTreeseedSafeRepairs(tenantRoot) {
670
1172
  writeTreeseedMachineConfig(tenantRoot, machineConfig2);
671
1173
  actions.push({ id: "machine-config", detail: "Created the default Treeseed machine config." });
672
1174
  }
673
- resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
674
- actions.push({ id: "machine-key", detail: "Ensured the Treeseed machine key exists." });
1175
+ const keyStatus = inspectTreeseedKeyAgentStatus(tenantRoot);
1176
+ if (!keyStatus.wrappedKeyPresent && !keyStatus.migrationRequired) {
1177
+ actions.push({ id: "machine-key", detail: "Treeseed will create a wrapped machine key the first time the secret session is unlocked." });
1178
+ } else if (keyStatus.migrationRequired) {
1179
+ actions.push({ id: "machine-key-migration", detail: "Detected a legacy plaintext machine key that must be wrapped on the next unlock." });
1180
+ }
675
1181
  const machineConfig = loadTreeseedMachineConfig(tenantRoot);
676
1182
  writeTreeseedMachineConfig(tenantRoot, machineConfig);
677
- writeTreeseedLocalEnvironmentFiles(tenantRoot);
678
- actions.push({ id: "local-env", detail: "Regenerated .env.local and .dev.vars from the current machine config." });
679
1183
  for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
680
1184
  const target = createPersistentDeployTarget(scope);
681
1185
  const state = loadDeployState(tenantRoot, deployConfig, { target });
@@ -695,6 +1199,70 @@ function decryptMachineEnvironmentBucket(tenantRoot, config, key, bucket) {
695
1199
  }
696
1200
  return values;
697
1201
  }
1202
+ function readMachineBucketEntryValue(tenantRoot, key, bucket, entry) {
1203
+ if (entry.sensitivity === "secret") {
1204
+ const payload = bucket?.secrets?.[entry.id];
1205
+ return typeof payload === "string" && payload.length > 0 ? decryptValueWithMachineKey(tenantRoot, payload, key) : "";
1206
+ }
1207
+ return typeof bucket?.values?.[entry.id] === "string" ? bucket.values[entry.id] : "";
1208
+ }
1209
+ function writeMachineBucketEntryValue(target, entry, value, key) {
1210
+ if (entry.sensitivity === "secret") {
1211
+ delete target.values[entry.id];
1212
+ if (value) {
1213
+ target.secrets[entry.id] = encryptValue(value, key);
1214
+ } else {
1215
+ delete target.secrets[entry.id];
1216
+ }
1217
+ return;
1218
+ }
1219
+ delete target.secrets[entry.id];
1220
+ if (value) {
1221
+ target.values[entry.id] = value;
1222
+ } else {
1223
+ delete target.values[entry.id];
1224
+ }
1225
+ }
1226
+ function migrateLegacyScopedSharedEntries(tenantRoot, config, registryEntries, key) {
1227
+ const notices = [];
1228
+ let changed = false;
1229
+ for (const entry of registryEntries) {
1230
+ if (entry.storage !== "shared") {
1231
+ continue;
1232
+ }
1233
+ const sharedValue = readMachineBucketEntryValue(tenantRoot, key, config.shared, entry);
1234
+ if (sharedValue.length > 0) {
1235
+ continue;
1236
+ }
1237
+ const scopedValues = TREESEED_ENVIRONMENT_SCOPES.map((scope) => ({
1238
+ scope,
1239
+ value: readMachineBucketEntryValue(tenantRoot, key, config.environments?.[scope], entry)
1240
+ })).filter((candidate) => candidate.value.length > 0);
1241
+ if (scopedValues.length === 0) {
1242
+ continue;
1243
+ }
1244
+ const promotedFrom = (scopedValues.find((candidate) => candidate.scope === "staging") ?? scopedValues.find((candidate) => candidate.scope === "prod") ?? scopedValues[0]).scope;
1245
+ const promotedValue = scopedValues.find((candidate) => candidate.scope === promotedFrom)?.value ?? "";
1246
+ const hadConflicts = new Set(scopedValues.map((candidate) => candidate.value)).size > 1;
1247
+ writeMachineBucketEntryValue(config.shared, entry, promotedValue, key);
1248
+ for (const candidateScope of TREESEED_ENVIRONMENT_SCOPES) {
1249
+ delete config.environments[candidateScope].values[entry.id];
1250
+ delete config.environments[candidateScope].secrets[entry.id];
1251
+ }
1252
+ notices.push({
1253
+ entryId: entry.id,
1254
+ label: entry.label,
1255
+ promotedFrom,
1256
+ consolidatedScopes: scopedValues.map((candidate) => candidate.scope),
1257
+ hadConflicts
1258
+ });
1259
+ changed = true;
1260
+ }
1261
+ if (changed) {
1262
+ writeTreeseedMachineConfig(tenantRoot, config);
1263
+ }
1264
+ return notices;
1265
+ }
698
1266
  function resolveEntryValueFromBuckets(entry, entryId, scope, bucketValuesByScope) {
699
1267
  if (!entry) {
700
1268
  return bucketValuesByScope[scope]?.[entryId] ?? bucketValuesByScope.shared?.[entryId] ?? "";
@@ -780,14 +1348,28 @@ function collectTreeseedEnvironmentContext(tenantRoot) {
780
1348
  });
781
1349
  }
782
1350
  function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
1351
+ warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1352
+ let machineValues = {};
1353
+ try {
1354
+ machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1355
+ } catch (error) {
1356
+ if (!(error instanceof TreeseedKeyAgentError)) {
1357
+ throw error;
1358
+ }
1359
+ }
1360
+ const normalizedEnv = { ...env };
1361
+ for (const [legacyKey, canonicalKey] of Object.entries(LEGACY_ENVIRONMENT_ALIASES)) {
1362
+ if ((!normalizedEnv[canonicalKey] || String(normalizedEnv[canonicalKey]).length === 0) && normalizedEnv[legacyKey]) {
1363
+ normalizedEnv[canonicalKey] = normalizedEnv[legacyKey];
1364
+ }
1365
+ }
783
1366
  return {
784
- ...readEnvFileIfPresent(resolve(tenantRoot, ".env.local")),
785
- ...readEnvFileIfPresent(resolve(tenantRoot, ".dev.vars")),
786
- ...Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0])),
787
- ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope)
1367
+ ...machineValues,
1368
+ ...Object.fromEntries(Object.entries(normalizedEnv).map(([key, value]) => [key, value ?? void 0]))
788
1369
  };
789
1370
  }
790
1371
  function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
1372
+ warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
791
1373
  const values = {};
792
1374
  const sources = {};
793
1375
  const merge = (source, entries) => {
@@ -799,12 +1381,29 @@ function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.
799
1381
  sources[key] = source;
800
1382
  }
801
1383
  };
802
- merge(".env.local", readEnvFileIfPresent(resolve(tenantRoot, ".env.local")));
803
- merge(".dev.vars", readEnvFileIfPresent(resolve(tenantRoot, ".dev.vars")));
1384
+ try {
1385
+ merge("machine-config", resolveTreeseedMachineEnvironmentValues(tenantRoot, scope));
1386
+ } catch (error) {
1387
+ if (!(error instanceof TreeseedKeyAgentError)) {
1388
+ throw error;
1389
+ }
1390
+ }
804
1391
  merge("process.env", Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0])));
805
- merge("machine-config", resolveTreeseedMachineEnvironmentValues(tenantRoot, scope));
806
1392
  return { values, sources };
807
1393
  }
1394
+ function resolveTreeseedLaunchEnvironment({
1395
+ tenantRoot,
1396
+ scope,
1397
+ baseEnv = process.env,
1398
+ overrides = {}
1399
+ }) {
1400
+ warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1401
+ return {
1402
+ ...baseEnv,
1403
+ ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
1404
+ ...overrides
1405
+ };
1406
+ }
808
1407
  function formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env = process.env, revealSecrets = false }) {
809
1408
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
810
1409
  const { values, sources } = collectTreeseedConfigSeedValueSources(tenantRoot, scope, env);
@@ -820,7 +1419,14 @@ function formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env = proces
820
1419
  return lines.join("\n");
821
1420
  }
822
1421
  function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false }) {
823
- const resolvedValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
1422
+ let resolvedValues = {};
1423
+ try {
1424
+ resolvedValues = resolveTreeseedLaunchEnvironment({ tenantRoot, scope, baseEnv: {} });
1425
+ } catch (error) {
1426
+ if (!(error instanceof TreeseedKeyAgentError)) {
1427
+ throw error;
1428
+ }
1429
+ }
824
1430
  for (const [key, value] of Object.entries(resolvedValues)) {
825
1431
  const currentValue = process.env[key] ?? "";
826
1432
  const shouldReplacePlaceholder = key === "CLOUDFLARE_ACCOUNT_ID" && currentValue === CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER;
@@ -832,11 +1438,7 @@ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false
832
1438
  }
833
1439
  function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
834
1440
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
835
- const machineValues = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
836
- const values = {
837
- ...machineValues,
838
- ...Object.fromEntries(Object.entries(process.env).map(([key, value]) => [key, value ?? void 0]))
839
- };
1441
+ const values = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
840
1442
  const validation = validateTreeseedEnvironmentValues({
841
1443
  values,
842
1444
  scope,
@@ -868,29 +1470,6 @@ function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
868
1470
  error.details = report.validation;
869
1471
  throw error;
870
1472
  }
871
- function renderEnvEntries(entries, values) {
872
- return entries.map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0).map(([key, value]) => `${key}=${value}`).join("\n");
873
- }
874
- function writeTreeseedLocalEnvironmentFiles(tenantRoot) {
875
- const registry = collectTreeseedEnvironmentContext(tenantRoot);
876
- const scope = "local";
877
- const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
878
- const orderedEntries = listRelevantTreeseedConfigEntries(registry, scope);
879
- const envEntries = orderedEntries.filter(
880
- (entry) => entry.scopes.includes(scope) && entry.targets.includes("local-file")
881
- );
882
- const devVarsEntries = orderedEntries.filter(
883
- (entry) => entry.scopes.includes(scope) && entry.targets.includes("wrangler-dev-vars")
884
- );
885
- writeFileSync(resolve(tenantRoot, ".env.local"), `${renderEnvEntries(envEntries, values)}
886
- `, "utf8");
887
- writeFileSync(resolve(tenantRoot, ".dev.vars"), `${renderEnvEntries(devVarsEntries, values)}
888
- `, "utf8");
889
- return {
890
- envLocalPath: resolve(tenantRoot, ".env.local"),
891
- devVarsPath: resolve(tenantRoot, ".dev.vars")
892
- };
893
- }
894
1473
  function runGh(args, { cwd, dryRun = false, input } = {}) {
895
1474
  if (dryRun) {
896
1475
  return { status: 0, stdout: "", stderr: "" };
@@ -933,15 +1512,18 @@ function checkCommand(command, args, { cwd, env } = {}) {
933
1512
  cwd,
934
1513
  stdio: "pipe",
935
1514
  encoding: "utf8",
936
- env: { ...process.env, ...env ?? {} }
1515
+ env: { ...process.env, ...env ?? {} },
1516
+ timeout: CLI_CHECK_TIMEOUT_MS
937
1517
  });
1518
+ const timedOut = result.error && "code" in result.error && result.error.code === "ETIMEDOUT";
1519
+ const detail = timedOut ? `Command timed out after ${CLI_CHECK_TIMEOUT_MS}ms: ${command} ${args.join(" ")}` : `${result.stderr ?? ""}
1520
+ ${result.stdout ?? ""}`.trim();
938
1521
  return {
939
1522
  ok: result.status === 0,
940
1523
  status: result.status ?? 1,
941
1524
  stdout: result.stdout?.trim() ?? "",
942
1525
  stderr: result.stderr?.trim() ?? "",
943
- detail: `${result.stderr ?? ""}
944
- ${result.stdout ?? ""}`.trim()
1526
+ detail
945
1527
  };
946
1528
  }
947
1529
  function toolStatus(name, available, detail, extra = {}) {
@@ -961,6 +1543,18 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
961
1543
  attemptedInstall: false,
962
1544
  installedDuringConfig: false
963
1545
  });
1546
+ const wranglerCheck = checkCommand(process.execPath, [resolveWranglerBin(), "--version"], { cwd: tenantRoot, env });
1547
+ const wranglerCli = toolStatus(
1548
+ "wranglerCli",
1549
+ wranglerCheck.ok,
1550
+ wranglerCheck.ok ? wranglerCheck.stdout.split("\n")[0] ?? "Wrangler CLI detected." : wranglerCheck.detail || "Wrangler CLI is unavailable."
1551
+ );
1552
+ const railwayCheck = checkCommand("railway", ["--version"], { cwd: tenantRoot, env });
1553
+ const railwayCli = toolStatus(
1554
+ "railwayCli",
1555
+ railwayCheck.ok,
1556
+ railwayCheck.ok ? railwayCheck.stdout.split("\n")[0] ?? "Railway CLI detected." : railwayCheck.detail || "Railway CLI is unavailable."
1557
+ );
964
1558
  if (githubCli.available) {
965
1559
  const check = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
966
1560
  if (check.ok) {
@@ -1005,10 +1599,18 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1005
1599
  if (!dockerDaemon.available) {
1006
1600
  remediation.push("Start Docker Desktop or another local Docker daemon, then rerun `treeseed config`.");
1007
1601
  }
1602
+ if (!wranglerCli.available) {
1603
+ remediation.push("Install Wrangler or ensure the packaged Wrangler dependency is runnable, then rerun `treeseed config`.");
1604
+ }
1605
+ if (!railwayCli.available) {
1606
+ remediation.push("Install Railway CLI if you plan to manage Railway services from this machine.");
1607
+ }
1008
1608
  return {
1009
1609
  githubCli,
1010
1610
  ghActExtension,
1011
1611
  dockerDaemon,
1612
+ wranglerCli,
1613
+ railwayCli,
1012
1614
  actVerificationReady: githubCli.available && ghActExtension.available && dockerDaemon.available,
1013
1615
  remediation
1014
1616
  };
@@ -1038,7 +1640,8 @@ function checkGitHubConnection({ tenantRoot, env }) {
1038
1640
  cwd: tenantRoot,
1039
1641
  stdio: "pipe",
1040
1642
  encoding: "utf8",
1041
- env: { ...process.env, ...env }
1643
+ env: { ...process.env, ...env },
1644
+ timeout: CLI_CHECK_TIMEOUT_MS
1042
1645
  });
1043
1646
  if (result.status !== 0) {
1044
1647
  return providerConnectionResult("github", false, formatCheckOutput(result) || "GitHub API check failed.");
@@ -1059,7 +1662,8 @@ function checkCloudflareConnection({ tenantRoot, env }) {
1059
1662
  cwd: tenantRoot,
1060
1663
  stdio: "pipe",
1061
1664
  encoding: "utf8",
1062
- env: { ...process.env, ...env }
1665
+ env: { ...process.env, ...env },
1666
+ timeout: CLI_CHECK_TIMEOUT_MS
1063
1667
  });
1064
1668
  if (result.status !== 0) {
1065
1669
  return providerConnectionResult("cloudflare", false, formatCheckOutput(result) || "Cloudflare Wrangler check failed.");
@@ -1080,7 +1684,8 @@ function checkRailwayConnection({ tenantRoot, env }) {
1080
1684
  cwd: tenantRoot,
1081
1685
  stdio: "pipe",
1082
1686
  encoding: "utf8",
1083
- env: { ...process.env, ...env }
1687
+ env: { ...process.env, ...env },
1688
+ timeout: CLI_CHECK_TIMEOUT_MS
1084
1689
  });
1085
1690
  if (result.status !== 0) {
1086
1691
  return providerConnectionResult("railway", false, formatCheckOutput(result) || "Railway CLI check failed.");
@@ -1104,7 +1709,8 @@ function checkTreeseedProviderConnections({ tenantRoot, scope = "prod", env = pr
1104
1709
  return {
1105
1710
  scope,
1106
1711
  ok: checks.every((check) => check.ready || check.skipped),
1107
- checks
1712
+ checks,
1713
+ issues: checks.filter((check) => !check.ready && !check.skipped).map((check) => check.detail)
1108
1714
  };
1109
1715
  }
1110
1716
  function formatTreeseedProviderConnectionReport(report) {
@@ -1184,14 +1790,14 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
1184
1790
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1185
1791
  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);
1186
1792
  const railwayVariableNames = registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("railway-var")).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
1187
- const services = ["api", "agents", "manager", "worker", "runner", "workdayStart", "workdayReport"].map((serviceKey) => {
1793
+ const services = ["api", "manager", "worker", "workdayStart", "workdayReport"].map((serviceKey) => {
1188
1794
  const service = deployConfig.services?.[serviceKey];
1189
1795
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
1190
1796
  return null;
1191
1797
  }
1192
1798
  const environment = service.environments?.[scope];
1193
- const fallbackServiceName = serviceKey === "api" ? config.settings.services.railway.apiServiceName : serviceKey === "agents" ? config.settings.services.railway.agentsServiceName : "";
1194
- const defaultRootDir = ["api", "manager", "worker", "runner", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
1799
+ const fallbackServiceName = serviceKey === "api" ? config.settings.services.railway.apiServiceName : "";
1800
+ const defaultRootDir = ["api", "manager", "worker", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
1195
1801
  return {
1196
1802
  service: serviceKey,
1197
1803
  projectName: service.railway?.projectName ?? config.settings.services.railway.projectName,
@@ -1241,39 +1847,97 @@ function initializeTreeseedPersistentEnvironment({ tenantRoot, scope = "prod", d
1241
1847
  };
1242
1848
  }
1243
1849
  function summarizePersistentReadiness(tenantRoot, scope, validation, connectionChecks) {
1850
+ const validationProblems = [...validation.missing, ...validation.invalid];
1851
+ const validationBlockers = validationProblems.map((problem) => problem.message);
1852
+ const connectionReady = connectionChecks.every((check) => check.ready || check.skipped);
1853
+ const connectionIssues = connectionChecks.filter((check) => !check.ready && !check.skipped).map((check) => `${check.provider}: ${check.detail}`);
1854
+ const connectionWarnings = connectionChecks.filter((check) => check.skipped).map((check) => `${check.provider}: ${check.detail}`);
1244
1855
  if (scope === "local") {
1245
1856
  return {
1246
1857
  configured: validation.ok,
1247
1858
  provisioned: true,
1248
- deployable: validation.ok,
1859
+ deployable: validation.ok && connectionReady,
1860
+ phase: validation.ok ? "code_ready" : "config_incomplete",
1861
+ blockers: [
1862
+ ...validationBlockers,
1863
+ ...connectionIssues
1864
+ ],
1865
+ warnings: connectionWarnings,
1249
1866
  checks: {
1250
1867
  validation: validation.ok,
1251
- connections: connectionChecks.every((check) => check.ready || check.skipped)
1868
+ connections: connectionReady
1869
+ }
1870
+ };
1871
+ }
1872
+ if (!validation.ok) {
1873
+ return {
1874
+ configured: false,
1875
+ provisioned: false,
1876
+ deployable: false,
1877
+ phase: "config_incomplete",
1878
+ blockers: [
1879
+ ...validationBlockers,
1880
+ ...connectionIssues
1881
+ ],
1882
+ warnings: connectionWarnings,
1883
+ checks: {
1884
+ validation: false,
1885
+ connections: connectionReady,
1886
+ cloudflare: null,
1887
+ railway: false
1252
1888
  }
1253
1889
  };
1254
1890
  }
1255
1891
  const cloudflare = verifyProvisionedCloudflareResources(tenantRoot, { scope });
1256
1892
  let railwayReady = true;
1893
+ let railwayIssue = null;
1257
1894
  try {
1258
1895
  validateRailwayDeployPrerequisites(tenantRoot, scope);
1259
- } catch {
1896
+ } catch (error) {
1260
1897
  railwayReady = false;
1898
+ railwayIssue = error instanceof Error ? error.message : String(error);
1261
1899
  }
1262
1900
  const configured = validation.ok;
1263
1901
  const provisioned = cloudflare.ok && railwayReady;
1264
- const deployable = configured && provisioned && connectionChecks.every((check) => check.ready || check.skipped);
1902
+ const deployable = configured && provisioned && connectionReady;
1903
+ const blockers = [
1904
+ ...connectionIssues,
1905
+ ...railwayIssue ? [railwayIssue] : []
1906
+ ];
1907
+ if (!cloudflare.ok) {
1908
+ blockers.push("Cloudflare foundational resources have not been fully provisioned yet.");
1909
+ }
1265
1910
  return {
1266
1911
  configured,
1267
1912
  provisioned,
1268
1913
  deployable,
1914
+ phase: provisioned ? "provisioned" : "config_complete",
1915
+ blockers,
1916
+ warnings: connectionWarnings,
1269
1917
  checks: {
1270
1918
  validation: validation.ok,
1271
- connections: connectionChecks.every((check) => check.ready || check.skipped),
1919
+ connections: connectionReady,
1272
1920
  cloudflare: cloudflare.checks,
1273
1921
  railway: railwayReady
1274
1922
  }
1275
1923
  };
1276
1924
  }
1925
+ function formatTreeseedConfigValidationFailure(validations, scopes) {
1926
+ const lines = ["Treeseed config validation failed."];
1927
+ for (const scope of scopes) {
1928
+ const validation = validations[scope];
1929
+ if (!validation || validation.ok) {
1930
+ continue;
1931
+ }
1932
+ lines.push("");
1933
+ lines.push(`${scope}:`);
1934
+ for (const problem of [...validation.missing, ...validation.invalid]) {
1935
+ const targets = problem.entry.targets.length > 0 ? ` Targets: ${problem.entry.targets.join(", ")}.` : "";
1936
+ lines.push(`- ${problem.id}: ${problem.message}${targets}`);
1937
+ }
1938
+ }
1939
+ return lines.join("\n");
1940
+ }
1277
1941
  function colorize(value, code) {
1278
1942
  return `\x1B[${code}m${value}\x1B[0m`;
1279
1943
  }
@@ -1284,31 +1948,40 @@ function formatConfigSectionTitle(label) {
1284
1948
  function hasConfigValue(values, key) {
1285
1949
  return typeof values[key] === "string" && values[key].trim().length > 0;
1286
1950
  }
1287
- function createConfigAuthStatus(values) {
1288
- const ghReady = hasConfigValue(values, "GH_TOKEN");
1951
+ function createConfigReadiness(values, validation) {
1952
+ const invalidIds = /* @__PURE__ */ new Set([
1953
+ ...(validation?.invalid ?? []).map((problem) => problem.id)
1954
+ ]);
1955
+ const validConfigValue = (key) => hasConfigValue(values, key) && !invalidIds.has(key);
1956
+ const cloudflareReady = validConfigValue("CLOUDFLARE_API_TOKEN");
1957
+ const railwayReady = validConfigValue("RAILWAY_API_TOKEN");
1958
+ const localDevelopmentIssues = [
1959
+ ...validation?.missing ?? [],
1960
+ ...validation?.invalid ?? []
1961
+ ].filter((problem) => problem.entry.group === "local-development");
1289
1962
  return {
1290
- gh: {
1291
- authenticated: ghReady
1963
+ github: {
1964
+ configured: validConfigValue("GH_TOKEN")
1292
1965
  },
1293
- wrangler: {
1294
- authenticated: hasConfigValue(values, "CLOUDFLARE_API_TOKEN")
1966
+ cloudflare: {
1967
+ configured: cloudflareReady
1295
1968
  },
1296
1969
  railway: {
1297
- authenticated: hasConfigValue(values, "RAILWAY_API_TOKEN")
1970
+ configured: railwayReady
1298
1971
  },
1299
- copilot: {
1300
- configured: ghReady
1972
+ localDevelopment: {
1973
+ configured: localDevelopmentIssues.length === 0
1301
1974
  }
1302
1975
  };
1303
1976
  }
1304
- const CONFIG_GROUP_ORDER = ["auth", "cloudflare", "local-development", "forms", "smtp"];
1977
+ const CONFIG_GROUP_ORDER = ["auth", "github", "cloudflare", "railway", "local-development", "forms", "smtp"];
1305
1978
  function configGroupRank(group) {
1306
1979
  const index = CONFIG_GROUP_ORDER.indexOf(group);
1307
1980
  return index === -1 ? CONFIG_GROUP_ORDER.length : index;
1308
1981
  }
1309
1982
  function listRelevantTreeseedConfigEntries(registry, scope) {
1310
1983
  return registry.entries.filter(
1311
- (entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config"))
1984
+ (entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config") || Boolean(entry.onboardingFeature))
1312
1985
  ).sort((left, right) => {
1313
1986
  const leftRequired = isTreeseedEnvironmentEntryRequired(left, registry.context, scope, "config");
1314
1987
  const rightRequired = isTreeseedEnvironmentEntryRequired(right, registry.context, scope, "config");
@@ -1325,22 +1998,55 @@ function listRelevantTreeseedConfigEntries(registry, scope) {
1325
1998
  });
1326
1999
  }
1327
2000
  function buildConfigEntrySnapshot(scope, entry, currentValue, suggestedValue) {
2001
+ const currentValueValid = (() => {
2002
+ if (!currentValue || !entry.validation) {
2003
+ return currentValue.length > 0;
2004
+ }
2005
+ switch (entry.validation.kind) {
2006
+ case "string":
2007
+ case "nonempty":
2008
+ return currentValue.trim().length > 0 && (typeof entry.validation.minLength !== "number" || currentValue.trim().length >= entry.validation.minLength);
2009
+ case "boolean":
2010
+ return /^(true|false|1|0)$/i.test(currentValue);
2011
+ case "number":
2012
+ return Number.isFinite(Number(currentValue));
2013
+ case "url":
2014
+ try {
2015
+ new URL(currentValue);
2016
+ return true;
2017
+ } catch {
2018
+ return false;
2019
+ }
2020
+ case "email":
2021
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(currentValue);
2022
+ case "enum":
2023
+ return entry.validation.values.includes(currentValue);
2024
+ default:
2025
+ return true;
2026
+ }
2027
+ })();
2028
+ const allowSuggestedDefault = !(entry.sensitivity === "secret" && entry.requirement !== "optional");
2029
+ const effectiveValue = currentValueValid ? currentValue || (allowSuggestedDefault ? suggestedValue : "") || "" : (allowSuggestedDefault ? suggestedValue : "") || currentValue || "";
1328
2030
  return {
1329
2031
  id: entry.id,
1330
2032
  label: entry.label,
1331
2033
  group: entry.group,
2034
+ cluster: entry.cluster ?? `${entry.group}:${entry.id}`,
2035
+ startupProfile: entry.startupProfile ?? "advanced",
2036
+ requirement: entry.requirement,
1332
2037
  description: entry.description,
1333
2038
  howToGet: entry.howToGet,
1334
2039
  sensitivity: entry.sensitivity,
1335
2040
  targets: [...entry.targets],
1336
2041
  purposes: [...entry.purposes],
1337
2042
  storage: entry.storage ?? "scoped",
2043
+ validation: entry.validation,
1338
2044
  scope,
1339
2045
  sharedScopes: entry.storage === "shared" ? [...entry.scopes] : [scope],
1340
2046
  required: false,
1341
2047
  currentValue,
1342
2048
  suggestedValue,
1343
- effectiveValue: currentValue || suggestedValue || ""
2049
+ effectiveValue
1344
2050
  };
1345
2051
  }
1346
2052
  function collectTreeseedConfigContext({
@@ -1364,9 +2070,6 @@ function collectTreeseedConfigContext({
1364
2070
  values: valuesByScope[scope]
1365
2071
  })])
1366
2072
  );
1367
- const authStatusByScope = Object.fromEntries(
1368
- scopes.map((scope) => [scope, createConfigAuthStatus(valuesByScope[scope])])
1369
- );
1370
2073
  const validationByScope = Object.fromEntries(
1371
2074
  scopes.map((scope) => [scope, validateTreeseedEnvironmentValues({
1372
2075
  values: {
@@ -1380,6 +2083,9 @@ function collectTreeseedConfigContext({
1380
2083
  plugins: registry.context.plugins
1381
2084
  })])
1382
2085
  );
2086
+ const configReadinessByScope = Object.fromEntries(
2087
+ scopes.map((scope) => [scope, createConfigReadiness(valuesByScope[scope], validationByScope[scope])])
2088
+ );
1383
2089
  const entriesByScope = Object.fromEntries(
1384
2090
  scopes.map((scope) => [scope, listRelevantTreeseedConfigEntries(registry, scope).map((entry) => ({
1385
2091
  ...buildConfigEntrySnapshot(
@@ -1404,18 +2110,21 @@ function collectTreeseedConfigContext({
1404
2110
  entriesByScope,
1405
2111
  valuesByScope,
1406
2112
  suggestedValuesByScope,
1407
- authStatusByScope,
2113
+ configReadinessByScope,
1408
2114
  validationByScope,
2115
+ sharedStorageMigrations: [],
1409
2116
  registry
1410
2117
  };
1411
2118
  }
1412
2119
  function applyTreeseedConfigValues({
1413
2120
  tenantRoot,
1414
2121
  updates,
1415
- writeLocalFiles = true,
1416
2122
  applyLocalEnvironment = true
1417
2123
  }) {
1418
2124
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
2125
+ const key = loadMachineKey(tenantRoot);
2126
+ const machineConfig = loadTreeseedMachineConfig(tenantRoot);
2127
+ const sharedStorageMigrations = migrateLegacyScopedSharedEntries(tenantRoot, machineConfig, registry.entries, key);
1419
2128
  const entryById = new Map(registry.entries.map((entry) => [entry.id, entry]));
1420
2129
  const applied = [];
1421
2130
  for (const update of updates) {
@@ -1434,13 +2143,12 @@ function applyTreeseedConfigValues({
1434
2143
  cleared: update.value.length === 0
1435
2144
  });
1436
2145
  }
1437
- const envFiles = writeLocalFiles ? writeTreeseedLocalEnvironmentFiles(tenantRoot) : null;
1438
2146
  if (applyLocalEnvironment) {
1439
2147
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
1440
2148
  }
1441
2149
  return {
1442
2150
  updated: applied,
1443
- envFiles
2151
+ sharedStorageMigrations
1444
2152
  };
1445
2153
  }
1446
2154
  function finalizeTreeseedConfig({
@@ -1449,7 +2157,8 @@ function finalizeTreeseedConfig({
1449
2157
  sync = "all",
1450
2158
  env = process.env,
1451
2159
  checkConnections = true,
1452
- initializePersistent = true
2160
+ initializePersistent = true,
2161
+ onProgress
1453
2162
  }) {
1454
2163
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1455
2164
  const summary = {
@@ -1460,6 +2169,12 @@ function finalizeTreeseedConfig({
1460
2169
  validationByScope: {},
1461
2170
  readinessByScope: {}
1462
2171
  };
2172
+ const progress = (message) => {
2173
+ if (typeof onProgress === "function") {
2174
+ onProgress(message);
2175
+ }
2176
+ };
2177
+ progress(`Validating configuration for ${scopes.join(", ")}...`);
1463
2178
  for (const scope of scopes) {
1464
2179
  const validation = validateTreeseedEnvironmentValues({
1465
2180
  values: resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
@@ -1470,22 +2185,31 @@ function finalizeTreeseedConfig({
1470
2185
  plugins: registry.context.plugins
1471
2186
  });
1472
2187
  summary.validationByScope[scope] = validation;
1473
- if (!validation.ok) {
1474
- const details = [...validation.missing, ...validation.invalid].map((problem) => `- ${problem.message}`).join("\n");
1475
- throw new Error(`Treeseed config validation failed for ${scope}:
1476
- ${details}`);
1477
- }
1478
2188
  if (checkConnections) {
2189
+ progress(`Checking provider connectivity for ${scope}...`);
1479
2190
  summary.connectionChecks.push(checkTreeseedProviderConnections({ tenantRoot, scope, env }));
1480
2191
  }
1481
2192
  }
1482
- writeTreeseedLocalEnvironmentFiles(tenantRoot);
2193
+ for (const scope of scopes) {
2194
+ summary.readinessByScope[scope] = summarizePersistentReadiness(
2195
+ tenantRoot,
2196
+ scope,
2197
+ summary.validationByScope[scope],
2198
+ summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
2199
+ );
2200
+ }
2201
+ const invalidScopes = scopes.filter((scope) => summary.validationByScope[scope]?.ok !== true);
2202
+ if (invalidScopes.length > 0) {
2203
+ throw new Error(formatTreeseedConfigValidationFailure(summary.validationByScope, scopes));
2204
+ }
2205
+ progress("Syncing managed service settings from treeseed.site.yaml...");
1483
2206
  syncManagedServiceSettingsFromDeployConfig(tenantRoot);
1484
2207
  if (initializePersistent) {
1485
2208
  for (const scope of scopes) {
1486
2209
  if (scope === "local") {
1487
2210
  continue;
1488
2211
  }
2212
+ progress(`Initializing persistent ${scope} environment resources...`);
1489
2213
  applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
1490
2214
  const initialized = initializeTreeseedPersistentEnvironment({ tenantRoot, scope });
1491
2215
  summary.initialized.push({
@@ -1493,16 +2217,18 @@ ${details}`);
1493
2217
  secrets: initialized.secrets.length,
1494
2218
  target: initialized.summary.target
1495
2219
  });
1496
- markManagedServicesInitialized(tenantRoot, { scope });
1497
2220
  }
1498
2221
  }
1499
2222
  if (sync === "github" || sync === "all") {
2223
+ progress(`Syncing GitHub environment for ${scopes.at(-1) ?? "prod"}...`);
1500
2224
  summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1501
2225
  }
1502
2226
  if (sync === "cloudflare" || sync === "all") {
2227
+ progress(`Syncing Cloudflare environment for ${scopes.at(-1) ?? "prod"}...`);
1503
2228
  summary.synced.cloudflare = syncTreeseedCloudflareEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1504
2229
  }
1505
2230
  if (sync === "railway" || sync === "all") {
2231
+ progress(`Syncing Railway environment for ${scopes.at(-1) ?? "prod"}...`);
1506
2232
  summary.synced.railway = syncTreeseedRailwayEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
1507
2233
  }
1508
2234
  for (const scope of scopes) {
@@ -1512,11 +2238,6 @@ ${details}`);
1512
2238
  summary.validationByScope[scope],
1513
2239
  summary.connectionChecks.find((report) => report.scope === scope)?.checks ?? []
1514
2240
  );
1515
- if (scope !== "local" && summary.readinessByScope[scope].deployable !== true) {
1516
- throw new Error(
1517
- `Treeseed config readiness failed for ${scope}: configuration is not deployable.`
1518
- );
1519
- }
1520
2241
  }
1521
2242
  return summary;
1522
2243
  }
@@ -1548,7 +2269,9 @@ export {
1548
2269
  DEFAULT_TEMPLATE_CATALOG_URL,
1549
2270
  DEFAULT_TREESEED_API_BASE_URL,
1550
2271
  TREESEED_API_BASE_URL_ENV,
2272
+ TREESEED_MACHINE_KEY_PASSPHRASE_ENV2 as TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
1551
2273
  TREESEED_TEMPLATE_CATALOG_URL_ENV,
2274
+ TreeseedKeyAgentError2 as TreeseedKeyAgentError,
1552
2275
  applyTreeseedConfigValues,
1553
2276
  applyTreeseedEnvironmentToProcess,
1554
2277
  applyTreeseedSafeRepairs,
@@ -1559,31 +2282,44 @@ export {
1559
2282
  collectTreeseedConfigSeedValues,
1560
2283
  collectTreeseedEnvironmentContext,
1561
2284
  collectTreeseedPrintEnvReport,
2285
+ configGroupRank,
1562
2286
  createDefaultTreeseedMachineConfig,
1563
2287
  ensureTreeseedActVerificationTooling,
1564
2288
  ensureTreeseedGitignoreEntries,
2289
+ ensureTreeseedSecretSessionForConfig,
1565
2290
  finalizeTreeseedConfig,
1566
2291
  formatTreeseedConfigEnvironmentReport,
1567
2292
  formatTreeseedProviderConnectionReport,
1568
2293
  getTreeseedMachineConfigPaths,
1569
2294
  getTreeseedRemoteAuthPaths,
1570
2295
  initializeTreeseedPersistentEnvironment,
2296
+ inspectTreeseedKeyAgentStatus,
2297
+ listDeprecatedTreeseedLocalEnvFiles,
1571
2298
  listRelevantTreeseedConfigEntries,
1572
2299
  loadTreeseedMachineConfig,
1573
2300
  loadTreeseedRemoteAuthState,
2301
+ lockTreeseedSecretSession,
2302
+ migrateTreeseedMachineKeyToWrapped,
2303
+ resolveTreeseedLaunchEnvironment,
1574
2304
  resolveTreeseedMachineEnvironmentValues,
1575
2305
  resolveTreeseedRemoteConfig,
1576
2306
  resolveTreeseedRemoteSession,
1577
2307
  resolveTreeseedTemplateCatalogCachePath,
1578
2308
  resolveTreeseedTemplateCatalogEndpoint,
1579
2309
  rotateTreeseedMachineKey,
2310
+ rotateTreeseedMachineKeyPassphrase,
1580
2311
  setTreeseedMachineEnvironmentValue,
1581
2312
  setTreeseedRemoteSession,
1582
2313
  syncTreeseedCloudflareEnvironment,
1583
2314
  syncTreeseedGitHubEnvironment,
1584
2315
  syncTreeseedRailwayEnvironment,
2316
+ unlockTreeseedSecretSessionFromEnv,
2317
+ unlockTreeseedSecretSessionInteractive,
2318
+ unlockTreeseedSecretSessionWithPassphrase,
2319
+ updateTreeseedDeployConfigFeatureToggles,
1585
2320
  validateTreeseedCommandEnvironment,
1586
- writeTreeseedLocalEnvironmentFiles,
2321
+ warnDeprecatedTreeseedLocalEnvFiles,
2322
+ withTreeseedKeyAgentAutopromptDisabled,
1587
2323
  writeTreeseedMachineConfig,
1588
2324
  writeTreeseedRemoteAuthState
1589
2325
  };