@treeseed/sdk 0.4.13 → 0.5.0
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.
- package/dist/control-plane-client.d.ts +60 -1
- package/dist/control-plane-client.js +59 -0
- package/dist/control-plane.d.ts +1 -1
- package/dist/control-plane.js +11 -4
- package/dist/d1-store.d.ts +58 -0
- package/dist/d1-store.js +64 -0
- package/dist/dispatch.js +6 -0
- package/dist/graph/schema.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +32 -0
- package/dist/knowledge-coop.d.ts +223 -0
- package/dist/knowledge-coop.js +82 -0
- package/dist/model-registry.js +79 -0
- package/dist/operations/providers/default.js +126 -7
- package/dist/operations/services/config-runtime.d.ts +102 -24
- package/dist/operations/services/config-runtime.js +896 -160
- package/dist/operations/services/deploy.d.ts +223 -15
- package/dist/operations/services/deploy.js +626 -55
- package/dist/operations/services/github-automation.d.ts +60 -0
- package/dist/operations/services/github-automation.js +138 -0
- package/dist/operations/services/key-agent.d.ts +118 -0
- package/dist/operations/services/key-agent.js +476 -0
- package/dist/operations/services/knowledge-coop-launch.d.ts +90 -0
- package/dist/operations/services/knowledge-coop-launch.js +753 -0
- package/dist/operations/services/knowledge-coop-packaging.d.ts +59 -0
- package/dist/operations/services/knowledge-coop-packaging.js +234 -0
- package/dist/operations/services/local-dev.d.ts +0 -1
- package/dist/operations/services/local-dev.js +1 -14
- package/dist/operations/services/project-platform.d.ts +42 -182
- package/dist/operations/services/project-platform.js +162 -59
- package/dist/operations/services/railway-deploy.d.ts +1 -0
- package/dist/operations/services/railway-deploy.js +31 -13
- package/dist/operations/services/runtime-tools.d.ts +52 -5
- package/dist/operations/services/runtime-tools.js +186 -26
- package/dist/operations/services/watch-dev.js +2 -4
- package/dist/operations/services/workspace-preflight.d.ts +4 -4
- package/dist/operations/services/workspace-preflight.js +22 -20
- package/dist/operations-registry.js +7 -2
- package/dist/platform/contracts.d.ts +39 -3
- package/dist/platform/deploy-config.d.ts +12 -1
- package/dist/platform/deploy-config.js +214 -15
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +10 -2
- package/dist/platform/env.yaml +93 -61
- package/dist/platform/environment.d.ts +13 -2
- package/dist/platform/environment.js +90 -20
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +7 -6
- package/dist/platform/tenant/runtime-config.js +8 -1
- package/dist/platform/tenant-config.js +4 -0
- package/dist/platform/utils/site-config-schema.js +18 -0
- package/dist/plugin-default.js +2 -2
- package/dist/scripts/key-agent.js +165 -0
- package/dist/scripts/tenant-build.js +4 -1
- package/dist/scripts/tenant-check.js +4 -1
- package/dist/scripts/tenant-deploy.js +43 -4
- package/dist/scripts/tenant-dev.js +0 -1
- package/dist/sdk-types.d.ts +2 -2
- package/dist/sdk-types.js +2 -0
- package/dist/sdk.d.ts +13 -0
- package/dist/sdk.js +40 -0
- package/dist/stores/knowledge-coop-store.d.ts +56 -0
- package/dist/stores/knowledge-coop-store.js +482 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +6 -2
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +4 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +25 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +22 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +11 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +17 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +17 -10
- package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +69 -7
- package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +1 -0
- package/dist/workflow/operations.d.ts +98 -0
- package/dist/workflow/operations.js +229 -7
- package/dist/workflow-state.d.ts +54 -2
- package/dist/workflow-state.js +170 -24
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +32 -2
- package/dist/workflow.d.ts +29 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +11 -1
- package/dist/scripts/sync-dev-vars.js +0 -6
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
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
|
|
98
|
-
return
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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 ??
|
|
137
|
-
railway.projectName = deployConfig.services?.api?.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
|
-
|
|
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
|
|
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
|
-
|
|
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 = [".
|
|
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
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
...
|
|
785
|
-
...
|
|
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
|
-
|
|
803
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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", "
|
|
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 :
|
|
1194
|
-
const defaultRootDir = ["api", "manager", "worker", "
|
|
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:
|
|
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 &&
|
|
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:
|
|
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
|
|
1288
|
-
const
|
|
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
|
-
|
|
1291
|
-
|
|
1963
|
+
github: {
|
|
1964
|
+
configured: validConfigValue("GH_TOKEN")
|
|
1292
1965
|
},
|
|
1293
|
-
|
|
1294
|
-
|
|
1966
|
+
cloudflare: {
|
|
1967
|
+
configured: cloudflareReady
|
|
1295
1968
|
},
|
|
1296
1969
|
railway: {
|
|
1297
|
-
|
|
1970
|
+
configured: railwayReady
|
|
1298
1971
|
},
|
|
1299
|
-
|
|
1300
|
-
configured:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2321
|
+
warnDeprecatedTreeseedLocalEnvFiles,
|
|
2322
|
+
withTreeseedKeyAgentAutopromptDisabled,
|
|
1587
2323
|
writeTreeseedMachineConfig,
|
|
1588
2324
|
writeTreeseedRemoteAuthState
|
|
1589
2325
|
};
|