@treeseed/sdk 0.5.3 → 0.6.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/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/operations/providers/default.js +1 -1
- package/dist/operations/services/config-runtime.d.ts +49 -42
- package/dist/operations/services/config-runtime.js +449 -136
- package/dist/operations/services/deploy.d.ts +298 -0
- package/dist/operations/services/deploy.js +381 -137
- package/dist/operations/services/git-workflow.d.ts +9 -0
- package/dist/operations/services/git-workflow.js +32 -0
- package/dist/operations/services/github-api.d.ts +115 -0
- package/dist/operations/services/github-api.js +455 -0
- package/dist/operations/services/github-automation.d.ts +19 -33
- package/dist/operations/services/github-automation.js +44 -131
- package/dist/operations/services/key-agent.d.ts +20 -1
- package/dist/operations/services/key-agent.js +267 -102
- package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
- package/dist/operations/services/knowledge-coop-launch.js +26 -12
- package/dist/operations/services/project-platform.d.ts +157 -150
- package/dist/operations/services/project-platform.js +129 -26
- package/dist/operations/services/railway-api.d.ts +244 -0
- package/dist/operations/services/railway-api.js +882 -0
- package/dist/operations/services/railway-deploy.d.ts +171 -27
- package/dist/operations/services/railway-deploy.js +672 -172
- package/dist/operations/services/runtime-tools.d.ts +18 -0
- package/dist/operations/services/runtime-tools.js +19 -6
- package/dist/operations/services/workspace-preflight.js +2 -2
- package/dist/platform/contracts.d.ts +7 -0
- package/dist/platform/deploy-config.js +23 -0
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +7 -9
- package/dist/platform/env.yaml +10 -9
- package/dist/platform/environment.js +4 -0
- package/dist/platform/plugin.d.ts +6 -0
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +1 -0
- package/dist/platform/plugins/runtime.d.ts +4 -0
- package/dist/platform/plugins/runtime.js +8 -1
- package/dist/platform/published-content.js +27 -4
- package/dist/platform/tenant/runtime-config.js +33 -24
- package/dist/plugin-default.d.ts +1 -0
- package/dist/plugin-default.js +1 -0
- package/dist/reconcile/builtin-adapters.d.ts +3 -0
- package/dist/reconcile/builtin-adapters.js +2093 -0
- package/dist/reconcile/contracts.d.ts +155 -0
- package/dist/reconcile/contracts.js +0 -0
- package/dist/reconcile/desired-state.d.ts +179 -0
- package/dist/reconcile/desired-state.js +319 -0
- package/dist/reconcile/engine.d.ts +405 -0
- package/dist/reconcile/engine.js +356 -0
- package/dist/reconcile/errors.d.ts +5 -0
- package/dist/reconcile/errors.js +13 -0
- package/dist/reconcile/index.d.ts +7 -0
- package/dist/reconcile/index.js +7 -0
- package/dist/reconcile/registry.d.ts +7 -0
- package/dist/reconcile/registry.js +64 -0
- package/dist/reconcile/state.d.ts +7 -0
- package/dist/reconcile/state.js +303 -0
- package/dist/reconcile/units.d.ts +6 -0
- package/dist/reconcile/units.js +68 -0
- package/dist/scripts/config-treeseed.js +27 -19
- package/dist/scripts/tenant-deploy.js +35 -14
- package/dist/workflow/operations.js +127 -22
- package/dist/workflow-support.d.ts +3 -1
- package/dist/workflow-support.js +50 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +7 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { existsSync,
|
|
2
|
+
import { accessSync, chmodSync, constants as fsConstants, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
|
-
import { dirname,
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { createConnection, createServer } from "node:net";
|
|
6
6
|
const TRESEED_MACHINE_KEY_PASSPHRASE_ENV = "TREESEED_KEY_PASSPHRASE";
|
|
7
7
|
const TRESEED_KEY_AGENT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
8
8
|
const TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS = TRESEED_KEY_AGENT_IDLE_TIMEOUT_MS;
|
|
@@ -10,6 +10,7 @@ const TRESEED_WRAPPED_MACHINE_KEY_VERSION = 2;
|
|
|
10
10
|
const TRESEED_WRAPPED_MACHINE_KEY_KIND = "treeseed-wrapped-machine-key";
|
|
11
11
|
const TREESEED_MACHINE_KEY_PASSPHRASE_ENV = TRESEED_MACHINE_KEY_PASSPHRASE_ENV;
|
|
12
12
|
const KEY_AGENT_SOCKET_RELATIVE_PATH = ".treeseed/run/key-agent.sock";
|
|
13
|
+
const KEY_AGENT_REQUEST_TIMEOUT_MS = 3e3;
|
|
13
14
|
const WRAPPED_KEY_KDF_PARAMS = {
|
|
14
15
|
N: 1 << 14,
|
|
15
16
|
r: 8,
|
|
@@ -29,55 +30,37 @@ class TreeseedKeyAgentError extends Error {
|
|
|
29
30
|
function nowIso() {
|
|
30
31
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
31
32
|
}
|
|
32
|
-
function
|
|
33
|
-
mkdirSync(
|
|
33
|
+
function ensureDirectory(path, mode = 448) {
|
|
34
|
+
mkdirSync(path, { recursive: true, mode });
|
|
35
|
+
chmodSync(path, mode);
|
|
34
36
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
if (existsSync(filePath)) {
|
|
38
|
-
const stats = statSync(filePath);
|
|
39
|
-
if (stats.isFIFO()) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
rmSync(filePath, { force: true });
|
|
43
|
-
}
|
|
44
|
-
const result = spawnSync("mkfifo", [filePath], { stdio: "pipe", encoding: "utf8" });
|
|
45
|
-
if (result.status !== 0) {
|
|
46
|
-
throw new Error(result.stderr?.trim() || `Unable to create FIFO ${filePath}.`);
|
|
47
|
-
}
|
|
37
|
+
function ensureParent(filePath) {
|
|
38
|
+
ensureDirectory(dirname(filePath), 448);
|
|
48
39
|
}
|
|
49
40
|
function pidFilePath(socketPath) {
|
|
50
41
|
return `${socketPath}.pid`;
|
|
51
42
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function writePidFile(socketPath) {
|
|
56
|
-
writeFileSync(pidFilePath(socketPath), `${process.pid}
|
|
57
|
-
`, { mode: 384 });
|
|
58
|
-
}
|
|
59
|
-
function clearPidFile(socketPath) {
|
|
60
|
-
rmSync(pidFilePath(socketPath), { force: true });
|
|
61
|
-
}
|
|
62
|
-
function readAgentPid(socketPath) {
|
|
63
|
-
const pidPath = pidFilePath(socketPath);
|
|
64
|
-
if (!existsSync(pidPath)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const raw = readFileSync(pidPath, "utf8").trim();
|
|
68
|
-
const pid = Number.parseInt(raw, 10);
|
|
69
|
-
return Number.isFinite(pid) ? pid : null;
|
|
70
|
-
}
|
|
71
|
-
function agentProcessAlive(socketPath) {
|
|
72
|
-
const pid = readAgentPid(socketPath);
|
|
73
|
-
if (!pid) {
|
|
74
|
-
return false;
|
|
43
|
+
function detectSocketKind(socketPath) {
|
|
44
|
+
if (!existsSync(socketPath)) {
|
|
45
|
+
return "missing";
|
|
75
46
|
}
|
|
76
47
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
const stats = lstatSync(socketPath);
|
|
49
|
+
if (stats.isSocket()) {
|
|
50
|
+
return "socket";
|
|
51
|
+
}
|
|
52
|
+
if (stats.isFIFO()) {
|
|
53
|
+
return "fifo";
|
|
54
|
+
}
|
|
55
|
+
if (stats.isFile()) {
|
|
56
|
+
return "file";
|
|
57
|
+
}
|
|
58
|
+
if (stats.isDirectory()) {
|
|
59
|
+
return "directory";
|
|
60
|
+
}
|
|
61
|
+
return "other";
|
|
79
62
|
} catch {
|
|
80
|
-
return
|
|
63
|
+
return "other";
|
|
81
64
|
}
|
|
82
65
|
}
|
|
83
66
|
function createFingerprint(machineKey) {
|
|
@@ -199,21 +182,177 @@ function replaceWrappedMachineKey(keyPath, machineKey, passphrase) {
|
|
|
199
182
|
writeWrappedMachineKeyFile(keyPath, payload);
|
|
200
183
|
return payload;
|
|
201
184
|
}
|
|
185
|
+
function resolveRuntimeRoot() {
|
|
186
|
+
const xdgRuntimeDir = process.env.XDG_RUNTIME_DIR;
|
|
187
|
+
if (typeof xdgRuntimeDir === "string" && xdgRuntimeDir.trim().length > 0) {
|
|
188
|
+
try {
|
|
189
|
+
accessSync(xdgRuntimeDir, fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK);
|
|
190
|
+
return resolve(xdgRuntimeDir, "treeseed");
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const homeRoot = process.env.HOME && process.env.HOME.trim().length > 0 ? process.env.HOME : homedir();
|
|
195
|
+
return resolve(homeRoot, dirname(KEY_AGENT_SOCKET_RELATIVE_PATH));
|
|
196
|
+
}
|
|
202
197
|
function getTreeseedKeyAgentPaths() {
|
|
203
198
|
const homeRoot = process.env.HOME && process.env.HOME.trim().length > 0 ? process.env.HOME : homedir();
|
|
199
|
+
const runtimeRoot = resolveRuntimeRoot();
|
|
200
|
+
const socketPath = runtimeRoot === resolve(homeRoot, dirname(KEY_AGENT_SOCKET_RELATIVE_PATH)) ? resolve(homeRoot, KEY_AGENT_SOCKET_RELATIVE_PATH) : resolve(runtimeRoot, "key-agent.sock");
|
|
204
201
|
return {
|
|
205
202
|
homeRoot,
|
|
206
|
-
|
|
203
|
+
runtimeRoot,
|
|
204
|
+
socketPath,
|
|
205
|
+
pidPath: pidFilePath(socketPath)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function readAgentPid(socketPath) {
|
|
209
|
+
const pidPath = pidFilePath(socketPath);
|
|
210
|
+
if (!existsSync(pidPath)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const raw = readFileSync(pidPath, "utf8").trim();
|
|
214
|
+
const pid = Number.parseInt(raw, 10);
|
|
215
|
+
return Number.isFinite(pid) ? pid : null;
|
|
216
|
+
}
|
|
217
|
+
function writePidFile(socketPath) {
|
|
218
|
+
ensureParent(socketPath);
|
|
219
|
+
writeFileSync(pidFilePath(socketPath), `${process.pid}
|
|
220
|
+
`, { mode: 384 });
|
|
221
|
+
}
|
|
222
|
+
function clearPidFile(socketPath) {
|
|
223
|
+
rmSync(pidFilePath(socketPath), { force: true });
|
|
224
|
+
}
|
|
225
|
+
function classifySocketError(error) {
|
|
226
|
+
const errno = "code" in error ? error.code : void 0;
|
|
227
|
+
if (errno === "ENOENT") {
|
|
228
|
+
return {
|
|
229
|
+
code: "daemon_unavailable",
|
|
230
|
+
message: "Treeseed key-agent socket is missing."
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (errno === "EACCES" || errno === "EPERM") {
|
|
234
|
+
return {
|
|
235
|
+
code: "permission_denied",
|
|
236
|
+
message: "Permission denied while connecting to the Treeseed key-agent socket."
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (errno === "ECONNREFUSED" || errno === "ECONNRESET") {
|
|
240
|
+
return {
|
|
241
|
+
code: "daemon_unavailable",
|
|
242
|
+
message: "Treeseed key-agent daemon is not accepting connections."
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
code: "protocol_error",
|
|
247
|
+
message: error.message || "Treeseed key-agent request failed."
|
|
207
248
|
};
|
|
208
249
|
}
|
|
250
|
+
async function requestTreeseedKeyAgentOverSocket(command, timeoutMs = KEY_AGENT_REQUEST_TIMEOUT_MS) {
|
|
251
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
252
|
+
const socket = createConnection(command.socketPath);
|
|
253
|
+
let responseBuffer = "";
|
|
254
|
+
let settled = false;
|
|
255
|
+
const finalize = (callback) => {
|
|
256
|
+
if (settled) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
settled = true;
|
|
260
|
+
clearTimeout(timeoutHandle);
|
|
261
|
+
socket.removeAllListeners();
|
|
262
|
+
callback();
|
|
263
|
+
};
|
|
264
|
+
const timeoutHandle = setTimeout(() => {
|
|
265
|
+
socket.destroy();
|
|
266
|
+
rejectPromise(new TreeseedKeyAgentError("daemon_unavailable", "Timed out waiting for the Treeseed key-agent response.", {
|
|
267
|
+
socketPath: command.socketPath
|
|
268
|
+
}));
|
|
269
|
+
}, timeoutMs);
|
|
270
|
+
socket.setEncoding("utf8");
|
|
271
|
+
socket.on("connect", () => {
|
|
272
|
+
socket.write(`${JSON.stringify(command)}
|
|
273
|
+
`);
|
|
274
|
+
});
|
|
275
|
+
socket.on("data", (chunk) => {
|
|
276
|
+
responseBuffer += chunk;
|
|
277
|
+
const newlineIndex = responseBuffer.indexOf("\n");
|
|
278
|
+
if (newlineIndex === -1) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const payload = responseBuffer.slice(0, newlineIndex).trim();
|
|
282
|
+
socket.end();
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(payload || "{}");
|
|
285
|
+
finalize(() => resolvePromise(parsed));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
finalize(() => rejectPromise(new TreeseedKeyAgentError("protocol_error", "Treeseed key-agent returned an invalid JSON response.", {
|
|
288
|
+
socketPath: command.socketPath,
|
|
289
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
290
|
+
})));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
socket.on("error", (error) => {
|
|
294
|
+
const classified = classifySocketError(error);
|
|
295
|
+
finalize(() => rejectPromise(new TreeseedKeyAgentError(classified.code, classified.message, {
|
|
296
|
+
socketPath: command.socketPath,
|
|
297
|
+
cause: error.message
|
|
298
|
+
})));
|
|
299
|
+
});
|
|
300
|
+
socket.on("end", () => {
|
|
301
|
+
if (!settled && responseBuffer.trim().length === 0) {
|
|
302
|
+
finalize(() => rejectPromise(new TreeseedKeyAgentError("protocol_error", "Treeseed key-agent closed the connection without returning a response.", {
|
|
303
|
+
socketPath: command.socketPath
|
|
304
|
+
})));
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function inspectTreeseedKeyAgentDiagnostics(socketPath) {
|
|
310
|
+
const socketKind = detectSocketKind(socketPath);
|
|
311
|
+
const diagnostics = {
|
|
312
|
+
socketPath,
|
|
313
|
+
pidPath: pidFilePath(socketPath),
|
|
314
|
+
socketPresent: socketKind !== "missing",
|
|
315
|
+
socketKind,
|
|
316
|
+
socketConnectable: false,
|
|
317
|
+
healthOk: false,
|
|
318
|
+
daemonPid: readAgentPid(socketPath),
|
|
319
|
+
lastError: null
|
|
320
|
+
};
|
|
321
|
+
if (!diagnostics.socketPresent) {
|
|
322
|
+
diagnostics.lastError = "socket_missing";
|
|
323
|
+
return diagnostics;
|
|
324
|
+
}
|
|
325
|
+
if (diagnostics.socketKind !== "socket") {
|
|
326
|
+
diagnostics.lastError = `stale_transport_${diagnostics.socketKind}`;
|
|
327
|
+
return diagnostics;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const response = await requestTreeseedKeyAgentOverSocket({
|
|
331
|
+
command: "health",
|
|
332
|
+
keyPath: "",
|
|
333
|
+
socketPath,
|
|
334
|
+
idleTimeoutMs: TREESEED_KEY_AGENT_IDLE_TIMEOUT_MS
|
|
335
|
+
});
|
|
336
|
+
diagnostics.socketConnectable = true;
|
|
337
|
+
diagnostics.healthOk = response.ok;
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
diagnostics.lastError = response.message ?? response.code ?? "health_failed";
|
|
340
|
+
}
|
|
341
|
+
return diagnostics;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const classified = classifySocketError(error instanceof Error ? error : new Error(String(error)));
|
|
344
|
+
diagnostics.lastError = classified.message;
|
|
345
|
+
return diagnostics;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
209
348
|
function createStatus(command, session) {
|
|
210
|
-
const wrapped = readWrappedMachineKeyFile(command.keyPath);
|
|
349
|
+
const wrapped = command.keyPath ? readWrappedMachineKeyFile(command.keyPath) : { exists: false, wrapped: null, migrationRequired: false };
|
|
211
350
|
const idleRemainingMs = session.machineKey ? Math.max(0, session.idleTimeoutMs - (Date.now() - session.lastTouchedAt)) : 0;
|
|
212
351
|
return {
|
|
213
352
|
running: true,
|
|
214
353
|
unlocked: Boolean(session.machineKey) && idleRemainingMs > 0,
|
|
215
354
|
wrappedKeyPresent: wrapped.exists && Boolean(wrapped.wrapped),
|
|
216
|
-
migrationRequired: wrapped.migrationRequired,
|
|
355
|
+
migrationRequired: Boolean(wrapped.migrationRequired),
|
|
217
356
|
keyPath: command.keyPath,
|
|
218
357
|
socketPath: command.socketPath,
|
|
219
358
|
idleTimeoutMs: session.idleTimeoutMs,
|
|
@@ -269,6 +408,7 @@ function ok(response = {}) {
|
|
|
269
408
|
return { ok: true, ...response };
|
|
270
409
|
}
|
|
271
410
|
function fail(error, command) {
|
|
411
|
+
const wrappedState = command.keyPath ? readWrappedMachineKeyFile(command.keyPath) : { wrapped: null, migrationRequired: false };
|
|
272
412
|
if (error instanceof TreeseedKeyAgentError) {
|
|
273
413
|
return {
|
|
274
414
|
ok: false,
|
|
@@ -277,8 +417,8 @@ function fail(error, command) {
|
|
|
277
417
|
status: {
|
|
278
418
|
running: true,
|
|
279
419
|
unlocked: false,
|
|
280
|
-
wrappedKeyPresent:
|
|
281
|
-
migrationRequired:
|
|
420
|
+
wrappedKeyPresent: wrappedState.wrapped !== null,
|
|
421
|
+
migrationRequired: Boolean(wrappedState.migrationRequired),
|
|
282
422
|
keyPath: command.keyPath,
|
|
283
423
|
socketPath: command.socketPath,
|
|
284
424
|
idleTimeoutMs: command.idleTimeoutMs,
|
|
@@ -293,8 +433,8 @@ function fail(error, command) {
|
|
|
293
433
|
status: {
|
|
294
434
|
running: true,
|
|
295
435
|
unlocked: false,
|
|
296
|
-
wrappedKeyPresent:
|
|
297
|
-
migrationRequired:
|
|
436
|
+
wrappedKeyPresent: wrappedState.wrapped !== null,
|
|
437
|
+
migrationRequired: Boolean(wrappedState.migrationRequired),
|
|
298
438
|
keyPath: command.keyPath,
|
|
299
439
|
socketPath: command.socketPath,
|
|
300
440
|
idleTimeoutMs: command.idleTimeoutMs,
|
|
@@ -304,6 +444,11 @@ function fail(error, command) {
|
|
|
304
444
|
}
|
|
305
445
|
function handleTreeseedKeyAgentCommand(command, session) {
|
|
306
446
|
maybeExpireSession(session);
|
|
447
|
+
if (command.command === "health") {
|
|
448
|
+
return ok({
|
|
449
|
+
status: createStatus(command, session)
|
|
450
|
+
});
|
|
451
|
+
}
|
|
307
452
|
if (command.command === "status") {
|
|
308
453
|
return ok({ status: createStatus(command, session) });
|
|
309
454
|
}
|
|
@@ -338,39 +483,33 @@ function handleTreeseedKeyAgentCommand(command, session) {
|
|
|
338
483
|
});
|
|
339
484
|
}
|
|
340
485
|
async function requestTreeseedKeyAgent(command) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const responsePath = responseFifoPath(command.socketPath);
|
|
345
|
-
ensureFifo(responsePath);
|
|
346
|
-
try {
|
|
347
|
-
const responsePromise = Promise.resolve().then(() => readFileSync(responsePath, "utf8"));
|
|
348
|
-
writeFileSync(command.socketPath, `${JSON.stringify({ ...command, responsePath })}
|
|
349
|
-
`, "utf8");
|
|
350
|
-
return JSON.parse((await responsePromise).trim() || "{}");
|
|
351
|
-
} finally {
|
|
352
|
-
rmSync(dirname(responsePath), { recursive: true, force: true });
|
|
486
|
+
const diagnostics = await inspectTreeseedKeyAgentDiagnostics(command.socketPath);
|
|
487
|
+
if (!diagnostics.healthOk && command.command !== "health") {
|
|
488
|
+
throw new TreeseedKeyAgentError("daemon_unavailable", diagnostics.lastError ?? "Treeseed key-agent is not running.", { diagnostics });
|
|
353
489
|
}
|
|
490
|
+
return requestTreeseedKeyAgentOverSocket(command);
|
|
354
491
|
}
|
|
355
492
|
async function socketAlreadyServed(socketPath) {
|
|
356
|
-
|
|
493
|
+
const diagnostics = await inspectTreeseedKeyAgentDiagnostics(socketPath);
|
|
494
|
+
return diagnostics.socketConnectable && diagnostics.healthOk;
|
|
357
495
|
}
|
|
358
496
|
async function removeStaleSocket(socketPath) {
|
|
359
497
|
if (!existsSync(socketPath)) {
|
|
360
498
|
return true;
|
|
361
499
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
rmSync(socketPath, { force: true });
|
|
367
|
-
clearPidFile(socketPath);
|
|
368
|
-
return true;
|
|
369
|
-
} catch {
|
|
500
|
+
const socketKind = detectSocketKind(socketPath);
|
|
501
|
+
if (socketKind !== "socket") {
|
|
370
502
|
rmSync(socketPath, { force: true });
|
|
371
503
|
clearPidFile(socketPath);
|
|
372
504
|
return true;
|
|
373
505
|
}
|
|
506
|
+
const diagnostics = await inspectTreeseedKeyAgentDiagnostics(socketPath);
|
|
507
|
+
if (diagnostics.socketConnectable && diagnostics.healthOk) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
rmSync(socketPath, { force: true });
|
|
511
|
+
clearPidFile(socketPath);
|
|
512
|
+
return true;
|
|
374
513
|
}
|
|
375
514
|
async function startTreeseedKeyAgentServer(options) {
|
|
376
515
|
const socketPath = options.socketPath ?? getTreeseedKeyAgentPaths().socketPath;
|
|
@@ -378,49 +517,71 @@ async function startTreeseedKeyAgentServer(options) {
|
|
|
378
517
|
if (!canStart) {
|
|
379
518
|
return;
|
|
380
519
|
}
|
|
381
|
-
|
|
382
|
-
writePidFile(socketPath);
|
|
520
|
+
ensureParent(socketPath);
|
|
383
521
|
const session = {
|
|
384
522
|
machineKey: null,
|
|
385
523
|
lastTouchedAt: 0,
|
|
386
524
|
idleTimeoutMs: options.idleTimeoutMs ?? TRESEED_KEY_AGENT_IDLE_TIMEOUT_MS
|
|
387
525
|
};
|
|
388
|
-
|
|
526
|
+
const cleanup = () => {
|
|
389
527
|
try {
|
|
390
528
|
rmSync(socketPath, { force: true });
|
|
391
529
|
clearPidFile(socketPath);
|
|
392
530
|
} catch {
|
|
393
531
|
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (parsed.responsePath) {
|
|
404
|
-
writeFileSync(parsed.responsePath, `${JSON.stringify(response)}
|
|
405
|
-
`, "utf8");
|
|
532
|
+
};
|
|
533
|
+
const server = createServer((connection) => {
|
|
534
|
+
let requestBuffer = "";
|
|
535
|
+
connection.setEncoding("utf8");
|
|
536
|
+
connection.on("data", (chunk) => {
|
|
537
|
+
requestBuffer += chunk;
|
|
538
|
+
const newlineIndex = requestBuffer.indexOf("\n");
|
|
539
|
+
if (newlineIndex === -1) {
|
|
540
|
+
return;
|
|
406
541
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
keyPath: options.keyPath,
|
|
411
|
-
socketPath,
|
|
412
|
-
idleTimeoutMs: options.idleTimeoutMs ?? TRESEED_KEY_AGENT_IDLE_TIMEOUT_MS
|
|
413
|
-
});
|
|
542
|
+
const line = requestBuffer.slice(0, newlineIndex).trim();
|
|
543
|
+
requestBuffer = "";
|
|
544
|
+
let response;
|
|
414
545
|
try {
|
|
415
546
|
const parsed = JSON.parse(line);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
547
|
+
response = handleTreeseedKeyAgentCommand(parsed, session);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
response = fail(error, {
|
|
550
|
+
command: "status",
|
|
551
|
+
keyPath: options.keyPath,
|
|
552
|
+
socketPath,
|
|
553
|
+
idleTimeoutMs: options.idleTimeoutMs ?? TRESEED_KEY_AGENT_IDLE_TIMEOUT_MS
|
|
554
|
+
});
|
|
421
555
|
}
|
|
422
|
-
|
|
423
|
-
|
|
556
|
+
connection.end(`${JSON.stringify(response)}
|
|
557
|
+
`);
|
|
558
|
+
});
|
|
559
|
+
connection.on("error", () => {
|
|
560
|
+
connection.destroy();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
564
|
+
server.once("error", (error) => {
|
|
565
|
+
rejectPromise(error);
|
|
566
|
+
});
|
|
567
|
+
server.listen(socketPath, () => {
|
|
568
|
+
chmodSync(socketPath, 384);
|
|
569
|
+
writePidFile(socketPath);
|
|
570
|
+
resolvePromise();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
process.on("exit", cleanup);
|
|
574
|
+
process.on("SIGINT", () => {
|
|
575
|
+
cleanup();
|
|
576
|
+
process.exit(0);
|
|
577
|
+
});
|
|
578
|
+
process.on("SIGTERM", () => {
|
|
579
|
+
cleanup();
|
|
580
|
+
process.exit(0);
|
|
581
|
+
});
|
|
582
|
+
await new Promise(() => {
|
|
583
|
+
});
|
|
584
|
+
server.close();
|
|
424
585
|
}
|
|
425
586
|
function assertTreeseedKeyAgentResponse(response, fallback = "Treeseed secret session request failed.") {
|
|
426
587
|
if (response.ok) {
|
|
@@ -429,7 +590,10 @@ function assertTreeseedKeyAgentResponse(response, fallback = "Treeseed secret se
|
|
|
429
590
|
throw new TreeseedKeyAgentError(
|
|
430
591
|
response.code ?? "unlock_failed",
|
|
431
592
|
response.message ?? fallback,
|
|
432
|
-
|
|
593
|
+
{
|
|
594
|
+
status: response.status,
|
|
595
|
+
diagnostics: response.diagnostics
|
|
596
|
+
}
|
|
433
597
|
);
|
|
434
598
|
}
|
|
435
599
|
function rotateWrappedMachineKeyPassphrase(keyPath, machineKey, passphrase) {
|
|
@@ -464,6 +628,7 @@ export {
|
|
|
464
628
|
assertTreeseedKeyAgentResponse,
|
|
465
629
|
getTreeseedKeyAgentPaths,
|
|
466
630
|
handleTreeseedKeyAgentCommand,
|
|
631
|
+
inspectTreeseedKeyAgentDiagnostics,
|
|
467
632
|
machineKeysEqual,
|
|
468
633
|
migrateLegacyProjectMachineKeyToWrapped,
|
|
469
634
|
readWrappedMachineKeyFile,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { checkTreeseedProviderConnections } from './config-runtime.ts';
|
|
2
|
-
import { provisionCloudflareResources, verifyProvisionedCloudflareResources } from './deploy.ts';
|
|
3
2
|
import { configuredRailwayServices, deployRailwayService, ensureRailwayScheduledJobs, verifyRailwayScheduledJobs } from './railway-deploy.ts';
|
|
4
3
|
import { buildKnowledgeCoopKnowledgePackPackage, buildKnowledgeCoopTemplatePackage } from './knowledge-coop-packaging.ts';
|
|
5
4
|
export type KnowledgeCoopLaunchFailurePhase = 'repo_provision_failed' | 'content_bootstrap_failed' | 'workflow_bootstrap_failed' | 'hosting_registration_failed' | 'runtime_connection_failed';
|
|
@@ -63,7 +62,7 @@ export interface KnowledgeCoopManagedLaunchResult {
|
|
|
63
62
|
};
|
|
64
63
|
railway: {
|
|
65
64
|
services: ReturnType<typeof configuredRailwayServices>;
|
|
66
|
-
deployments: ReturnType<typeof deployRailwayService
|
|
65
|
+
deployments: Awaited<ReturnType<typeof deployRailwayService>>[];
|
|
67
66
|
schedules: Awaited<ReturnType<typeof ensureRailwayScheduledJobs>>;
|
|
68
67
|
verification: Awaited<ReturnType<typeof verifyRailwayScheduledJobs>>;
|
|
69
68
|
};
|
|
@@ -86,5 +85,5 @@ export interface KnowledgeCoopLaunchPreflightReport {
|
|
|
86
85
|
railway: boolean;
|
|
87
86
|
};
|
|
88
87
|
}
|
|
89
|
-
export declare function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot?: string): KnowledgeCoopLaunchPreflightReport
|
|
88
|
+
export declare function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot?: string): Promise<KnowledgeCoopLaunchPreflightReport>;
|
|
90
89
|
export declare function executeKnowledgeCoopManagedLaunch(input: KnowledgeCoopManagedLaunchInput): Promise<KnowledgeCoopManagedLaunchResult>;
|
|
@@ -3,8 +3,9 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
6
|
+
import { collectTreeseedReconcileStatus, reconcileTreeseedTarget } from "../../reconcile/index.js";
|
|
6
7
|
import { checkTreeseedProviderConnections, collectTreeseedConfigSeedValues } from "./config-runtime.js";
|
|
7
|
-
import {
|
|
8
|
+
import { createPersistentDeployTarget, runRemoteD1Migrations, finalizeDeploymentState } from "./deploy.js";
|
|
8
9
|
import {
|
|
9
10
|
createGitHubRepository,
|
|
10
11
|
ensureGitHubDeployAutomation,
|
|
@@ -572,7 +573,7 @@ function scaffoldKnowledgeCoopSource(projectRoot, input) {
|
|
|
572
573
|
env: templateCatalogEnv
|
|
573
574
|
});
|
|
574
575
|
}
|
|
575
|
-
function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot = process.cwd()) {
|
|
576
|
+
async function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot = process.cwd()) {
|
|
576
577
|
const values = collectTreeseedConfigSeedValues(tenantRoot, "prod", process.env);
|
|
577
578
|
const requiredConfig = [
|
|
578
579
|
["TREESEED_BETTER_AUTH_SECRET"],
|
|
@@ -589,7 +590,7 @@ function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot = process.cw
|
|
|
589
590
|
const value = values[name];
|
|
590
591
|
return typeof value === "string" && value.trim().length > 0;
|
|
591
592
|
})).map((group) => group.join(" or "));
|
|
592
|
-
const providerChecks = checkTreeseedProviderConnections({ tenantRoot, scope: "prod", env: process.env });
|
|
593
|
+
const providerChecks = await checkTreeseedProviderConnections({ tenantRoot, scope: "prod", env: process.env });
|
|
593
594
|
const commands = {
|
|
594
595
|
git: commandAvailable("git"),
|
|
595
596
|
gh: commandAvailable("gh"),
|
|
@@ -606,7 +607,7 @@ function validateKnowledgeCoopManagedLaunchPrerequisites(tenantRoot = process.cw
|
|
|
606
607
|
}
|
|
607
608
|
async function executeKnowledgeCoopManagedLaunch(input) {
|
|
608
609
|
const phases = [];
|
|
609
|
-
const preflight = validateKnowledgeCoopManagedLaunchPrerequisites(process.cwd());
|
|
610
|
+
const preflight = await validateKnowledgeCoopManagedLaunchPrerequisites(process.cwd());
|
|
610
611
|
if (!preflight.ok) {
|
|
611
612
|
throw new KnowledgeCoopLaunchError(
|
|
612
613
|
"runtime_connection_failed",
|
|
@@ -619,7 +620,7 @@ async function executeKnowledgeCoopManagedLaunch(input) {
|
|
|
619
620
|
const repoName = slugify(input.projectSlug, "project");
|
|
620
621
|
try {
|
|
621
622
|
appendPhase(phases, "repo_provision", "running", "Creating GitHub repository.");
|
|
622
|
-
const repository = createGitHubRepository({
|
|
623
|
+
const repository = await createGitHubRepository({
|
|
623
624
|
owner: repoOwner,
|
|
624
625
|
name: repoName,
|
|
625
626
|
description: input.summary ?? `Knowledge Coop hub for ${input.projectName}`,
|
|
@@ -641,15 +642,25 @@ async function executeKnowledgeCoopManagedLaunch(input) {
|
|
|
641
642
|
commitMessage: `Initialize ${input.projectName}`
|
|
642
643
|
});
|
|
643
644
|
pushDefaultWorkstreamBranch(workingRoot);
|
|
644
|
-
const workflows = ensureGitHubDeployAutomation(workingRoot);
|
|
645
|
+
const workflows = await ensureGitHubDeployAutomation(workingRoot);
|
|
645
646
|
appendPhase(phases, "workflow_bootstrap", "completed", "Configured GitHub workflows, secrets, and variables.");
|
|
646
647
|
appendPhase(phases, "hosting_registration", "running", "Provisioning Cloudflare resources and deploy state.");
|
|
647
|
-
const staging =
|
|
648
|
-
|
|
649
|
-
|
|
648
|
+
const staging = await reconcileTreeseedTarget({
|
|
649
|
+
tenantRoot: workingRoot,
|
|
650
|
+
target: createPersistentDeployTarget("staging"),
|
|
651
|
+
env: process.env
|
|
652
|
+
});
|
|
653
|
+
const prod = await reconcileTreeseedTarget({
|
|
654
|
+
tenantRoot: workingRoot,
|
|
655
|
+
target: createPersistentDeployTarget("prod"),
|
|
656
|
+
env: process.env
|
|
657
|
+
});
|
|
650
658
|
runRemoteD1Migrations(workingRoot, { scope: "prod" });
|
|
651
|
-
|
|
652
|
-
|
|
659
|
+
const verification = await collectTreeseedReconcileStatus({
|
|
660
|
+
tenantRoot: workingRoot,
|
|
661
|
+
target: createPersistentDeployTarget("prod"),
|
|
662
|
+
env: process.env
|
|
663
|
+
});
|
|
653
664
|
appendPhase(phases, "hosting_registration", "completed", "Provisioned Cloudflare resources.");
|
|
654
665
|
const launchConfig = loadCliDeployConfig(workingRoot);
|
|
655
666
|
const managedRuntime = launchConfig.runtime?.mode === "treeseed_managed";
|
|
@@ -661,7 +672,10 @@ async function executeKnowledgeCoopManagedLaunch(input) {
|
|
|
661
672
|
appendPhase(phases, "runtime_connection", "running", "Deploying Railway services and registering runtime connectivity.");
|
|
662
673
|
validateRailwayDeployPrerequisites(workingRoot, "prod");
|
|
663
674
|
services = configuredRailwayServices(workingRoot, "prod");
|
|
664
|
-
deployments =
|
|
675
|
+
deployments = [];
|
|
676
|
+
for (const service of services) {
|
|
677
|
+
deployments.push(await deployRailwayService(workingRoot, service));
|
|
678
|
+
}
|
|
665
679
|
schedules = await ensureRailwayScheduledJobs(workingRoot, "prod");
|
|
666
680
|
railwayVerification = await verifyRailwayScheduledJobs(workingRoot, "prod");
|
|
667
681
|
finalizeDeploymentState(workingRoot, { scope: "prod", serviceResults: deployments });
|