@treeseed/core 0.6.18 → 0.6.19

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/dev.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, resolve, sep } from "node:path";
@@ -7,10 +7,17 @@ import { setTimeout as delay } from "node:timers/promises";
7
7
  import {
8
8
  applyTreeseedEnvironmentToProcess,
9
9
  assertTreeseedCommandEnvironment,
10
+ createPersistentDeployTarget,
11
+ ensureGeneratedWranglerConfig,
10
12
  ensureLocalWorkspaceLinks,
11
13
  findNearestTreeseedWorkspaceRoot,
12
- resolveTreeseedToolBinary
14
+ packageScriptPath,
15
+ resolveTreeseedMachineEnvironmentValues,
16
+ resolveTreeseedToolBinary,
17
+ resolveWranglerBin,
18
+ stopKnownMailpitContainers
13
19
  } from "@treeseed/sdk/workflow-support";
20
+ import { loadTreeseedDeployConfig } from "@treeseed/sdk/platform/deploy-config";
14
21
  import {
15
22
  startPollingWatch
16
23
  } from "./dev-watch.js";
@@ -21,6 +28,9 @@ const TREESEED_DEFAULT_WEB_PORT = 4321;
21
28
  const TREESEED_DEFAULT_API_HOST = "127.0.0.1";
22
29
  const TREESEED_DEFAULT_API_PORT = 3e3;
23
30
  const TREESEED_DEFAULT_MANAGER_PORT = 3100;
31
+ const TREESEED_DEFAULT_MAILPIT_SMTP_HOST = "127.0.0.1";
32
+ const TREESEED_DEFAULT_MAILPIT_SMTP_PORT = 1025;
33
+ const TREESEED_DEFAULT_MAILPIT_UI_PORT = 8025;
24
34
  const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
25
35
  const DEFAULT_READINESS_TIMEOUT_MS = 9e4;
26
36
  const DEFAULT_PROCESS_READY_GRACE_MS = 1200;
@@ -106,12 +116,129 @@ function normalizeFeedbackMode(value) {
106
116
  function normalizeOpenMode(value) {
107
117
  return value ?? "auto";
108
118
  }
119
+ function normalizeLocalRuntimeMode(value) {
120
+ return value === "provider" || value === "local" ? value : "auto";
121
+ }
122
+ function normalizeProvider(value, fallback = "local") {
123
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
124
+ }
125
+ function unsupportedProviderRuntimeMessage(kind, name, provider) {
126
+ return [
127
+ `Local provider runtime is not supported for ${kind} "${name}" with provider "${provider}".`,
128
+ `Set ${kind === "surface" ? "surfaces" : "services"}.${name}.local.runtime to "auto" or "local" in treeseed.site.yaml,`,
129
+ "or add a provider-local adapter before requiring provider runtime."
130
+ ].join(" ");
131
+ }
132
+ function fallbackWebProviderFromDeployConfig(deployConfig) {
133
+ const record = deployConfig && typeof deployConfig === "object" ? deployConfig : {};
134
+ return normalizeProvider(record.providers?.deploy, "local");
135
+ }
136
+ function selectWebLocalRuntime(surfaceConfig, providerFallback = "local") {
137
+ const record = surfaceConfig && typeof surfaceConfig === "object" ? surfaceConfig : {};
138
+ const provider = normalizeProvider(record.provider, providerFallback);
139
+ const requested = normalizeLocalRuntimeMode(record.local?.runtime);
140
+ if (provider === "cloudflare" && requested !== "local") {
141
+ return {
142
+ requested,
143
+ provider,
144
+ selected: "cloudflare-wrangler-local",
145
+ reason: "Cloudflare web surfaces support local Wrangler runtime."
146
+ };
147
+ }
148
+ if (requested === "provider") {
149
+ throw new Error(unsupportedProviderRuntimeMessage("surface", "web", provider));
150
+ }
151
+ return {
152
+ requested,
153
+ provider,
154
+ selected: "astro-local",
155
+ reason: requested === "local" ? "Configured to use the full local Astro runtime." : `Provider "${provider}" has no provider-local web runtime; using Astro local.`
156
+ };
157
+ }
158
+ function selectServiceLocalRuntime(serviceName, serviceConfig) {
159
+ const record = serviceConfig && typeof serviceConfig === "object" ? serviceConfig : {};
160
+ const provider = normalizeProvider(record.provider, "railway");
161
+ const requested = normalizeLocalRuntimeMode(record.local?.runtime);
162
+ if (requested === "provider") {
163
+ throw new Error(unsupportedProviderRuntimeMessage("service", serviceName, provider));
164
+ }
165
+ return {
166
+ requested,
167
+ provider,
168
+ selected: "node-local",
169
+ reason: requested === "local" ? "Configured to use the full local Node runtime." : `Provider "${provider}" has no provider-local service runtime; using Node local.`
170
+ };
171
+ }
172
+ function loadDevDeployConfig(tenantRoot) {
173
+ try {
174
+ return loadTreeseedDeployConfig(resolve(tenantRoot, "treeseed.site.yaml"));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function generatedLocalWranglerPath(tenantRoot) {
180
+ return resolve(tenantRoot, ".treeseed", "generated", "environments", "local", "wrangler.toml");
181
+ }
109
182
  function browserHost(host) {
110
183
  return host === "0.0.0.0" || host === "::" || host === "[::]" ? "127.0.0.1" : host;
111
184
  }
112
185
  function webUrlFor(host, port) {
113
186
  return `http://${browserHost(host)}:${port}`;
114
187
  }
188
+ function dockerComposeIsAvailable(env) {
189
+ const docker = resolveTreeseedToolBinary("docker", { env });
190
+ if (!docker) return false;
191
+ const result = spawnSync(docker, ["compose", "version"], {
192
+ encoding: "utf8",
193
+ env
194
+ });
195
+ return (result.status ?? 1) === 0;
196
+ }
197
+ function resetActionForPath(id, label, path) {
198
+ return {
199
+ id,
200
+ label,
201
+ kind: "path",
202
+ path,
203
+ status: existsSync(path) ? "planned" : "skipped",
204
+ detail: existsSync(path) ? void 0 : "Path does not exist."
205
+ };
206
+ }
207
+ function createTreeseedIntegratedDevResetPlan(options) {
208
+ if (!options.enabled) {
209
+ return null;
210
+ }
211
+ const tenantRoot = options.tenantRoot;
212
+ const d1PersistTo = options.env.TREESEED_API_D1_LOCAL_PERSIST_TO?.trim() || resolve(tenantRoot, ".wrangler", "state", "v3", "d1");
213
+ return {
214
+ enabled: true,
215
+ actions: [
216
+ resetActionForPath("d1-state", "Remove local D1 state", d1PersistTo),
217
+ {
218
+ id: "mailpit",
219
+ label: options.mailpitEnabled ? "Reset Mailpit email runtime" : "Skip Mailpit email runtime",
220
+ kind: "service",
221
+ status: options.mailpitEnabled ? "planned" : "skipped",
222
+ detail: options.mailpitEnabled ? "The Treeseed-managed Mailpit container and inbox will be stopped and removed." : "Docker Compose is unavailable, so Mailpit is disabled for this local dev run."
223
+ },
224
+ resetActionForPath("wrangler-tmp", "Remove Wrangler temporary output", resolve(tenantRoot, ".wrangler", "tmp")),
225
+ resetActionForPath("worker-bundle", "Remove generated local worker bundle", resolve(tenantRoot, ".treeseed", "generated", "worker")),
226
+ resetActionForPath("dev-reload", "Remove browser reload marker", resolve(tenantRoot, DEV_RELOAD_FILE))
227
+ ],
228
+ preserved: [
229
+ ".env*",
230
+ "treeseed.site.yaml",
231
+ "src/env.yaml",
232
+ ".treeseed/config",
233
+ ".treeseed/generated/environments",
234
+ ".treeseed/state",
235
+ ".treeseed/workflow",
236
+ ".treeseed/workspace-links.json",
237
+ "migrations",
238
+ "node_modules"
239
+ ]
240
+ };
241
+ }
115
242
  function createWatchEntries(tenantRoot, sdkPackageRoot) {
116
243
  const entries = [
117
244
  { kind: "tenant", root: resolve(tenantRoot, "src") },
@@ -147,7 +274,7 @@ function createWatchEntries(tenantRoot, sdkPackageRoot) {
147
274
  function isSurfaceIncluded(plan, id) {
148
275
  return plan.commands.some((command) => command.id === id);
149
276
  }
150
- function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env) {
277
+ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env, mailpitEnabled, usesCloudflareWebRuntime) {
151
278
  if (setupMode === "off") {
152
279
  return [
153
280
  {
@@ -164,6 +291,15 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env)
164
291
  ["books", "Generate book/public artifacts", "scripts/aggregate-book.ts", "dist/scripts/aggregate-book.js"],
165
292
  ["worker-bundle", "Generate local worker bundle", "scripts/build-tenant-worker.ts", "dist/scripts/build-tenant-worker.js"]
166
293
  ];
294
+ const tenantBuild = usesCloudflareWebRuntime ? {
295
+ command: process.execPath,
296
+ args: [packageScriptPath("tenant-build")]
297
+ } : null;
298
+ const mailpit = resolveOptionalScriptEntrypoint(
299
+ sdkPackageRoot,
300
+ "scripts/ensure-mailpit.ts",
301
+ "dist/scripts/ensure-mailpit.js"
302
+ );
167
303
  const steps = [
168
304
  {
169
305
  id: "workspace-links",
@@ -174,11 +310,28 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env)
174
310
  {
175
311
  id: "wrangler",
176
312
  label: "Verify Wrangler executable",
177
- required: isSurfaceIncluded(planLike, "api"),
313
+ required: usesCloudflareWebRuntime || isSurfaceIncluded(planLike, "api"),
178
314
  status: "planned",
179
315
  detail: resolveTreeseedToolBinary("wrangler", { env }) ?? void 0
180
316
  },
181
- ...coreScripts.map(([id, label, source, dist]) => {
317
+ ...usesCloudflareWebRuntime ? [
318
+ {
319
+ id: "wrangler-config",
320
+ label: "Generate local Wrangler config",
321
+ required: true,
322
+ status: "planned",
323
+ detail: generatedLocalWranglerPath(tenantRoot)
324
+ },
325
+ {
326
+ id: "web-build",
327
+ label: "Build local Cloudflare web runtime",
328
+ required: true,
329
+ command: tenantBuild?.command,
330
+ args: tenantBuild?.args,
331
+ status: tenantBuild ? "planned" : "failed",
332
+ detail: tenantBuild ? void 0 : "Unable to resolve the tenant build script."
333
+ }
334
+ ] : coreScripts.map(([id, label, source, dist]) => {
182
335
  const script = resolveOptionalScriptEntrypoint(packageRoot, source, dist);
183
336
  return {
184
337
  id,
@@ -192,12 +345,15 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env)
192
345
  }),
193
346
  {
194
347
  id: "mailpit",
195
- label: "Check optional Mailpit email runtime",
196
- required: false,
197
- status: "planned"
348
+ label: mailpitEnabled ? "Start Mailpit email runtime" : "Disable Mailpit email runtime",
349
+ required: mailpitEnabled,
350
+ command: mailpitEnabled ? mailpit?.command : void 0,
351
+ args: mailpitEnabled ? mailpit?.args : void 0,
352
+ status: mailpitEnabled ? mailpit ? "planned" : "failed" : "skipped",
353
+ detail: mailpitEnabled ? mailpit ? "Mailpit SMTP will listen on 127.0.0.1:1025 and the web UI on http://127.0.0.1:8025." : "Unable to resolve the Mailpit startup script." : "Docker Compose is unavailable, so Mailpit is disabled for this local dev run."
198
354
  }
199
355
  ];
200
- if (isSurfaceIncluded(planLike, "api") && existsSync(resolve(tenantRoot, "migrations"))) {
356
+ if ((isSurfaceIncluded(planLike, "api") || usesCloudflareWebRuntime) && existsSync(resolve(tenantRoot, "migrations"))) {
201
357
  const migrate = resolveOptionalScriptEntrypoint(
202
358
  sdkPackageRoot,
203
359
  "scripts/tenant-d1-migrate-local.ts",
@@ -228,18 +384,41 @@ function createTreeseedIntegratedDevPlan(options = {}) {
228
384
  const apiPort = normalizePort(options.apiPort, TREESEED_DEFAULT_API_PORT);
229
385
  const managerPort = normalizePort(options.managerPort, TREESEED_DEFAULT_MANAGER_PORT);
230
386
  const includeServices = options.includeServices ?? (surface === "integrated" || surface === "services");
231
- const projectId = options.projectId ?? process.env.TREESEED_PROJECT_ID;
232
- const teamId = options.teamId ?? process.env.TREESEED_HOSTING_TEAM_ID;
233
- const mergedEnv = { ...process.env, ...options.env ?? {} };
387
+ const machineEnv = resolveLocalMachineEnv(tenantRoot);
388
+ const mergedEnv = { ...process.env, ...machineEnv, ...options.env ?? {} };
389
+ const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID;
390
+ const teamId = options.teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
234
391
  const apiBaseUrl = options.apiHost != null || options.apiPort != null ? `http://${apiHost}:${apiPort}` : mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
235
392
  const webUrl = surface === "integrated" || surface === "web" ? webUrlFor(webHost, webPort) : null;
236
393
  const sdkPackageRoot = resolvePackageRoot("@treeseed/sdk", tenantRoot);
394
+ const deployConfig = loadDevDeployConfig(tenantRoot);
395
+ const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig));
396
+ const serviceLocalRuntimes = {
397
+ api: selectServiceLocalRuntime("api", deployConfig?.services?.api),
398
+ manager: selectServiceLocalRuntime("manager", deployConfig?.services?.manager),
399
+ worker: selectServiceLocalRuntime("worker", deployConfig?.services?.worker)
400
+ };
401
+ const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
237
402
  const coreRunTsPath = resolve(packageRoot, "scripts", "run-ts.mjs");
238
403
  const webEntrypoint = resolveNodeEntrypoint(
239
404
  sdkPackageRoot,
240
405
  "scripts/tenant-astro-command.ts",
241
406
  "dist/scripts/tenant-astro-command.js"
242
407
  );
408
+ const wranglerEntrypoint = {
409
+ command: process.execPath,
410
+ args: [
411
+ resolveWranglerBin(),
412
+ "dev",
413
+ "--local",
414
+ "--config",
415
+ generatedLocalWranglerPath(tenantRoot),
416
+ "--ip",
417
+ webHost,
418
+ "--port",
419
+ String(webPort)
420
+ ]
421
+ };
243
422
  const apiEntrypoint = resolveTenantApiEntrypoint(tenantRoot, coreRunTsPath) ?? resolveNodeEntrypoint(
244
423
  packageRoot,
245
424
  "src/api/server.ts",
@@ -256,6 +435,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
256
435
  "dist/services/worker.js"
257
436
  );
258
437
  const watchEntries = watch ? createWatchEntries(tenantRoot, sdkPackageRoot) : [];
438
+ const mailpitEnabled = dockerComposeIsAvailable(mergedEnv);
439
+ const resetRequested = options.reset === true;
259
440
  const sharedEnv = {
260
441
  ...mergedEnv,
261
442
  TREESEED_LOCAL_DEV_MODE: mergedEnv.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
@@ -265,8 +446,22 @@ function createTreeseedIntegratedDevPlan(options = {}) {
265
446
  TREESEED_HOSTING_TEAM_ID: teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID,
266
447
  TREESEED_API_D1_DATABASE_NAME: mergedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
267
448
  SITE_DATA_DB: mergedEnv.SITE_DATA_DB ?? "SITE_DATA_DB",
268
- TREESEED_API_D1_LOCAL_PERSIST_TO: mergedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO ?? resolve(tenantRoot, ".wrangler", "state", "v3", "d1")
449
+ TREESEED_API_D1_LOCAL_PERSIST_TO: mergedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO ?? (usesCloudflareWebRuntime ? resolve(tenantRoot, ".treeseed", "generated", "environments", "local", ".wrangler", "state", "v3", "d1") : resolve(tenantRoot, ".wrangler", "state", "v3", "d1")),
450
+ TREESEED_FORM_TOKEN_SECRET: mergedEnv.TREESEED_FORM_TOKEN_SECRET ?? "treeseed-local-form-token-secret",
451
+ TREESEED_BETTER_AUTH_SECRET: mergedEnv.TREESEED_BETTER_AUTH_SECRET ?? "treeseed-local-better-auth-secret-minimum-32-characters",
452
+ TREESEED_AUTH_LOCAL_USE_MAILPIT: mailpitEnabled ? mergedEnv.TREESEED_AUTH_LOCAL_USE_MAILPIT ?? "true" : "false",
453
+ TREESEED_FORMS_LOCAL_USE_MAILPIT: mailpitEnabled ? mergedEnv.TREESEED_FORMS_LOCAL_USE_MAILPIT ?? "true" : "false",
454
+ TREESEED_MAILPIT_SMTP_HOST: mergedEnv.TREESEED_MAILPIT_SMTP_HOST ?? TREESEED_DEFAULT_MAILPIT_SMTP_HOST,
455
+ TREESEED_MAILPIT_SMTP_PORT: mergedEnv.TREESEED_MAILPIT_SMTP_PORT ?? String(TREESEED_DEFAULT_MAILPIT_SMTP_PORT),
456
+ TREESEED_MAILPIT_UI_PORT: mergedEnv.TREESEED_MAILPIT_UI_PORT ?? String(TREESEED_DEFAULT_MAILPIT_UI_PORT),
457
+ TREESEED_AUTH_EMAIL_FROM: mergedEnv.TREESEED_AUTH_EMAIL_FROM ?? "Treeseed Market <auth@treeseed.local>"
269
458
  };
459
+ const reset = createTreeseedIntegratedDevResetPlan({
460
+ tenantRoot,
461
+ env: sharedEnv,
462
+ mailpitEnabled,
463
+ enabled: resetRequested
464
+ });
270
465
  if (watch && feedbackMode === "live") {
271
466
  sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
272
467
  }
@@ -274,11 +469,12 @@ function createTreeseedIntegratedDevPlan(options = {}) {
274
469
  if (surface === "integrated" || surface === "web") {
275
470
  commands.push({
276
471
  id: "web",
277
- label: "Astro UI",
278
- command: webEntrypoint.command,
279
- args: [...webEntrypoint.args, "dev", "--host", webHost, "--port", String(webPort)],
472
+ label: usesCloudflareWebRuntime ? "Cloudflare Wrangler UI" : "Astro UI",
473
+ command: usesCloudflareWebRuntime ? wranglerEntrypoint.command : webEntrypoint.command,
474
+ args: usesCloudflareWebRuntime ? wranglerEntrypoint.args : [...webEntrypoint.args, "dev", "--host", webHost, "--port", String(webPort)],
280
475
  cwd: tenantRoot,
281
- env: sharedEnv
476
+ env: sharedEnv,
477
+ localRuntime: webLocalRuntime
282
478
  });
283
479
  }
284
480
  if (surface === "integrated" || surface === "api") {
@@ -291,7 +487,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
291
487
  env: {
292
488
  ...sharedEnv,
293
489
  PORT: options.apiPort != null ? String(apiPort) : sharedEnv.PORT ?? String(apiPort)
294
- }
490
+ },
491
+ localRuntime: serviceLocalRuntimes.api
295
492
  });
296
493
  }
297
494
  if (includeServices || surface === "manager") {
@@ -305,7 +502,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
305
502
  ...sharedEnv,
306
503
  PORT: options.managerPort != null ? String(managerPort) : sharedEnv.PORT ?? String(managerPort),
307
504
  TREESEED_MANAGER_BASE_URL: options.managerPort != null ? `http://${apiHost}:${managerPort}` : sharedEnv.TREESEED_MANAGER_BASE_URL ?? `http://${apiHost}:${managerPort}`
308
- }
505
+ },
506
+ localRuntime: serviceLocalRuntimes.manager
309
507
  });
310
508
  }
311
509
  if (includeServices || surface === "worker") {
@@ -315,7 +513,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
315
513
  command: workerEntrypoint.command,
316
514
  args: workerEntrypoint.args,
317
515
  cwd: tenantRoot,
318
- env: sharedEnv
516
+ env: sharedEnv,
517
+ localRuntime: serviceLocalRuntimes.worker
319
518
  });
320
519
  }
321
520
  const readyChecks = commands.map((command) => {
@@ -344,6 +543,15 @@ function createTreeseedIntegratedDevPlan(options = {}) {
344
543
  strategy: "process"
345
544
  };
346
545
  });
546
+ if (mailpitEnabled && setupMode !== "off") {
547
+ readyChecks.push({
548
+ id: "mailpit",
549
+ label: "Mailpit",
550
+ required: true,
551
+ strategy: "http",
552
+ url: `http://127.0.0.1:${sharedEnv.TREESEED_MAILPIT_UI_PORT ?? TREESEED_DEFAULT_MAILPIT_UI_PORT}`
553
+ });
554
+ }
347
555
  return {
348
556
  surface,
349
557
  setupMode,
@@ -353,10 +561,15 @@ function createTreeseedIntegratedDevPlan(options = {}) {
353
561
  tenantRoot,
354
562
  apiBaseUrl,
355
563
  webUrl,
356
- setupSteps: createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, { commands }, sharedEnv),
564
+ setupSteps: createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, { commands }, sharedEnv, mailpitEnabled, usesCloudflareWebRuntime),
357
565
  readyChecks,
358
566
  watchEntries,
359
- commands
567
+ commands,
568
+ localRuntimes: {
569
+ web: webLocalRuntime,
570
+ ...serviceLocalRuntimes
571
+ },
572
+ reset
360
573
  };
361
574
  }
362
575
  function defaultSignalRegistrar(signal, handler) {
@@ -372,6 +585,9 @@ function defaultPrepareEnvironment(tenantRoot) {
372
585
  function defaultKillProcess(pid, signal) {
373
586
  process.kill(pid, signal);
374
587
  }
588
+ function defaultRemovePath(path) {
589
+ rmSync(path, { recursive: true, force: true });
590
+ }
375
591
  function createManagedDevProcess(command, child) {
376
592
  let resolveExit = () => {
377
593
  };
@@ -441,6 +657,13 @@ function defaultWrite(line, stream) {
441
657
  const target = stream === "stderr" ? process.stderr : process.stdout;
442
658
  target.write(line);
443
659
  }
660
+ function resolveLocalMachineEnv(tenantRoot) {
661
+ try {
662
+ return resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
663
+ } catch {
664
+ return {};
665
+ }
666
+ }
444
667
  function emitEvent(options, write, event, stream = event.type === "error" ? "stderr" : "stdout") {
445
668
  if (options.json) {
446
669
  write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.event", ...event })}
@@ -452,6 +675,83 @@ function emitEvent(options, write, event, stream = event.type === "error" ? "std
452
675
  write(`${surface} ${String(message)}
453
676
  `, stream);
454
677
  }
678
+ function runTreeseedIntegratedDevReset(reset, options, deps) {
679
+ if (!reset?.enabled) {
680
+ return null;
681
+ }
682
+ const results = reset.actions.map((action) => {
683
+ if (action.status === "skipped") {
684
+ emitEvent(options, deps.write, {
685
+ type: "reset",
686
+ status: action.status,
687
+ message: `${action.label}: skipped`,
688
+ detail: action
689
+ });
690
+ return action;
691
+ }
692
+ if (action.kind === "service") {
693
+ const stopped = deps.stopMailpitContainers();
694
+ const result = {
695
+ ...action,
696
+ status: stopped ? "removed" : "failed",
697
+ detail: stopped ? "Mailpit container state was reset." : "Unable to stop or remove the Treeseed-managed Mailpit container."
698
+ };
699
+ emitEvent(options, deps.write, {
700
+ type: "reset",
701
+ status: result.status,
702
+ message: `${result.label}: ${result.status}`,
703
+ detail: result
704
+ }, result.status === "failed" ? "stderr" : "stdout");
705
+ return result;
706
+ }
707
+ if (!action.path || !existsSync(action.path)) {
708
+ const result = {
709
+ ...action,
710
+ status: "skipped",
711
+ detail: action.detail ?? "Path does not exist."
712
+ };
713
+ emitEvent(options, deps.write, {
714
+ type: "reset",
715
+ status: result.status,
716
+ message: `${result.label}: skipped`,
717
+ detail: result
718
+ });
719
+ return result;
720
+ }
721
+ try {
722
+ deps.removePath(action.path);
723
+ const result = {
724
+ ...action,
725
+ status: "removed",
726
+ detail: action.path
727
+ };
728
+ emitEvent(options, deps.write, {
729
+ type: "reset",
730
+ status: result.status,
731
+ message: `${result.label}: removed`,
732
+ detail: result
733
+ });
734
+ return result;
735
+ } catch (error) {
736
+ const result = {
737
+ ...action,
738
+ status: "failed",
739
+ detail: error instanceof Error ? error.message : String(error)
740
+ };
741
+ emitEvent(options, deps.write, {
742
+ type: "reset",
743
+ status: result.status,
744
+ message: `${result.label}: failed`,
745
+ detail: result
746
+ }, "stderr");
747
+ return result;
748
+ }
749
+ });
750
+ return {
751
+ ...reset,
752
+ actions: results
753
+ };
754
+ }
455
755
  function writePlan(plan, options, write) {
456
756
  if (options.json) {
457
757
  write(`${JSON.stringify({ schemaVersion: 1, kind: "treeseed.dev.plan", ok: true, payload: plan }, null, 2)}
@@ -466,12 +766,24 @@ function writePlan(plan, options, write) {
466
766
  `, "stdout");
467
767
  write(`feedback: ${plan.feedbackMode}
468
768
  `, "stdout");
769
+ if (plan.reset) {
770
+ write(`reset: enabled
771
+ `, "stdout");
772
+ for (const action of plan.reset.actions) {
773
+ write(`- reset ${action.id}: ${action.status}${action.path ? ` ${action.path}` : ""}
774
+ `, "stdout");
775
+ }
776
+ }
469
777
  if (plan.webUrl) {
470
778
  write(`web: ${plan.webUrl}
471
779
  `, "stdout");
472
780
  }
473
781
  write(`api: ${plan.apiBaseUrl}
474
782
  `, "stdout");
783
+ for (const [name, runtime] of Object.entries(plan.localRuntimes)) {
784
+ write(`runtime ${name}: ${runtime.selected} (${runtime.provider}, requested ${runtime.requested})${runtime.reason ? ` - ${runtime.reason}` : ""}
785
+ `, "stdout");
786
+ }
475
787
  for (const command of plan.commands) {
476
788
  write(`- ${command.id}: ${command.command} ${command.args.join(" ")}
477
789
  `, "stdout");
@@ -582,9 +894,24 @@ function runLocalSetup(plan, options, deps) {
582
894
  status: step.required ? "failed" : "degraded",
583
895
  detail: "Wrangler was not found. Run `npx trsd install --json` and retry `npx trsd dev`."
584
896
  };
585
- } else if (step.id === "mailpit") {
586
- const docker = resolveTreeseedToolBinary("docker", { env: { ...process.env, ...plan.commands[0]?.env } });
587
- result = docker ? { ...step, status: "completed", detail: `Docker detected at ${docker}; Mailpit remains optional for local dev.` } : { ...step, status: "degraded", detail: "Docker is unavailable, so Mailpit email previews are disabled." };
897
+ } else if (step.id === "wrangler-config") {
898
+ if (plan.setupMode === "check") {
899
+ result = { ...step, status: "skipped", detail: "Local Wrangler config generation was checked in non-mutating mode." };
900
+ } else {
901
+ try {
902
+ const { wranglerPath } = ensureGeneratedWranglerConfig(plan.tenantRoot, {
903
+ target: createPersistentDeployTarget("local"),
904
+ env: plan.commands[0]?.env
905
+ });
906
+ result = { ...step, status: "completed", detail: wranglerPath };
907
+ } catch (error) {
908
+ result = {
909
+ ...step,
910
+ status: "failed",
911
+ detail: error instanceof Error ? error.message : String(error)
912
+ };
913
+ }
914
+ }
588
915
  } else if (plan.setupMode === "check") {
589
916
  result = { ...step, status: step.status === "failed" ? "failed" : "skipped", detail: step.detail ?? "Skipped in setup check mode." };
590
917
  } else {
@@ -661,6 +988,8 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
661
988
  const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
662
989
  const startWatch = deps.startWatch ?? startPollingWatch;
663
990
  const prepareEnvironment = deps.prepareEnvironment ?? defaultPrepareEnvironment;
991
+ const removePath = deps.removePath ?? defaultRemovePath;
992
+ const stopMailpit = deps.stopMailpitContainers ?? stopKnownMailpitContainers;
664
993
  prepareEnvironment(tenantRoot);
665
994
  const plan = createTreeseedIntegratedDevPlan({
666
995
  ...options,
@@ -674,6 +1003,20 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
674
1003
  writePlan(plan, options, write);
675
1004
  return 0;
676
1005
  }
1006
+ const resetResults = runTreeseedIntegratedDevReset(plan.reset, options, {
1007
+ write,
1008
+ removePath,
1009
+ stopMailpitContainers: stopMailpit
1010
+ });
1011
+ const failedReset = resetResults?.actions.find((action) => action.status === "failed");
1012
+ if (failedReset) {
1013
+ emitEvent(options, write, {
1014
+ type: "error",
1015
+ message: `${failedReset.label} failed during dev reset.`,
1016
+ detail: failedReset
1017
+ });
1018
+ return 1;
1019
+ }
677
1020
  const setupResults = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
678
1021
  const failedSetup = setupResults.find((step) => step.status === "failed" && step.required);
679
1022
  if (failedSetup) {
@@ -822,6 +1165,22 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
822
1165
  sdkChanged: change.sdkChanged
823
1166
  }
824
1167
  });
1168
+ if (commandsById.get("web")?.localRuntime?.selected === "cloudflare-wrangler-local" && (change.tenantChanged || change.packageChanged || change.sdkChanged)) {
1169
+ const web = children.get("web");
1170
+ if (web) {
1171
+ await stopManagedProcess(web, "SIGTERM", killProcess, Math.min(shutdownGraceMs, 500));
1172
+ children.delete("web");
1173
+ exited.delete("web");
1174
+ }
1175
+ const setupResults2 = runLocalSetup(plan, options, { spawnSync: spawnSyncProcess, write });
1176
+ const failedSetup2 = setupResults2.find((step) => step.status === "failed" && step.required);
1177
+ if (failedSetup2) {
1178
+ emitEvent(options, write, { type: "error", message: failedSetupMessage(failedSetup2), detail: failedSetup2 });
1179
+ finalize(1);
1180
+ return;
1181
+ }
1182
+ await restartCommand("web");
1183
+ }
825
1184
  if (change.packageChanged || change.sdkChanged) {
826
1185
  await Promise.all([
827
1186
  restartCommand("api"),
@@ -854,8 +1213,9 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
854
1213
  if (check.strategy === "http" && check.url) {
855
1214
  ready = await waitForHttpReady(fetchFn, check.url, readinessTimeoutMs);
856
1215
  } else {
1216
+ const commandId = check.id;
857
1217
  await delay(processReadyGraceMs);
858
- ready = !exited.has(check.id) && children.has(check.id);
1218
+ ready = !exited.has(commandId) && children.has(commandId);
859
1219
  }
860
1220
  if (settled) {
861
1221
  return;
@@ -914,9 +1274,14 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
914
1274
  export {
915
1275
  TREESEED_DEFAULT_API_HOST,
916
1276
  TREESEED_DEFAULT_API_PORT,
1277
+ TREESEED_DEFAULT_MAILPIT_SMTP_HOST,
1278
+ TREESEED_DEFAULT_MAILPIT_SMTP_PORT,
1279
+ TREESEED_DEFAULT_MAILPIT_UI_PORT,
917
1280
  TREESEED_DEFAULT_MANAGER_PORT,
918
1281
  TREESEED_DEFAULT_WEB_HOST,
919
1282
  TREESEED_DEFAULT_WEB_PORT,
920
1283
  createTreeseedIntegratedDevPlan,
921
- runTreeseedIntegratedDev
1284
+ createTreeseedIntegratedDevResetPlan,
1285
+ runTreeseedIntegratedDev,
1286
+ runTreeseedIntegratedDevReset
922
1287
  };
package/dist/env.yaml CHANGED
@@ -120,6 +120,7 @@ entries:
120
120
  sensitivity: secret
121
121
  targets:
122
122
  - local-runtime
123
+ - github-secret
123
124
  - railway-secret
124
125
  scopes:
125
126
  - staging
@@ -1,14 +1,12 @@
1
1
  import { handleFormSubmission, handleTokenRequest } from "../../../utils/forms/service.js";
2
- const prerender = false;
3
- const GET = async (context) => {
2
+ async function GET(context) {
4
3
  return handleTokenRequest(context);
5
- };
6
- const POST = async (context) => {
4
+ }
5
+ async function POST(context) {
7
6
  const result = await handleFormSubmission(context);
8
7
  return context.redirect(result.redirectTo, 303);
9
- };
8
+ }
10
9
  export {
11
10
  GET,
12
- POST,
13
- prerender
11
+ POST
14
12
  };
@@ -60,6 +60,7 @@ const exitCode = await runTreeseedIntegratedDev({
60
60
  feedbackMode: parseFeedbackMode(readOption('--feedback')),
61
61
  openMode: parseOpenMode(readOption('--open')),
62
62
  plan: readFlag('--plan'),
63
+ reset: readFlag('--reset'),
63
64
  json: readFlag('--json'),
64
65
  projectId: readOption('--project-id'),
65
66
  teamId: readOption('--team-id'),
package/dist/site.js CHANGED
@@ -245,6 +245,7 @@ function createTreeseedSite(tenantConfig, { starlight }) {
245
245
  return defineConfig({
246
246
  adapter: serverRendered ? cloudflare({ imageService: "compile" }) : void 0,
247
247
  output: serverRendered ? "server" : "static",
248
+ session: serverRendered ? { driver: "null" } : void 0,
248
249
  site: siteConfig.site.siteUrl,
249
250
  image: {
250
251
  service: {