@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.
Files changed (66) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +46 -0
  3. package/dist/operations/providers/default.js +1 -1
  4. package/dist/operations/services/config-runtime.d.ts +49 -42
  5. package/dist/operations/services/config-runtime.js +449 -136
  6. package/dist/operations/services/deploy.d.ts +298 -0
  7. package/dist/operations/services/deploy.js +381 -137
  8. package/dist/operations/services/git-workflow.d.ts +9 -0
  9. package/dist/operations/services/git-workflow.js +32 -0
  10. package/dist/operations/services/github-api.d.ts +115 -0
  11. package/dist/operations/services/github-api.js +455 -0
  12. package/dist/operations/services/github-automation.d.ts +19 -33
  13. package/dist/operations/services/github-automation.js +44 -131
  14. package/dist/operations/services/key-agent.d.ts +20 -1
  15. package/dist/operations/services/key-agent.js +267 -102
  16. package/dist/operations/services/knowledge-coop-launch.d.ts +2 -3
  17. package/dist/operations/services/knowledge-coop-launch.js +26 -12
  18. package/dist/operations/services/project-platform.d.ts +157 -150
  19. package/dist/operations/services/project-platform.js +129 -26
  20. package/dist/operations/services/railway-api.d.ts +244 -0
  21. package/dist/operations/services/railway-api.js +882 -0
  22. package/dist/operations/services/railway-deploy.d.ts +171 -27
  23. package/dist/operations/services/railway-deploy.js +672 -172
  24. package/dist/operations/services/runtime-tools.d.ts +18 -0
  25. package/dist/operations/services/runtime-tools.js +19 -6
  26. package/dist/operations/services/workspace-preflight.js +2 -2
  27. package/dist/platform/contracts.d.ts +7 -0
  28. package/dist/platform/deploy-config.js +23 -0
  29. package/dist/platform/deploy-runtime.d.ts +1 -0
  30. package/dist/platform/deploy-runtime.js +7 -9
  31. package/dist/platform/env.yaml +10 -9
  32. package/dist/platform/environment.js +4 -0
  33. package/dist/platform/plugin.d.ts +6 -0
  34. package/dist/platform/plugins/constants.d.ts +1 -0
  35. package/dist/platform/plugins/constants.js +1 -0
  36. package/dist/platform/plugins/runtime.d.ts +4 -0
  37. package/dist/platform/plugins/runtime.js +8 -1
  38. package/dist/platform/published-content.js +27 -4
  39. package/dist/platform/tenant/runtime-config.js +33 -24
  40. package/dist/plugin-default.d.ts +1 -0
  41. package/dist/plugin-default.js +1 -0
  42. package/dist/reconcile/builtin-adapters.d.ts +3 -0
  43. package/dist/reconcile/builtin-adapters.js +2093 -0
  44. package/dist/reconcile/contracts.d.ts +155 -0
  45. package/dist/reconcile/contracts.js +0 -0
  46. package/dist/reconcile/desired-state.d.ts +179 -0
  47. package/dist/reconcile/desired-state.js +319 -0
  48. package/dist/reconcile/engine.d.ts +405 -0
  49. package/dist/reconcile/engine.js +356 -0
  50. package/dist/reconcile/errors.d.ts +5 -0
  51. package/dist/reconcile/errors.js +13 -0
  52. package/dist/reconcile/index.d.ts +7 -0
  53. package/dist/reconcile/index.js +7 -0
  54. package/dist/reconcile/registry.d.ts +7 -0
  55. package/dist/reconcile/registry.js +64 -0
  56. package/dist/reconcile/state.d.ts +7 -0
  57. package/dist/reconcile/state.js +303 -0
  58. package/dist/reconcile/units.d.ts +6 -0
  59. package/dist/reconcile/units.js +68 -0
  60. package/dist/scripts/config-treeseed.js +27 -19
  61. package/dist/scripts/tenant-deploy.js +35 -14
  62. package/dist/workflow/operations.js +127 -22
  63. package/dist/workflow-support.d.ts +3 -1
  64. package/dist/workflow-support.js +50 -0
  65. package/dist/workflow.d.ts +2 -0
  66. 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, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
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, join, resolve } from "node:path";
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 ensureParent(filePath) {
33
- mkdirSync(dirname(filePath), { recursive: true });
33
+ function ensureDirectory(path, mode = 448) {
34
+ mkdirSync(path, { recursive: true, mode });
35
+ chmodSync(path, mode);
34
36
  }
35
- function ensureFifo(filePath) {
36
- ensureParent(filePath);
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 responseFifoPath(socketPath) {
53
- return join(mkdtempSync(resolve(dirname(socketPath), "key-agent-response-")), "response.fifo");
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
- process.kill(pid, 0);
78
- return true;
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 false;
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
- socketPath: resolve(homeRoot, KEY_AGENT_SOCKET_RELATIVE_PATH)
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: readWrappedMachineKeyFile(command.keyPath).wrapped !== null,
281
- migrationRequired: readWrappedMachineKeyFile(command.keyPath).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: readWrappedMachineKeyFile(command.keyPath).wrapped !== null,
297
- migrationRequired: readWrappedMachineKeyFile(command.keyPath).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
- if (!agentProcessAlive(command.socketPath) || !existsSync(command.socketPath)) {
342
- throw new TreeseedKeyAgentError("daemon_unavailable", "Treeseed key-agent is not running.");
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
- return agentProcessAlive(socketPath) && existsSync(socketPath);
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
- try {
363
- if (await socketAlreadyServed(socketPath)) {
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
- ensureFifo(socketPath);
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
- process.on("exit", () => {
526
+ const cleanup = () => {
389
527
  try {
390
528
  rmSync(socketPath, { force: true });
391
529
  clearPidFile(socketPath);
392
530
  } catch {
393
531
  }
394
- });
395
- for (; ; ) {
396
- const line = readFileSync(socketPath, "utf8").trim();
397
- if (!line) {
398
- continue;
399
- }
400
- try {
401
- const parsed = JSON.parse(line);
402
- const response = handleTreeseedKeyAgentCommand(parsed, session);
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
- } catch (error) {
408
- const fallback = fail(error, {
409
- command: "status",
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
- if (parsed.responsePath) {
417
- writeFileSync(parsed.responsePath, `${JSON.stringify(fallback)}
418
- `, "utf8");
419
- }
420
- } catch {
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
- response.status ? { status: response.status } : void 0
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 { provisionCloudflareResources, runRemoteD1Migrations, syncCloudflareSecrets, verifyProvisionedCloudflareResources, markDeploymentInitialized, finalizeDeploymentState } from "./deploy.js";
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 = provisionCloudflareResources(workingRoot, { scope: "staging" });
648
- const prod = provisionCloudflareResources(workingRoot, { scope: "prod" });
649
- syncCloudflareSecrets(workingRoot, { scope: "prod" });
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
- markDeploymentInitialized(workingRoot, { scope: "staging" });
652
- const verification = verifyProvisionedCloudflareResources(workingRoot, { scope: "prod" });
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 = services.map((service) => deployRailwayService(workingRoot, service));
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 });