@treeseed/core 0.6.17 → 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/api/app.js +29 -0
- package/dist/api/auth/d1-database.js +4 -8
- package/dist/api/auth/d1-provider.d.ts +12 -0
- package/dist/api/auth/d1-provider.js +10 -2
- package/dist/api/auth/d1-store.d.ts +16 -0
- package/dist/api/auth/d1-store.js +255 -16
- package/dist/api/auth/memory-provider.d.ts +5 -1
- package/dist/api/auth/memory-provider.js +6 -1
- package/dist/api/auth/rbac.js +1 -0
- package/dist/api/config.js +8 -0
- package/dist/api/providers.js +2 -1
- package/dist/api/railway.d.ts +1 -0
- package/dist/api/types.d.ts +15 -0
- package/dist/dev.d.ts +42 -1
- package/dist/dev.js +397 -31
- package/dist/env.yaml +1 -0
- package/dist/pages/api/form/submit.js +5 -7
- package/dist/scripts/dev-platform.js +1 -0
- package/dist/site.js +1 -0
- package/dist/utils/forms/service.js +2 -2
- package/dist/worker/forms-worker.js +1 -2
- package/package.json +14 -3
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
|
-
|
|
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
|
-
...
|
|
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: "
|
|
196
|
-
required:
|
|
197
|
-
|
|
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
|
|
232
|
-
const
|
|
233
|
-
const
|
|
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 === "
|
|
586
|
-
|
|
587
|
-
|
|
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) {
|
|
@@ -753,7 +1096,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
753
1096
|
}
|
|
754
1097
|
const exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : code ?? 0;
|
|
755
1098
|
const required = requiredSurfaceIds.has(command.id);
|
|
756
|
-
if (
|
|
1099
|
+
if (required) {
|
|
757
1100
|
emitEvent(options, write, {
|
|
758
1101
|
type: "error",
|
|
759
1102
|
surface: command.id,
|
|
@@ -764,14 +1107,15 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
764
1107
|
finalize(exitCode === 0 ? 1 : exitCode);
|
|
765
1108
|
return;
|
|
766
1109
|
}
|
|
1110
|
+
const status = exitCode === 0 ? "idle" : "degraded";
|
|
767
1111
|
emitEvent(options, write, {
|
|
768
1112
|
type: "error",
|
|
769
1113
|
surface: command.id,
|
|
770
1114
|
exitCode,
|
|
771
1115
|
signal,
|
|
772
|
-
status
|
|
773
|
-
message: `${command.label} exited with ${signal ?? exitCode}; continuing because it is not a required surface.`
|
|
774
|
-
}, "stderr");
|
|
1116
|
+
status,
|
|
1117
|
+
message: readinessComplete ? `${command.label} exited with ${signal ?? exitCode}; continuing because it is not a required surface.` : `${command.label} exited during startup with ${signal ?? exitCode}; continuing because it is not a required surface.`
|
|
1118
|
+
}, status === "idle" ? "stdout" : "stderr");
|
|
775
1119
|
void stopManagedProcess(managed, "SIGTERM", killProcess, 0).finally(() => {
|
|
776
1120
|
children.delete(command.id);
|
|
777
1121
|
});
|
|
@@ -821,6 +1165,22 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
821
1165
|
sdkChanged: change.sdkChanged
|
|
822
1166
|
}
|
|
823
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
|
+
}
|
|
824
1184
|
if (change.packageChanged || change.sdkChanged) {
|
|
825
1185
|
await Promise.all([
|
|
826
1186
|
restartCommand("api"),
|
|
@@ -853,8 +1213,9 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
853
1213
|
if (check.strategy === "http" && check.url) {
|
|
854
1214
|
ready = await waitForHttpReady(fetchFn, check.url, readinessTimeoutMs);
|
|
855
1215
|
} else {
|
|
1216
|
+
const commandId = check.id;
|
|
856
1217
|
await delay(processReadyGraceMs);
|
|
857
|
-
ready = !exited.has(
|
|
1218
|
+
ready = !exited.has(commandId) && children.has(commandId);
|
|
858
1219
|
}
|
|
859
1220
|
if (settled) {
|
|
860
1221
|
return;
|
|
@@ -913,9 +1274,14 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
|
|
|
913
1274
|
export {
|
|
914
1275
|
TREESEED_DEFAULT_API_HOST,
|
|
915
1276
|
TREESEED_DEFAULT_API_PORT,
|
|
1277
|
+
TREESEED_DEFAULT_MAILPIT_SMTP_HOST,
|
|
1278
|
+
TREESEED_DEFAULT_MAILPIT_SMTP_PORT,
|
|
1279
|
+
TREESEED_DEFAULT_MAILPIT_UI_PORT,
|
|
916
1280
|
TREESEED_DEFAULT_MANAGER_PORT,
|
|
917
1281
|
TREESEED_DEFAULT_WEB_HOST,
|
|
918
1282
|
TREESEED_DEFAULT_WEB_PORT,
|
|
919
1283
|
createTreeseedIntegratedDevPlan,
|
|
920
|
-
|
|
1284
|
+
createTreeseedIntegratedDevResetPlan,
|
|
1285
|
+
runTreeseedIntegratedDev,
|
|
1286
|
+
runTreeseedIntegratedDevReset
|
|
921
1287
|
};
|