@treeseed/core 0.10.6 → 0.10.7

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.
@@ -8,6 +8,7 @@ interface Props {
8
8
  size?: ButtonSize;
9
9
  disabled?: boolean;
10
10
  ariaLabel?: string;
11
+ reload?: boolean;
11
12
  class?: string;
12
13
  }
13
14
 
@@ -18,6 +19,7 @@ const {
18
19
  size = 'md',
19
20
  disabled = false,
20
21
  ariaLabel,
22
+ reload = false,
21
23
  class: className,
22
24
  } = Astro.props as Props;
23
25
 
@@ -34,6 +36,7 @@ const classes = ['ts-button', className].filter(Boolean).join(' ');
34
36
  aria-label={ariaLabel}
35
37
  aria-disabled={disabled ? 'true' : undefined}
36
38
  tabindex={disabled ? -1 : undefined}
39
+ data-astro-reload={reload ? true : undefined}
37
40
  >
38
41
  <slot />
39
42
  </a>
@@ -4,6 +4,7 @@ import '../../../styles/theme.css';
4
4
  import '../../../styles/ui.css';
5
5
  import '../../../styles/forms.css';
6
6
  import '../../../styles/app-shell.css';
7
+ import { ClientRouter } from 'astro:transitions';
7
8
  import ThemeScript from '../theme/ThemeScript.astro';
8
9
  import ThemeMenu from '../theme/ThemeMenu.astro';
9
10
  import RailNav from './RailNav.astro';
@@ -61,6 +62,7 @@ const {
61
62
  <title>{title}</title>
62
63
  <meta name="description" content={description} />
63
64
  <ThemeScript defaultScheme={appearance.scheme} defaultMode={appearance.mode} preferDefaultPreference />
65
+ <ClientRouter />
64
66
  </head>
65
67
  <body>
66
68
  <a class="ts-skip-link" href="#main-content">Skip to content</a>
@@ -84,6 +86,7 @@ const {
84
86
  variant={action.variant ?? 'secondary'}
85
87
  ariaLabel={action.ariaLabel}
86
88
  disabled={action.disabled}
89
+ reload={action.reload}
87
90
  size="sm"
88
91
  >
89
92
  {action.label}
@@ -4,6 +4,7 @@ import '../../../styles/theme.css';
4
4
  import '../../../styles/ui.css';
5
5
  import '../../../styles/forms.css';
6
6
  import '../../../styles/app-shell.css';
7
+ import { ClientRouter } from 'astro:transitions';
7
8
  import ThemeScript from '../theme/ThemeScript.astro';
8
9
  import ThemeMenu from '../theme/ThemeMenu.astro';
9
10
  import TopBar from './TopBar.astro';
@@ -79,6 +80,7 @@ function isCurrentPath(href: string) {
79
80
  defaultMode={appearance.mode}
80
81
  preferDefaultPreference={preferServerAppearance}
81
82
  />
83
+ <ClientRouter />
82
84
  <slot name="head" />
83
85
  </head>
84
86
  <body>
@@ -130,6 +132,7 @@ function isCurrentPath(href: string) {
130
132
  variant={action.variant ?? 'secondary'}
131
133
  ariaLabel={action.ariaLabel}
132
134
  disabled={action.disabled}
135
+ reload={action.reload}
133
136
  size="sm"
134
137
  >
135
138
  {action.label}
@@ -30,3 +30,29 @@ const {
30
30
  />
31
31
  </div>
32
32
  </details>
33
+
34
+ <script is:inline>
35
+ (() => {
36
+ const bindingKey = '__treeseedThemeMenuDismissBound';
37
+ if (window[bindingKey]) return;
38
+ window[bindingKey] = true;
39
+
40
+ function themeMenuForTarget(target) {
41
+ return target instanceof Element ? target.closest('[data-ts-theme-menu]') : null;
42
+ }
43
+
44
+ function closeOpenMenus(except = null) {
45
+ document.querySelectorAll('[data-ts-theme-menu][open]').forEach((menu) => {
46
+ if (menu !== except) menu.removeAttribute('open');
47
+ });
48
+ }
49
+
50
+ document.addEventListener('pointerdown', (event) => {
51
+ closeOpenMenus(themeMenuForTarget(event.target));
52
+ }, true);
53
+
54
+ document.addEventListener('keydown', (event) => {
55
+ if (event.key === 'Escape') closeOpenMenus();
56
+ });
57
+ })();
58
+ </script>
@@ -26,6 +26,7 @@ const themeCss = buildTreeseedThemeCss();
26
26
 
27
27
  <script
28
28
  is:inline
29
+ data-astro-rerun
29
30
  define:vars={{
30
31
  builtInSchemeIds,
31
32
  defaultScheme: preference.scheme,
@@ -84,7 +84,7 @@ const modes = [
84
84
  ) : null}
85
85
  </div>
86
86
 
87
- <script is:inline define:vars={{ schemes }}>
87
+ <script is:inline data-astro-rerun define:vars={{ schemes }}>
88
88
  (() => {
89
89
  const schemeKey = 'treeseed_color_scheme';
90
90
  const modeKey = 'treeseed_theme_mode';
@@ -135,7 +135,7 @@ const modes = [
135
135
  store(modeKey, safeMode);
136
136
  }
137
137
  window.dispatchEvent(new CustomEvent('treeseed:theme-change', {
138
- detail: { scheme: safeScheme, mode: safeMode, renderedMode: renderedMode(safeMode) },
138
+ detail: { scheme: safeScheme, mode: safeMode, renderedMode: renderedMode(safeMode), persist },
139
139
  }));
140
140
  }
141
141
 
package/dist/dev-watch.js CHANGED
@@ -85,7 +85,7 @@ function classifyChanges(changedPaths, watchEntries) {
85
85
  }
86
86
  function isTenantApiInput(filePath) {
87
87
  const normalized = filePath.split(sep).join("/");
88
- return normalized.endsWith("/treeseed.site.yaml") || normalized.endsWith("/treeseed.config.ts") || normalized.endsWith("/package.json") || normalized.endsWith("/tsconfig.json");
88
+ return normalized.endsWith("/treeseed.site.yaml") || normalized.endsWith("/treeseed.config.ts") || normalized.endsWith("/package.json") || normalized.endsWith("/tsconfig.json") || normalized.includes("/src/api/") || normalized.includes("/src/market-operations-runner/");
89
89
  }
90
90
  const tenantChanged = changedPaths.some(
91
91
  (filePath) => watchEntries.some((entry) => entry.kind === "tenant" && matchesEntry(filePath, entry))
package/dist/dev.d.ts CHANGED
@@ -8,13 +8,17 @@ export declare const TREESEED_DEFAULT_API_PORT = 3000;
8
8
  export declare const TREESEED_DEFAULT_LOCAL_SMTP_HOST = "127.0.0.1";
9
9
  export declare const TREESEED_DEFAULT_LOCAL_SMTP_PORT = 1025;
10
10
  export declare const TREESEED_DEFAULT_MAILPIT_UI_PORT = 8025;
11
+ export declare const TREESEED_DEFAULT_MARKET_POSTGRES_PORT = 55432;
12
+ export declare const TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER = "treeseed-market-local-postgres";
13
+ export declare const TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME = "treeseed-market-local-postgres-data";
14
+ export declare const TREESEED_DEFAULT_MARKET_POSTGRES_URL = "postgres://treeseed:treeseed@127.0.0.1:55432/market_local";
11
15
  export type TreeseedIntegratedDevSurface = 'integrated' | 'all' | 'web' | 'api' | 'manager' | 'worker' | 'agents' | 'services';
12
16
  export type TreeseedIntegratedDevSetupMode = 'auto' | 'check' | 'off';
13
17
  export type TreeseedIntegratedDevFeedbackMode = 'live' | 'restart' | 'off';
14
18
  export type TreeseedIntegratedDevOpenMode = 'auto' | 'on' | 'off';
15
19
  export type TreeseedLocalRuntimeMode = 'auto' | 'provider' | 'local';
16
20
  export type TreeseedSelectedLocalRuntime = 'astro-local' | 'cloudflare-wrangler-local' | 'node-local';
17
- export type TreeseedIntegratedDevCommandId = 'web' | 'api' | 'manager' | 'worker' | 'agents';
21
+ export type TreeseedIntegratedDevCommandId = 'web' | 'api' | 'manager' | 'worker' | 'agents' | 'market-runner';
18
22
  export type TreeseedLocalRuntimeSelection = {
19
23
  requested: TreeseedLocalRuntimeMode;
20
24
  selected: TreeseedSelectedLocalRuntime;
@@ -74,11 +78,11 @@ export type TreeseedIntegratedDevReadinessCheck = {
74
78
  url?: string;
75
79
  };
76
80
  export type TreeseedIntegratedDevResetAction = {
77
- id: 'd1-state' | 'mailpit' | 'wrangler-tmp' | 'worker-bundle' | 'dev-reload';
81
+ id: 'd1-state' | 'generated-d1-state' | 'generated-wrangler-state' | 'legacy-local-sqlite' | 'mailpit' | 'market-postgres' | 'root-wrangler-state' | 'wrangler-tmp' | 'worker-bundle' | 'dev-reload';
78
82
  label: string;
79
83
  kind: 'path' | 'service';
80
84
  path?: string;
81
- status: 'planned' | 'removed' | 'skipped' | 'failed';
85
+ status: 'planned' | 'removed' | 'refreshed' | 'skipped' | 'failed';
82
86
  detail?: string;
83
87
  };
84
88
  export type TreeseedIntegratedDevResetPlan = {
@@ -130,6 +134,8 @@ type TreeseedIntegratedDevDependencies = {
130
134
  startWatch: WatchStarter;
131
135
  removePath: (path: string) => void;
132
136
  stopMailpitContainers: () => boolean;
137
+ resetMarketPostgres: () => boolean;
138
+ stopMarketPostgres: () => boolean;
133
139
  inspectPortOwners: (ports: readonly number[]) => TreeseedDevPortOwner[];
134
140
  };
135
141
  export type TreeseedDevPortOwner = {
@@ -145,7 +151,7 @@ export declare function createTreeseedIntegratedDevResetPlan(options: {
145
151
  enabled?: boolean;
146
152
  }): TreeseedIntegratedDevResetPlan | null;
147
153
  export declare function createTreeseedIntegratedDevPlan(options?: TreeseedIntegratedDevOptions): TreeseedIntegratedDevPlan;
148
- export declare function runTreeseedIntegratedDevReset(reset: TreeseedIntegratedDevResetPlan | null, options: Pick<TreeseedIntegratedDevOptions, 'json'>, deps: Pick<TreeseedIntegratedDevDependencies, 'write' | 'removePath' | 'stopMailpitContainers'>): {
154
+ export declare function runTreeseedIntegratedDevReset(reset: TreeseedIntegratedDevResetPlan | null, options: Pick<TreeseedIntegratedDevOptions, 'json'>, deps: Pick<TreeseedIntegratedDevDependencies, 'write' | 'removePath' | 'stopMailpitContainers' | 'resetMarketPostgres'>): {
149
155
  actions: TreeseedIntegratedDevResetAction[];
150
156
  enabled: boolean;
151
157
  preserved: string[];
package/dist/dev.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, isAbsolute, resolve, sep } from "node:path";
4
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { setTimeout as delay } from "node:timers/promises";
7
7
  import { DatabaseSync } from "node:sqlite";
@@ -31,6 +31,10 @@ const TREESEED_DEFAULT_API_PORT = 3e3;
31
31
  const TREESEED_DEFAULT_LOCAL_SMTP_HOST = "127.0.0.1";
32
32
  const TREESEED_DEFAULT_LOCAL_SMTP_PORT = 1025;
33
33
  const TREESEED_DEFAULT_MAILPIT_UI_PORT = 8025;
34
+ const TREESEED_DEFAULT_MARKET_POSTGRES_PORT = 55432;
35
+ const TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER = "treeseed-market-local-postgres";
36
+ const TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME = "treeseed-market-local-postgres-data";
37
+ const TREESEED_DEFAULT_MARKET_POSTGRES_URL = `postgres://treeseed:treeseed@127.0.0.1:${TREESEED_DEFAULT_MARKET_POSTGRES_PORT}/market_local`;
34
38
  const DEV_RELOAD_FILE = "public/__treeseed/dev-reload.json";
35
39
  const DEV_RUNTIME_DIR = ".treeseed/generated/dev";
36
40
  const DEV_RUNTIME_LEGACY_FILE = ".treeseed/generated/dev/runtime.json";
@@ -172,6 +176,15 @@ function webUrlFor(host, port) {
172
176
  return `http://${browserHost(host)}:${port}`;
173
177
  }
174
178
  const CANONICAL_COMMAND_IDS = ["web", "api", "manager", "worker", "agents"];
179
+ const MARKET_DEV_COMMAND_IDS = ["web", "api", "market-runner"];
180
+ function isMarketWorkspace(tenantRoot) {
181
+ try {
182
+ const pkg = JSON.parse(readFileSync(resolve(tenantRoot, "package.json"), "utf8"));
183
+ return pkg.name === "@treeseed/market" && existsSync(resolve(tenantRoot, "src/api/server.js")) && existsSync(resolve(tenantRoot, "src/market-operations-runner/entrypoint.js"));
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
175
188
  function surfaceCommandIds(surface) {
176
189
  switch (surface) {
177
190
  case "web":
@@ -226,6 +239,15 @@ function dockerComposeIsAvailable(env) {
226
239
  });
227
240
  return (result.status ?? 1) === 0;
228
241
  }
242
+ function dockerIsAvailable(env) {
243
+ const docker = resolveTreeseedToolBinary("docker", { env });
244
+ if (!docker) return false;
245
+ const result = spawnSync(docker, ["info"], {
246
+ encoding: "utf8",
247
+ env
248
+ });
249
+ return (result.status ?? 1) === 0;
250
+ }
229
251
  function resetActionForPath(id, label, path) {
230
252
  return {
231
253
  id,
@@ -236,6 +258,58 @@ function resetActionForPath(id, label, path) {
236
258
  detail: existsSync(path) ? void 0 : "Path does not exist."
237
259
  };
238
260
  }
261
+ function uniqueResetActions(actions) {
262
+ const seen = /* @__PURE__ */ new Set();
263
+ return actions.filter((action) => {
264
+ const key = action.kind === "path" ? `${action.kind}:${action.path}` : `${action.kind}:${action.id}`;
265
+ if (seen.has(key)) return false;
266
+ seen.add(key);
267
+ return true;
268
+ });
269
+ }
270
+ function optionalResetActionForPath(id, label, path) {
271
+ return existsSync(path) ? resetActionForPath(id, label, path) : null;
272
+ }
273
+ function pathContains(parent, child) {
274
+ const diff = relative(parent, child);
275
+ return diff === "" || diff.length > 0 && !diff.startsWith("..") && !isAbsolute(diff);
276
+ }
277
+ function knownLocalRuntimeStateResetActions(tenantRoot, activePersistTo) {
278
+ const localGeneratedWranglerState = resolve(tenantRoot, ".treeseed", "generated", "environments", "local", ".wrangler", "state", "v3");
279
+ const rootWranglerState = resolve(tenantRoot, ".wrangler", "state", "v3");
280
+ const stateRoots = [
281
+ optionalResetActionForPath(
282
+ "generated-wrangler-state",
283
+ "Remove generated local Wrangler runtime state",
284
+ localGeneratedWranglerState
285
+ ),
286
+ optionalResetActionForPath(
287
+ "root-wrangler-state",
288
+ "Remove root Wrangler runtime state",
289
+ rootWranglerState
290
+ )
291
+ ].filter((action) => Boolean(action));
292
+ const coveredByStateRoot = stateRoots.some((action) => action.path && pathContains(action.path, activePersistTo));
293
+ return uniqueResetActions([
294
+ ...coveredByStateRoot ? [] : [resetActionForPath("d1-state", "Remove active local D1 state", activePersistTo)],
295
+ ...stateRoots,
296
+ optionalResetActionForPath(
297
+ "legacy-local-sqlite",
298
+ "Remove legacy local SQLite state",
299
+ resolve(tenantRoot, ".treeseed", "generated", "environments", "local", "site-data.sqlite")
300
+ )
301
+ ].filter((action) => Boolean(action)));
302
+ }
303
+ function isTreeseedManagedMarketPostgresUrl(value) {
304
+ if (!value?.trim()) return true;
305
+ try {
306
+ const url = new URL(value);
307
+ const port = url.port || (url.protocol === "postgres:" || url.protocol === "postgresql:" ? "5432" : "");
308
+ return ["postgres:", "postgresql:"].includes(url.protocol) && ["127.0.0.1", "localhost"].includes(url.hostname) && port === String(TREESEED_DEFAULT_MARKET_POSTGRES_PORT) && url.pathname === "/market_local" && decodeURIComponent(url.username) === "treeseed";
309
+ } catch {
310
+ return false;
311
+ }
312
+ }
239
313
  function resolveLocalD1SqlitePath(persistTo) {
240
314
  if (/\.sqlite$/u.test(persistTo) && existsSync(persistTo)) {
241
315
  return persistTo;
@@ -302,10 +376,12 @@ function createTreeseedIntegratedDevResetPlan(options) {
302
376
  }
303
377
  const tenantRoot = options.tenantRoot;
304
378
  const d1PersistTo = options.env.TREESEED_API_D1_LOCAL_PERSIST_TO?.trim() || resolve(tenantRoot, ".wrangler", "state", "v3", "d1");
379
+ const marketWorkspace = isMarketWorkspace(tenantRoot);
380
+ const managedMarketPostgres = options.env.TREESEED_MARKET_LOCAL_POSTGRES_MANAGED === "true";
305
381
  return {
306
382
  enabled: true,
307
383
  actions: [
308
- resetActionForPath("d1-state", "Remove local D1 state", d1PersistTo),
384
+ ...knownLocalRuntimeStateResetActions(tenantRoot, d1PersistTo),
309
385
  {
310
386
  id: "mailpit",
311
387
  label: options.mailpitEnabled ? "Reset Mailpit email runtime" : "Skip Mailpit email runtime",
@@ -313,9 +389,23 @@ function createTreeseedIntegratedDevResetPlan(options) {
313
389
  status: options.mailpitEnabled ? "planned" : "skipped",
314
390
  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."
315
391
  },
392
+ ...marketWorkspace ? [{
393
+ id: "market-postgres",
394
+ label: managedMarketPostgres ? "Reset local Market PostgreSQL" : "Skip external Market PostgreSQL",
395
+ kind: "service",
396
+ status: managedMarketPostgres ? "planned" : "skipped",
397
+ detail: managedMarketPostgres ? "The Treeseed-managed Market PostgreSQL container, database, and volume will be removed and recreated on the next dev run." : "TREESEED_MARKET_DATABASE_URL points at an external database, so dev reset will not drop it."
398
+ }] : [],
316
399
  resetActionForPath("wrangler-tmp", "Remove Wrangler temporary output", resolve(tenantRoot, ".wrangler", "tmp")),
317
400
  resetActionForPath("worker-bundle", "Remove generated local worker bundle", resolve(tenantRoot, ".treeseed", "generated", "worker")),
318
- resetActionForPath("dev-reload", "Remove browser reload marker", resolve(tenantRoot, DEV_RELOAD_FILE))
401
+ {
402
+ id: "dev-reload",
403
+ label: "Refresh browser reload marker",
404
+ kind: "path",
405
+ path: resolve(tenantRoot, DEV_RELOAD_FILE),
406
+ status: "planned",
407
+ detail: "The browser reload marker will be recreated so open tabs do not poll a missing file after reset."
408
+ }
319
409
  ],
320
410
  preserved: [
321
411
  ".env*",
@@ -327,7 +417,8 @@ function createTreeseedIntegratedDevResetPlan(options) {
327
417
  ".treeseed/workflow",
328
418
  ".treeseed/workspace-links.json",
329
419
  "migrations",
330
- "node_modules"
420
+ "node_modules",
421
+ "Treeseed-managed local service containers"
331
422
  ]
332
423
  };
333
424
  }
@@ -390,7 +481,11 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
390
481
  ];
391
482
  }
392
483
  const hasWebCommand = planLike.commands.some((command) => command.id === "web");
393
- const hasLocalRuntimeCommand = planLike.commands.some((command) => command.id !== "web");
484
+ const hasLocalRuntimeCommand = planLike.commands.some(
485
+ (command) => command.id !== "web" && command.id !== "market-runner" && command.label !== "Treeseed Market API"
486
+ );
487
+ const hasMarketApiCommand = planLike.commands.some((command) => command.label === "Treeseed Market API");
488
+ const managedMarketPostgres = env.TREESEED_MARKET_LOCAL_POSTGRES_MANAGED === "true";
394
489
  const needsCloudflareLocalRuntime = usesCloudflareWebRuntime || hasLocalRuntimeCommand;
395
490
  const coreScripts = [
396
491
  ["starlight-patch", "Patch Starlight content path", "scripts/patch-starlight-content-path.ts", "dist/scripts/patch-starlight-content-path.js"],
@@ -406,6 +501,11 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
406
501
  "scripts/ensure-mailpit.ts",
407
502
  "dist/scripts/ensure-mailpit.js"
408
503
  );
504
+ const dockerReady = dockerIsAvailable(env);
505
+ const marketMigrateScript = existsSync(resolve(tenantRoot, "scripts/migrate-market-db.mjs")) ? {
506
+ command: process.execPath,
507
+ args: [resolve(tenantRoot, "scripts/migrate-market-db.mjs")]
508
+ } : null;
409
509
  const steps = [
410
510
  {
411
511
  id: "workspace-links",
@@ -413,6 +513,26 @@ function createSetupSteps(tenantRoot, setupMode, sdkPackageRoot, planLike, env,
413
513
  required: setupMode === "auto",
414
514
  status: "planned"
415
515
  },
516
+ ...hasMarketApiCommand && managedMarketPostgres ? [
517
+ {
518
+ id: "market-postgres",
519
+ label: "Start local Market PostgreSQL",
520
+ required: true,
521
+ status: dockerReady ? "planned" : "failed",
522
+ detail: dockerReady ? "Treeseed will manage the local Market PostgreSQL container automatically." : "Docker daemon is unavailable; local Market API requires managed PostgreSQL."
523
+ }
524
+ ] : [],
525
+ ...hasMarketApiCommand ? [
526
+ {
527
+ id: "market-migrations",
528
+ label: "Apply local Market database migrations",
529
+ required: true,
530
+ command: marketMigrateScript?.command,
531
+ args: marketMigrateScript?.args,
532
+ status: marketMigrateScript ? "planned" : "failed",
533
+ detail: marketMigrateScript ? void 0 : "Unable to resolve scripts/migrate-market-db.mjs."
534
+ }
535
+ ] : [],
416
536
  {
417
537
  id: "wrangler",
418
538
  label: "Verify Wrangler executable",
@@ -533,6 +653,48 @@ function createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost
533
653
  localRuntime: nodeLocalRuntime(config.label)
534
654
  };
535
655
  }
656
+ function createMarketApiCommand(tenantRoot, sharedEnv, apiHost, apiPort) {
657
+ return {
658
+ id: "api",
659
+ label: "Treeseed Market API",
660
+ command: process.execPath,
661
+ args: [resolve(tenantRoot, "src/api/server.js")],
662
+ cwd: tenantRoot,
663
+ env: {
664
+ ...sharedEnv,
665
+ HOST: apiHost,
666
+ PORT: String(apiPort),
667
+ TREESEED_ENVIRONMENT: sharedEnv.TREESEED_ENVIRONMENT ?? "local",
668
+ TREESEED_API_ENVIRONMENT: sharedEnv.TREESEED_API_ENVIRONMENT ?? "local",
669
+ TREESEED_API_REQUEST_LOGS: sharedEnv.TREESEED_API_REQUEST_LOGS ?? "true"
670
+ },
671
+ localRuntime: nodeLocalRuntime("Treeseed Market API")
672
+ };
673
+ }
674
+ function createMarketOperationsRunnerCommand(tenantRoot, sharedEnv) {
675
+ return {
676
+ id: "market-runner",
677
+ label: "Market Operations Runner",
678
+ command: process.execPath,
679
+ args: [
680
+ "--experimental-transform-types",
681
+ resolve(tenantRoot, "src/market-operations-runner/entrypoint.js"),
682
+ "--market",
683
+ "local",
684
+ "--watch",
685
+ "--operation",
686
+ "project:web_deployment",
687
+ "--poll-interval-ms",
688
+ "5000"
689
+ ],
690
+ cwd: tenantRoot,
691
+ env: {
692
+ ...sharedEnv,
693
+ TREESEED_PLATFORM_RUNNER_ENVIRONMENT: sharedEnv.TREESEED_PLATFORM_RUNNER_ENVIRONMENT ?? "local"
694
+ },
695
+ localRuntime: nodeLocalRuntime("Market Operations Runner")
696
+ };
697
+ }
536
698
  function createTreeseedIntegratedDevPlan(options = {}) {
537
699
  const tenantRoot = resolve(options.cwd ?? process.cwd());
538
700
  const surface = options.surface ?? "integrated";
@@ -549,18 +711,23 @@ function createTreeseedIntegratedDevPlan(options = {}) {
549
711
  const teamId = options.teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
550
712
  const apiBaseUrl = options.apiHost != null || options.apiPort != null ? `http://${apiHost}:${apiPort}` : mergedEnv.TREESEED_API_BASE_URL?.trim() || `http://${apiHost}:${apiPort}`;
551
713
  const selectedCommandIds = selectedSurfaceCommandIds(options);
552
- const webUrl = selectedCommandIds.includes("web") ? webUrlFor(webHost, webPort) : null;
714
+ const marketWorkspace = isMarketWorkspace(tenantRoot);
715
+ const effectiveCommandIds = marketWorkspace ? MARKET_DEV_COMMAND_IDS.filter((id) => selectedCommandIds.includes(id) || id === "market-runner" && selectedCommandIds.includes("api")) : selectedCommandIds;
716
+ const devResetId = options.reset ? String(Date.now()) : void 0;
717
+ const webUrl = effectiveCommandIds.includes("web") ? webUrlFor(webHost, webPort) : null;
553
718
  const sdkPackageRoot = resolvePackageRoot("@treeseed/sdk", tenantRoot);
554
719
  const agentPackageRoot = resolvePackageRootEnvOverride(mergedEnv, "TREESEED_AGENT_PACKAGE_ROOT", tenantRoot) ?? resolveOptionalPackageRoot("@treeseed/agent", tenantRoot);
555
720
  const cliPackageRoot = resolveOptionalPackageRoot("@treeseed/cli", tenantRoot);
556
721
  const deployConfig = loadDevDeployConfig(tenantRoot);
557
722
  const webLocalRuntime = selectWebLocalRuntime(deployConfig?.surfaces?.web, fallbackWebProviderFromDeployConfig(deployConfig), options.webRuntime);
558
723
  const usesCloudflareWebRuntime = webLocalRuntime.selected === "cloudflare-wrangler-local";
559
- const usesGeneratedLocalD1State = usesCloudflareWebRuntime || webLocalRuntime.provider === "cloudflare" || selectedCommandIds.some((id) => id !== "web");
724
+ const usesGeneratedLocalD1State = usesCloudflareWebRuntime || webLocalRuntime.provider === "cloudflare" || !marketWorkspace && effectiveCommandIds.some((id) => id !== "web");
560
725
  const localD1PersistTo = mergedEnv.TREESEED_API_D1_LOCAL_PERSIST_TO ?? (usesGeneratedLocalD1State ? resolve(tenantRoot, ".treeseed", "generated", "environments", "local", ".wrangler", "state", "v3", "d1") : resolve(tenantRoot, ".wrangler", "state", "v3", "d1"));
561
726
  const projectId = options.projectId ?? mergedEnv.TREESEED_PROJECT_ID ?? resolveSeededLocalProjectId(localD1PersistTo);
562
727
  const resolvedHostingTeamId = teamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID;
563
728
  const resolvedTeamId = mergedEnv.TREESEED_TEAM_ID ?? resolvedHostingTeamId ?? resolveSeededLocalTeamId(localD1PersistTo, projectId ?? null);
729
+ const marketDatabaseUrl = mergedEnv.TREESEED_MARKET_DATABASE_URL ?? TREESEED_DEFAULT_MARKET_POSTGRES_URL;
730
+ const managedMarketPostgres = marketWorkspace && isTreeseedManagedMarketPostgresUrl(marketDatabaseUrl);
564
731
  const webEntrypoint = resolveNodeEntrypoint(
565
732
  sdkPackageRoot,
566
733
  "scripts/tenant-astro-command.ts",
@@ -590,14 +757,28 @@ function createTreeseedIntegratedDevPlan(options = {}) {
590
757
  BETTER_AUTH_URL: mergedEnv.BETTER_AUTH_URL ?? webUrl,
591
758
  TREESEED_API_BASE_URL: apiBaseUrl,
592
759
  TREESEED_MARKET_API_BASE_URL: mergedEnv.TREESEED_MARKET_API_BASE_URL ?? apiBaseUrl,
760
+ TREESEED_API_REQUEST_LOGS: mergedEnv.TREESEED_API_REQUEST_LOGS ?? "true",
761
+ ...marketWorkspace ? {
762
+ TREESEED_MARKET_DATABASE_URL: marketDatabaseUrl,
763
+ TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER ?? TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER,
764
+ TREESEED_MARKET_LOCAL_POSTGRES_VOLUME: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_VOLUME ?? TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME,
765
+ TREESEED_MARKET_LOCAL_POSTGRES_PORT: mergedEnv.TREESEED_MARKET_LOCAL_POSTGRES_PORT ?? String(TREESEED_DEFAULT_MARKET_POSTGRES_PORT),
766
+ TREESEED_MARKET_LOCAL_POSTGRES_MANAGED: managedMarketPostgres ? "true" : "false"
767
+ } : {},
593
768
  TREESEED_PROJECT_ID: projectId ?? mergedEnv.TREESEED_PROJECT_ID,
594
769
  TREESEED_TEAM_ID: resolvedTeamId ?? mergedEnv.TREESEED_TEAM_ID,
595
770
  TREESEED_HOSTING_TEAM_ID: resolvedHostingTeamId ?? mergedEnv.TREESEED_HOSTING_TEAM_ID,
596
771
  TREESEED_API_D1_DATABASE_NAME: mergedEnv.TREESEED_API_D1_DATABASE_NAME ?? "SITE_DATA_DB",
597
772
  SITE_DATA_DB: mergedEnv.SITE_DATA_DB ?? "SITE_DATA_DB",
598
773
  TREESEED_API_D1_LOCAL_PERSIST_TO: localD1PersistTo,
774
+ TREESEED_WEB_SERVICE_ID: mergedEnv.TREESEED_WEB_SERVICE_ID ?? mergedEnv.TREESEED_API_WEB_SERVICE_ID ?? "web",
775
+ TREESEED_WEB_SERVICE_SECRET: mergedEnv.TREESEED_WEB_SERVICE_SECRET ?? mergedEnv.TREESEED_API_WEB_SERVICE_SECRET ?? "treeseed-web-service-dev-secret",
776
+ TREESEED_API_WEB_SERVICE_ID: mergedEnv.TREESEED_API_WEB_SERVICE_ID ?? mergedEnv.TREESEED_WEB_SERVICE_ID ?? "web",
777
+ TREESEED_API_WEB_SERVICE_SECRET: mergedEnv.TREESEED_API_WEB_SERVICE_SECRET ?? mergedEnv.TREESEED_WEB_SERVICE_SECRET ?? "treeseed-web-service-dev-secret",
778
+ TREESEED_PLATFORM_RUNNER_SECRET: mergedEnv.TREESEED_PLATFORM_RUNNER_SECRET ?? "treeseed-platform-runner-dev-secret",
599
779
  TREESEED_FORM_TOKEN_SECRET: mergedEnv.TREESEED_FORM_TOKEN_SECRET ?? "treeseed-local-form-token-secret",
600
780
  TREESEED_BETTER_AUTH_SECRET: mergedEnv.TREESEED_BETTER_AUTH_SECRET ?? "treeseed-local-better-auth-secret-minimum-32-characters",
781
+ ...devResetId ? { TREESEED_DEV_RESET_ID: devResetId } : {},
601
782
  TREESEED_SMTP_HOST: TREESEED_DEFAULT_LOCAL_SMTP_HOST,
602
783
  TREESEED_SMTP_PORT: String(TREESEED_DEFAULT_LOCAL_SMTP_PORT),
603
784
  TREESEED_SMTP_USERNAME: "",
@@ -617,7 +798,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
617
798
  sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD = sharedEnv.TREESEED_PUBLIC_DEV_WATCH_RELOAD || "true";
618
799
  }
619
800
  const commands = [];
620
- if (selectedCommandIds.includes("web")) {
801
+ if (effectiveCommandIds.includes("web")) {
621
802
  commands.push({
622
803
  id: "web",
623
804
  label: usesCloudflareWebRuntime ? "Cloudflare Wrangler UI" : "Astro UI",
@@ -628,11 +809,19 @@ function createTreeseedIntegratedDevPlan(options = {}) {
628
809
  localRuntime: webLocalRuntime
629
810
  });
630
811
  }
631
- if (selectedCommandIds.some((id) => id !== "web") && !agentPackageRoot) {
812
+ if (!marketWorkspace && effectiveCommandIds.some((id) => id !== "web") && !agentPackageRoot) {
632
813
  throw new Error("Unable to resolve @treeseed/agent for local API or agent service surfaces.");
633
814
  }
634
- for (const id of selectedCommandIds) {
815
+ for (const id of effectiveCommandIds) {
635
816
  if (id === "web") continue;
817
+ if (marketWorkspace && id === "api") {
818
+ commands.push(createMarketApiCommand(tenantRoot, sharedEnv, apiHost, apiPort));
819
+ continue;
820
+ }
821
+ if (marketWorkspace && id === "market-runner") {
822
+ commands.push(createMarketOperationsRunnerCommand(tenantRoot, sharedEnv));
823
+ continue;
824
+ }
636
825
  commands.push(createAgentCommand(id, tenantRoot, agentPackageRoot, sharedEnv, apiHost, apiPort));
637
826
  }
638
827
  const readyChecks = commands.map((command) => {
@@ -648,7 +837,7 @@ function createTreeseedIntegratedDevPlan(options = {}) {
648
837
  return {
649
838
  id: command.id,
650
839
  label: command.label,
651
- required: false,
840
+ required: command.id === "market-runner",
652
841
  strategy: "process"
653
842
  };
654
843
  });
@@ -680,7 +869,8 @@ function createTreeseedIntegratedDevPlan(options = {}) {
680
869
  ...commands.some((command) => command.id === "api") ? { api: nodeLocalRuntime("Treeseed API") } : {},
681
870
  ...commands.some((command) => command.id === "manager") ? { manager: nodeLocalRuntime("Manager") } : {},
682
871
  ...commands.some((command) => command.id === "worker") ? { worker: nodeLocalRuntime("Worker Runner") } : {},
683
- ...commands.some((command) => command.id === "agents") ? { agents: nodeLocalRuntime("Agents Loop") } : {}
872
+ ...commands.some((command) => command.id === "agents") ? { agents: nodeLocalRuntime("Agents Loop") } : {},
873
+ ...commands.some((command) => command.id === "market-runner") ? { marketRunner: nodeLocalRuntime("Market Operations Runner") } : {}
684
874
  },
685
875
  restartPolicy: {
686
876
  initialBackoffMs: INITIAL_RESTART_BACKOFF_MS,
@@ -742,6 +932,12 @@ function defaultInspectPortOwners(ports) {
742
932
  function defaultRemovePath(path) {
743
933
  rmSync(path, { recursive: true, force: true });
744
934
  }
935
+ function defaultResetMarketPostgres() {
936
+ return resetMarketPostgres(process.env, { spawnSync });
937
+ }
938
+ function defaultStopMarketPostgres() {
939
+ return stopMarketPostgres(process.env, { spawnSync });
940
+ }
745
941
  function createManagedDevProcess(command, child) {
746
942
  let resolveExit = () => {
747
943
  };
@@ -790,8 +986,7 @@ async function stopManagedProcess(managed, signal, killProcess, graceMs) {
790
986
  }
791
987
  }
792
988
  }
793
- function writeDevReloadStamp(projectRoot) {
794
- const outputPath = resolve(projectRoot, DEV_RELOAD_FILE);
989
+ function writeDevReloadStampPath(outputPath) {
795
990
  mkdirSync(dirname(outputPath), { recursive: true });
796
991
  writeFileSync(
797
992
  outputPath,
@@ -807,6 +1002,9 @@ function writeDevReloadStamp(projectRoot) {
807
1002
  "utf8"
808
1003
  );
809
1004
  }
1005
+ function writeDevReloadStamp(projectRoot) {
1006
+ writeDevReloadStampPath(resolve(projectRoot, DEV_RELOAD_FILE));
1007
+ }
810
1008
  function defaultWrite(line, stream) {
811
1009
  const target = stream === "stderr" ? process.stderr : process.stdout;
812
1010
  target.write(line);
@@ -1112,11 +1310,11 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
1112
1310
  return action;
1113
1311
  }
1114
1312
  if (action.kind === "service") {
1115
- const stopped = deps.stopMailpitContainers();
1313
+ const stopped = action.id === "market-postgres" ? deps.resetMarketPostgres() : deps.stopMailpitContainers();
1116
1314
  const result = {
1117
1315
  ...action,
1118
1316
  status: stopped ? "removed" : "failed",
1119
- detail: stopped ? "Mailpit container state was reset." : "Unable to stop or remove the Treeseed-managed Mailpit container."
1317
+ detail: stopped ? action.id === "market-postgres" ? "Market PostgreSQL database state was reset." : "Mailpit container and inbox state were reset." : action.id === "market-postgres" ? "Unable to stop or remove the Treeseed-managed Market PostgreSQL container and volume." : "Unable to remove the Treeseed-managed Mailpit container and inbox state."
1120
1318
  };
1121
1319
  emitEvent(options, deps.write, {
1122
1320
  type: "reset",
@@ -1127,6 +1325,36 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
1127
1325
  return result;
1128
1326
  }
1129
1327
  if (!action.path || !existsSync(action.path)) {
1328
+ if (action.id === "dev-reload" && action.path) {
1329
+ try {
1330
+ writeDevReloadStampPath(action.path);
1331
+ const result2 = {
1332
+ ...action,
1333
+ status: "refreshed",
1334
+ detail: action.path
1335
+ };
1336
+ emitEvent(options, deps.write, {
1337
+ type: "reset",
1338
+ status: result2.status,
1339
+ message: `${result2.label}: refreshed`,
1340
+ detail: result2
1341
+ });
1342
+ return result2;
1343
+ } catch (error) {
1344
+ const result2 = {
1345
+ ...action,
1346
+ status: "failed",
1347
+ detail: error instanceof Error ? error.message : String(error)
1348
+ };
1349
+ emitEvent(options, deps.write, {
1350
+ type: "reset",
1351
+ status: result2.status,
1352
+ message: `${result2.label}: failed`,
1353
+ detail: result2
1354
+ }, "stderr");
1355
+ return result2;
1356
+ }
1357
+ }
1130
1358
  const result = {
1131
1359
  ...action,
1132
1360
  status: "skipped",
@@ -1140,6 +1368,36 @@ function runTreeseedIntegratedDevReset(reset, options, deps) {
1140
1368
  });
1141
1369
  return result;
1142
1370
  }
1371
+ if (action.id === "dev-reload") {
1372
+ try {
1373
+ writeDevReloadStampPath(action.path);
1374
+ const result = {
1375
+ ...action,
1376
+ status: "refreshed",
1377
+ detail: action.path
1378
+ };
1379
+ emitEvent(options, deps.write, {
1380
+ type: "reset",
1381
+ status: result.status,
1382
+ message: `${result.label}: refreshed`,
1383
+ detail: result
1384
+ });
1385
+ return result;
1386
+ } catch (error) {
1387
+ const result = {
1388
+ ...action,
1389
+ status: "failed",
1390
+ detail: error instanceof Error ? error.message : String(error)
1391
+ };
1392
+ emitEvent(options, deps.write, {
1393
+ type: "reset",
1394
+ status: result.status,
1395
+ message: `${result.label}: failed`,
1396
+ detail: result
1397
+ }, "stderr");
1398
+ return result;
1399
+ }
1400
+ }
1143
1401
  try {
1144
1402
  deps.removePath(action.path);
1145
1403
  const result = {
@@ -1219,6 +1477,15 @@ function attachPrefixedLogReader(child, surface, options, write) {
1219
1477
  stderr: { suppressWorkerdBrokenPipeBlock: false }
1220
1478
  };
1221
1479
  function shouldSuppressLogLine(line, name) {
1480
+ if (!options.json && surface === "market-runner" && name === "stdout") {
1481
+ try {
1482
+ const parsed = JSON.parse(line);
1483
+ if (parsed.ok === true && parsed.claimed === false && parsed.operation == null) {
1484
+ return true;
1485
+ }
1486
+ } catch {
1487
+ }
1488
+ }
1222
1489
  const state = filterState[name];
1223
1490
  if (state.suppressWorkerdBrokenPipeBlock) {
1224
1491
  const trimmed = line.trim();
@@ -1310,6 +1577,99 @@ function runSetupStep(step, plan, deps) {
1310
1577
  detail: [timeoutDetail, result.stdout, result.stderr].filter(Boolean).join("\n").trim() || `Exited with ${result.status ?? 1}.`
1311
1578
  };
1312
1579
  }
1580
+ function marketPostgresConfig(env) {
1581
+ return {
1582
+ container: env.TREESEED_MARKET_LOCAL_POSTGRES_CONTAINER?.trim() || TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER,
1583
+ volume: env.TREESEED_MARKET_LOCAL_POSTGRES_VOLUME?.trim() || TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME,
1584
+ port: env.TREESEED_MARKET_LOCAL_POSTGRES_PORT?.trim() || String(TREESEED_DEFAULT_MARKET_POSTGRES_PORT),
1585
+ user: "treeseed",
1586
+ password: "treeseed",
1587
+ database: "market_local"
1588
+ };
1589
+ }
1590
+ function dockerBinary(env) {
1591
+ return resolveTreeseedToolBinary("docker", { env }) ?? "docker";
1592
+ }
1593
+ function spawnDocker(args, env, deps, timeout = 3e4) {
1594
+ return deps.spawnSync(dockerBinary(env), args, {
1595
+ cwd: process.cwd(),
1596
+ env,
1597
+ encoding: "utf8",
1598
+ timeout
1599
+ });
1600
+ }
1601
+ function dockerResultText(result) {
1602
+ return [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1603
+ }
1604
+ function dockerVolumeIsMissing(result) {
1605
+ if (result.error) return false;
1606
+ const text = dockerResultText(result).toLowerCase();
1607
+ return text.includes("no such volume") || text.includes("not found");
1608
+ }
1609
+ function ensureMarketPostgres(env, deps) {
1610
+ const config = marketPostgresConfig(env);
1611
+ const inspect = spawnDocker(["inspect", config.container], env, deps);
1612
+ if ((inspect.status ?? 1) !== 0) {
1613
+ const run = spawnDocker([
1614
+ "run",
1615
+ "-d",
1616
+ "--name",
1617
+ config.container,
1618
+ "-e",
1619
+ `POSTGRES_USER=${config.user}`,
1620
+ "-e",
1621
+ `POSTGRES_PASSWORD=${config.password}`,
1622
+ "-e",
1623
+ `POSTGRES_DB=${config.database}`,
1624
+ "-p",
1625
+ `127.0.0.1:${config.port}:5432`,
1626
+ "-v",
1627
+ `${config.volume}:/var/lib/postgresql/data`,
1628
+ "postgres:16"
1629
+ ], env, deps, 6e4);
1630
+ if ((run.status ?? 1) !== 0) {
1631
+ throw new Error(dockerResultText(run) || `Unable to start ${config.container}.`);
1632
+ }
1633
+ } else {
1634
+ const start = spawnDocker(["start", config.container], env, deps);
1635
+ if ((start.status ?? 1) !== 0) {
1636
+ throw new Error(dockerResultText(start) || `Unable to start existing ${config.container}.`);
1637
+ }
1638
+ }
1639
+ const startedAt = Date.now();
1640
+ let last = "";
1641
+ while (Date.now() - startedAt < 45e3) {
1642
+ const ready = spawnDocker(["exec", config.container, "pg_isready", "-U", config.user, "-d", config.database], env, deps, 5e3);
1643
+ last = dockerResultText(ready);
1644
+ if ((ready.status ?? 1) === 0) {
1645
+ const query = spawnDocker(["exec", config.container, "psql", "-U", config.user, "-d", config.database, "-c", "SELECT 1"], env, deps, 5e3);
1646
+ last = dockerResultText(query) || last;
1647
+ if ((query.status ?? 1) === 0) {
1648
+ return `Market PostgreSQL is ready at 127.0.0.1:${config.port} (${config.container}).`;
1649
+ }
1650
+ }
1651
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
1652
+ }
1653
+ throw new Error(last || `Timed out waiting for ${config.container} to accept connections.`);
1654
+ }
1655
+ function resetMarketPostgres(env, deps) {
1656
+ const config = marketPostgresConfig(env);
1657
+ spawnDocker(["rm", "-f", config.container], env, deps, 3e4);
1658
+ const existingVolume = spawnDocker(["volume", "inspect", config.volume], env, deps, 3e4);
1659
+ if ((existingVolume.status ?? 1) !== 0) {
1660
+ return dockerVolumeIsMissing(existingVolume);
1661
+ }
1662
+ const volume = spawnDocker(["volume", "rm", config.volume], env, deps, 3e4);
1663
+ return (volume.status ?? 1) === 0 || dockerVolumeIsMissing(volume);
1664
+ }
1665
+ function stopMarketPostgres(env, deps) {
1666
+ if (env.TREESEED_MARKET_LOCAL_POSTGRES_MANAGED !== "true") {
1667
+ return true;
1668
+ }
1669
+ const config = marketPostgresConfig(env);
1670
+ const result = spawnDocker(["rm", "-f", config.container], env, deps, 3e4);
1671
+ return (result.status ?? 1) === 0;
1672
+ }
1313
1673
  function runLocalSetup(plan, options, deps) {
1314
1674
  const results = [];
1315
1675
  if (plan.setupMode === "off") {
@@ -1355,6 +1715,23 @@ function runLocalSetup(plan, options, deps) {
1355
1715
  status: step.required ? "failed" : "degraded",
1356
1716
  detail: "Wrangler was not found. Run `npx trsd install --json` and retry `npx trsd dev`."
1357
1717
  };
1718
+ } else if (step.id === "market-postgres") {
1719
+ if (plan.setupMode === "check") {
1720
+ result = { ...step, status: "skipped", detail: "Local Market PostgreSQL startup was checked in non-mutating mode." };
1721
+ } else if (step.status === "failed") {
1722
+ result = step;
1723
+ } else {
1724
+ try {
1725
+ const detail = ensureMarketPostgres({ ...process.env, ...plan.commands[0]?.env }, deps);
1726
+ result = { ...step, status: "completed", detail };
1727
+ } catch (error) {
1728
+ result = {
1729
+ ...step,
1730
+ status: "failed",
1731
+ detail: error instanceof Error ? error.message : String(error)
1732
+ };
1733
+ }
1734
+ }
1358
1735
  } else if (step.id === "wrangler-config") {
1359
1736
  if (plan.setupMode === "check") {
1360
1737
  result = { ...step, status: "skipped", detail: "Local Wrangler config generation was checked in non-mutating mode." };
@@ -1452,6 +1829,8 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1452
1829
  const prepareEnvironment = deps.prepareEnvironment ?? defaultPrepareEnvironment;
1453
1830
  const removePath = deps.removePath ?? defaultRemovePath;
1454
1831
  const stopMailpit = deps.stopMailpitContainers ?? stopKnownMailpitContainers;
1832
+ const resetMarketPostgresContainer = deps.resetMarketPostgres ?? defaultResetMarketPostgres;
1833
+ const stopMarketPostgresContainer = deps.stopMarketPostgres ?? defaultStopMarketPostgres;
1455
1834
  const inspectPortOwners = deps.inspectPortOwners ?? defaultInspectPortOwners;
1456
1835
  prepareEnvironment(tenantRoot);
1457
1836
  const plan = createTreeseedIntegratedDevPlan({
@@ -1479,7 +1858,8 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1479
1858
  const resetResults = runTreeseedIntegratedDevReset(plan.reset, options, {
1480
1859
  write,
1481
1860
  removePath,
1482
- stopMailpitContainers: stopMailpit
1861
+ stopMailpitContainers: stopMailpit,
1862
+ resetMarketPostgres: deps.resetMarketPostgres ? resetMarketPostgresContainer : () => resetMarketPostgres(plan.commands[0]?.env ?? process.env, { spawnSync: spawnSyncProcess })
1483
1863
  });
1484
1864
  const failedReset = resetResults?.actions.find((action) => action.status === "failed");
1485
1865
  if (failedReset) {
@@ -1539,6 +1919,18 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1539
1919
  await Promise.all(
1540
1920
  [...children.values()].map((managed) => stopManagedProcess(managed, "SIGTERM", killProcess, shutdownGraceMs))
1541
1921
  );
1922
+ const marketEnv = plan.commands[0]?.env ?? process.env;
1923
+ const shouldStopMarketPostgres = marketEnv.TREESEED_MARKET_LOCAL_POSTGRES_MANAGED === "true";
1924
+ if (shouldStopMarketPostgres) {
1925
+ const stopped = deps.stopMarketPostgres ? stopMarketPostgresContainer() : stopMarketPostgres(marketEnv, { spawnSync: spawnSyncProcess });
1926
+ if (!stopped) {
1927
+ emitEvent(options, write, {
1928
+ type: "shutdown",
1929
+ status: "degraded",
1930
+ message: "Unable to stop the managed local Market PostgreSQL container."
1931
+ }, "stderr");
1932
+ }
1933
+ }
1542
1934
  children.clear();
1543
1935
  for (const dispose of disposers) {
1544
1936
  dispose();
@@ -1749,6 +2141,9 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1749
2141
  restartIds.add(id);
1750
2142
  }
1751
2143
  }
2144
+ if ((change.tenantApiChanged || change.sdkChanged) && commandsById.has("market-runner")) {
2145
+ restartIds.add("market-runner");
2146
+ }
1752
2147
  for (const id of restartIds) {
1753
2148
  await restartCommand(id);
1754
2149
  }
@@ -1812,7 +2207,7 @@ async function runTreeseedIntegratedDev(options = {}, deps = {}) {
1812
2207
  surface: check.id,
1813
2208
  status: ready ? "ready" : "degraded",
1814
2209
  url: check.url,
1815
- message: `${check.label} is ${ready ? "ready" : "degraded"}.`
2210
+ message: `${check.label} is ${ready ? "ready" : "degraded"}${check.url ? ` at ${check.url}` : ""}.`
1816
2211
  });
1817
2212
  }
1818
2213
  readinessInProgress = false;
@@ -1865,6 +2260,10 @@ export {
1865
2260
  TREESEED_DEFAULT_LOCAL_SMTP_HOST,
1866
2261
  TREESEED_DEFAULT_LOCAL_SMTP_PORT,
1867
2262
  TREESEED_DEFAULT_MAILPIT_UI_PORT,
2263
+ TREESEED_DEFAULT_MARKET_POSTGRES_CONTAINER,
2264
+ TREESEED_DEFAULT_MARKET_POSTGRES_PORT,
2265
+ TREESEED_DEFAULT_MARKET_POSTGRES_URL,
2266
+ TREESEED_DEFAULT_MARKET_POSTGRES_VOLUME,
1868
2267
  TREESEED_DEFAULT_WEB_HOST,
1869
2268
  TREESEED_DEFAULT_WEB_PORT,
1870
2269
  createTreeseedIntegratedDevPlan,
@@ -162,7 +162,7 @@ const plainIntroText = displayValue(introText ?? entry.motivation);
162
162
  </dl>
163
163
  </aside>
164
164
  </header>
165
- <div class="prose-karyon max-w-3xl">
165
+ <div class="prose max-w-3xl">
166
166
  <slot />
167
167
  </div>
168
168
  {relationSections.length > 0 && (
@@ -19,7 +19,7 @@ const { entry, currentPath } = Astro.props;
19
19
  </div>
20
20
  </div>
21
21
  <div class="grid gap-6 md:grid-cols-[1.25fr_0.75fr]">
22
- <div class="prose-karyon">
22
+ <div class="prose">
23
23
  <slot />
24
24
  </div>
25
25
  <div class="border border-[color:var(--ts-color-border)] bg-[color:var(--ts-color-surface)] p-5">
@@ -17,7 +17,7 @@ const { entry, currentPath } = Astro.props;
17
17
  <h1 class="max-w-4xl font-serif text-5xl font-bold tracking-tight text-[color:var(--ts-color-text)] md:text-7xl">{entry.title}</h1>
18
18
  <p class="max-w-4xl text-xl leading-10 text-[color:var(--ts-color-text-muted)]">{entry.summary}</p>
19
19
  </section>
20
- <article class="prose-karyon max-w-4xl">
20
+ <article class="prose max-w-4xl">
21
21
  <slot />
22
22
  </article>
23
23
  </div>
@@ -19,7 +19,7 @@ const { note } = Astro.props;
19
19
  <p class="text-sm uppercase tracking-[0.14em] text-[color:var(--ts-color-accent-strong)]">{note.tags.join(' / ')}</p>
20
20
  )}
21
21
  </div>
22
- <div class="prose-karyon">
22
+ <div class="prose">
23
23
  <slot />
24
24
  </div>
25
25
  </article>
@@ -45,7 +45,7 @@ const {
45
45
  )}
46
46
  </div>
47
47
  <div class="grid gap-6 md:grid-cols-[1.3fr_0.7fr]">
48
- <div class="prose-karyon">
48
+ <div class="prose">
49
49
  <slot />
50
50
  </div>
51
51
  <div class="space-y-6">
@@ -92,7 +92,7 @@ const download = activeBook
92
92
  <p class="max-w-3xl text-xl leading-10 text-[color:var(--ts-color-text-muted)]">{document.entry.data.summary}</p>
93
93
  )}
94
94
  </header>
95
- <div class="prose-karyon max-w-none">
95
+ <div class="prose max-w-none">
96
96
  <PublishedContentBody html={document.html} />
97
97
  </div>
98
98
  </article>
@@ -79,7 +79,7 @@ const ungroupedEntries = (docsTree ?? []).filter((entry) =>
79
79
  <p class="max-w-3xl text-xl leading-10 text-[color:var(--ts-color-text-muted)]">{document.entry.data.summary}</p>
80
80
  )}
81
81
  </header>
82
- <div class="prose-karyon max-w-none">
82
+ <div class="prose max-w-none">
83
83
  <PublishedContentBody html={document.html} />
84
84
  </div>
85
85
  </article>
@@ -3,6 +3,7 @@ import '../../styles/global.css';
3
3
  import '../../styles/theme.css';
4
4
  import '../../styles/ui.css';
5
5
  import '../../styles/forms.css';
6
+ import { ClientRouter } from 'astro:transitions';
6
7
  import ThemeScript from '../../components/ui/theme/ThemeScript.astro';
7
8
  import ThemeSelector from '../../components/ui/theme/ThemeSelector.astro';
8
9
  import Button from '../../components/ui/forms/Button.astro';
@@ -85,6 +86,7 @@ const tableRows = [
85
86
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
86
87
  <title>TreeSeed UI Catalog</title>
87
88
  <ThemeScript defaultScheme={selected.scheme} defaultMode={selected.mode} />
89
+ <ClientRouter />
88
90
  </head>
89
91
  <body>
90
92
  <main class="ts-ui-catalog">
@@ -43,7 +43,7 @@
43
43
 
44
44
  .ts-control {
45
45
  appearance: none;
46
- background: var(--ts-color-surface);
46
+ background-color: var(--ts-color-surface);
47
47
  border: 1px solid var(--ts-color-border);
48
48
  border-radius: var(--ts-radius-md);
49
49
  color: var(--ts-color-text);
@@ -71,22 +71,28 @@
71
71
 
72
72
  .ts-control:disabled,
73
73
  .ts-control:read-only {
74
- background: var(--ts-color-surface-muted);
74
+ background-color: var(--ts-color-surface-muted);
75
75
  color: var(--ts-color-text-muted);
76
76
  }
77
77
 
78
- .ts-control--select {
78
+ .ts-control--select,
79
+ select.ts-control {
80
+ appearance: none;
81
+ background-color: var(--ts-color-surface);
79
82
  background-image:
83
+ linear-gradient(var(--ts-color-border), var(--ts-color-border)),
80
84
  linear-gradient(45deg, transparent 50%, currentColor 50%),
81
85
  linear-gradient(135deg, currentColor 50%, transparent 50%);
82
86
  background-position:
83
- calc(100% - 1rem) 50%,
84
- calc(100% - 0.7rem) 50%;
85
- background-size:
86
- 0.32rem 0.32rem,
87
- 0.32rem 0.32rem;
87
+ calc(100% - 2rem) 50%,
88
+ calc(100% - 1.17rem) calc(50% - 0.08rem),
89
+ calc(100% - 0.86rem) calc(50% - 0.08rem);
88
90
  background-repeat: no-repeat;
89
- padding-right: 2rem;
91
+ background-size:
92
+ 1px 1.2rem,
93
+ 0.42rem 0.42rem,
94
+ 0.42rem 0.42rem;
95
+ padding-right: 2.5rem;
90
96
  }
91
97
 
92
98
  .ts-control--textarea {
@@ -1,88 +1,88 @@
1
- .prose-karyon {
1
+ .prose {
2
2
  color: var(--ts-color-text);
3
3
  font-size: 1.08rem;
4
4
  }
5
5
 
6
- .prose-karyon h1,
7
- .prose-karyon h2,
8
- .prose-karyon h3 {
6
+ .prose h1,
7
+ .prose h2,
8
+ .prose h3 {
9
9
  color: var(--ts-color-text);
10
10
  line-height: 1.12;
11
11
  font-weight: 700;
12
12
  }
13
13
 
14
- .prose-karyon h1 {
14
+ .prose h1 {
15
15
  font-size: clamp(2.4rem, 5vw, 4.1rem);
16
16
  margin-bottom: 1.2rem;
17
17
  font-family: var(--font-serif);
18
18
  }
19
19
 
20
- .prose-karyon h2 {
20
+ .prose h2 {
21
21
  font-size: clamp(1.85rem, 2.8vw, 2.5rem);
22
22
  margin-top: 3.1rem;
23
23
  margin-bottom: 1rem;
24
24
  }
25
25
 
26
- .prose-karyon h3 {
26
+ .prose h3 {
27
27
  font-size: 1.32rem;
28
28
  margin-top: 2rem;
29
29
  margin-bottom: 0.7rem;
30
30
  }
31
31
 
32
- .prose-karyon p,
33
- .prose-karyon li {
32
+ .prose p,
33
+ .prose li {
34
34
  color: var(--ts-color-text-muted);
35
35
  line-height: 1.85;
36
36
  }
37
37
 
38
- .prose-karyon p {
38
+ .prose p {
39
39
  margin: 1rem 0 1.2rem;
40
40
  }
41
41
 
42
- .prose-karyon p + p {
42
+ .prose p + p {
43
43
  margin-top: 1.35rem;
44
44
  }
45
45
 
46
- .prose-karyon a {
46
+ .prose a {
47
47
  color: var(--ts-color-accent-strong);
48
48
  text-decoration: underline;
49
49
  text-underline-offset: 0.16em;
50
50
  font-weight: 600;
51
51
  }
52
52
 
53
- .prose-karyon strong {
53
+ .prose strong {
54
54
  color: var(--ts-color-text);
55
55
  }
56
56
 
57
- .prose-karyon ul,
58
- .prose-karyon ol {
57
+ .prose ul,
58
+ .prose ol {
59
59
  padding-left: 1.3rem;
60
60
  }
61
61
 
62
- .prose-karyon blockquote {
62
+ .prose blockquote {
63
63
  border-left: 4px solid var(--ts-color-accent);
64
64
  padding-left: 1rem;
65
65
  color: var(--ts-color-text-muted);
66
66
  }
67
67
 
68
- .prose-karyon code {
68
+ .prose code {
69
69
  background: var(--ts-color-info-soft);
70
70
  border: 1px solid var(--ts-color-info-border);
71
71
  border-radius: 0.5rem;
72
72
  padding: 0.1rem 0.35rem;
73
73
  }
74
74
 
75
- .prose-karyon hr {
75
+ .prose hr {
76
76
  border-color: var(--ts-color-border);
77
77
  margin: 2.4rem 0;
78
78
  }
79
79
 
80
- .prose-karyon :where(table) {
80
+ .prose :where(table) {
81
81
  width: 100%;
82
82
  border-collapse: collapse;
83
83
  }
84
84
 
85
- .prose-karyon :where(th, td) {
85
+ .prose :where(th, td) {
86
86
  border-bottom: 1px solid var(--ts-color-border);
87
87
  padding: 0.85rem 0.75rem;
88
88
  text-align: left;
@@ -119,16 +119,30 @@ body {
119
119
  }
120
120
 
121
121
  .ts-theme-selector__field select {
122
- width: 100%;
123
- min-height: 2.25rem;
122
+ appearance: none;
123
+ background-color: var(--ts-color-surface);
124
+ background-image:
125
+ linear-gradient(var(--ts-color-border), var(--ts-color-border)),
126
+ linear-gradient(45deg, transparent 50%, currentColor 50%),
127
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
128
+ background-position:
129
+ calc(100% - 1.7rem) 50%,
130
+ calc(100% - 1rem) calc(50% - 0.08rem),
131
+ calc(100% - 0.72rem) calc(50% - 0.08rem);
132
+ background-repeat: no-repeat;
133
+ background-size:
134
+ 1px 1.05rem,
135
+ 0.36rem 0.36rem,
136
+ 0.36rem 0.36rem;
124
137
  border: 1px solid var(--ts-color-border-strong);
125
138
  border-radius: var(--ts-radius-sm);
126
- background: var(--ts-color-surface);
127
139
  color: var(--ts-color-text);
128
140
  font: inherit;
129
141
  font-size: 0.86rem;
130
142
  font-weight: 700;
131
- padding: 0.42rem 1.75rem 0.42rem 0.62rem;
143
+ min-height: 2.25rem;
144
+ padding: 0.42rem 2.15rem 0.42rem 0.62rem;
145
+ width: 100%;
132
146
  }
133
147
 
134
148
  .ts-theme-selector__field select:focus-visible {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/core",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "description": "Treeseed web framework package for Astro/Starlight site runtimes.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -70,7 +70,7 @@
70
70
  "@astrojs/sitemap": "3.7.0",
71
71
  "@astrojs/starlight": "0.37.6",
72
72
  "@tailwindcss/vite": "^4.1.4",
73
- "@treeseed/sdk": "github:treeseed-ai/sdk#0.10.12",
73
+ "@treeseed/sdk": "github:treeseed-ai/sdk#0.10.13",
74
74
  "astro": "^5.6.1",
75
75
  "esbuild": "^0.28.0",
76
76
  "katex": "^0.16.22",