@sudajs/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,11 +7,9 @@ import { fileURLToPath, pathToFileURL } from 'url';
7
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, createThemeAgentManifest, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
10
- import { extractLayoutChrome, ThemeRender } from '@sudajs/theme-engine/server';
10
+ import { FileSystemThemeRegistry } from '@sudajs/theme-engine/server';
11
11
  import { Command } from 'commander';
12
12
  import { build, context } from 'esbuild';
13
- import { createElement } from 'react';
14
- import { renderToStaticMarkup } from 'react-dom/server';
15
13
  import { z } from 'zod';
16
14
  import os from 'os';
17
15
 
@@ -43,9 +41,42 @@ async function clearAuthConfig() {
43
41
  }
44
42
  }
45
43
  }
44
+ function protocolForHost(host) {
45
+ return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
46
+ }
47
+ var CliAuthExpiredError = class extends Error {
48
+ code;
49
+ constructor(code, message) {
50
+ super(message);
51
+ this.name = "CliAuthExpiredError";
52
+ this.code = code;
53
+ }
54
+ };
55
+ async function cliAuthFetch(pathname, init = {}) {
56
+ const config = await readAuthConfig();
57
+ if (!config) {
58
+ throw new CliAuthExpiredError(
59
+ "unauthorized",
60
+ "Not logged in. Run `suda auth login` to authenticate first."
61
+ );
62
+ }
63
+ const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
64
+ const headers = new Headers(init.headers);
65
+ headers.set("Authorization", `Bearer ${config.sessionToken}`);
66
+ const res = await fetch(`${baseUrl}${pathname}`, { ...init, headers });
67
+ if (res.status === 401) {
68
+ const body = await res.clone().json().catch(() => ({}));
69
+ const code = ["token_invalid", "token_revoked", "token_expired"].includes(body.error ?? "") ? body.error : "unauthorized";
70
+ await clearAuthConfig();
71
+ throw new CliAuthExpiredError(
72
+ code,
73
+ body.message ?? "Authentication is no longer valid. Run `suda auth login` to authenticate again."
74
+ );
75
+ }
76
+ return res;
77
+ }
46
78
  async function login(host = "app.sudayun.cn") {
47
- const protocol = host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
48
- const baseUrl = `${protocol}://${host}`;
79
+ const baseUrl = `${protocolForHost(host)}://${host}`;
49
80
  console.log(`Requesting device authorization from ${baseUrl}...`);
50
81
  const deviceRes = await fetch(`${baseUrl}/api/cli-auth/device`, {
51
82
  method: "POST"
@@ -97,30 +128,26 @@ async function status() {
97
128
  console.log("Not logged in. Run `suda auth login` to authenticate.");
98
129
  return;
99
130
  }
100
- const protocol = config.host.includes("localhost") || config.host.includes("127.0.0.1") ? "http" : "https";
101
131
  let res;
102
132
  try {
103
- res = await fetch(`${protocol}://${config.host}/api/auth/get-session`, {
104
- headers: {
105
- Authorization: `Bearer ${config.sessionToken}`
106
- }
107
- });
108
- } catch {
133
+ res = await cliAuthFetch("/api/cli-auth/whoami");
134
+ } catch (err) {
135
+ if (err instanceof CliAuthExpiredError) {
136
+ console.log(err.message);
137
+ return;
138
+ }
109
139
  console.error(`Failed to connect to ${config.host}.`);
110
140
  return;
111
141
  }
112
142
  if (!res.ok) {
113
- console.log("Session expired or invalid. Please run `suda auth login` again.");
114
- await clearAuthConfig();
115
- return;
116
- }
117
- const session = await res.json();
118
- if (!session?.user) {
119
- console.log("Session expired or invalid. Please run `suda auth login` again.");
120
- await clearAuthConfig();
143
+ console.error(
144
+ `Failed to verify session (${res.status} ${res.statusText}). Try again later.`
145
+ );
121
146
  return;
122
147
  }
123
- console.log(`Logged in as ${session.user.email || session.user.name} on ${config.host}`);
148
+ const data = await res.json();
149
+ const label = data.user?.email ?? data.user?.name ?? "your account";
150
+ console.log(`Logged in as ${label} on ${config.host}`);
124
151
  }
125
152
  async function logout() {
126
153
  const config = await readAuthConfig();
@@ -128,6 +155,17 @@ async function logout() {
128
155
  console.log("Not logged in.");
129
156
  return;
130
157
  }
158
+ try {
159
+ await cliAuthFetch("/api/cli-auth/revoke", { method: "POST" });
160
+ } catch (err) {
161
+ if (err instanceof CliAuthExpiredError) {
162
+ console.log("Logged out (token was already invalid).");
163
+ return;
164
+ }
165
+ console.warn(
166
+ `Could not contact ${config.host} to revoke token. The token will be cleared locally only.`
167
+ );
168
+ }
131
169
  await clearAuthConfig();
132
170
  console.log("Logged out successfully.");
133
171
  }
@@ -327,7 +365,7 @@ async function readJsonIfExists(filePath) {
327
365
  }
328
366
  return readJson(filePath);
329
367
  }
330
- function protocolForHost(host) {
368
+ function protocolForHost2(host) {
331
369
  return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
332
370
  }
333
371
  async function requireCliBaseUrl() {
@@ -336,7 +374,7 @@ async function requireCliBaseUrl() {
336
374
  throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
337
375
  }
338
376
  return {
339
- baseUrl: `${protocolForHost(config.host)}://${config.host}`,
377
+ baseUrl: `${protocolForHost2(config.host)}://${config.host}`,
340
378
  token: config.sessionToken
341
379
  };
342
380
  }
@@ -526,11 +564,12 @@ function runThemeCheck(theme) {
526
564
  }
527
565
  return result.ok;
528
566
  }
529
- async function watchTheme(root, skipThemeBuild) {
567
+ async function watchTheme(root, skipThemeBuild, port) {
530
568
  if (!skipThemeBuild) {
531
569
  await runThemePackageBuild(root);
532
570
  }
533
571
  await buildServerBundle(root);
572
+ const theme = await validateTheme(root);
534
573
  const entryPoint = await findClientEntry(root);
535
574
  await mkdir(path2.join(root, "dist"), { recursive: true });
536
575
  const hostReactShimPlugin = createHostReactShimPlugin();
@@ -549,7 +588,8 @@ async function watchTheme(root, skipThemeBuild) {
549
588
  treeShaking: true
550
589
  });
551
590
  await context$1.watch();
552
- console.log("watching theme runtime; press Ctrl+C to stop");
591
+ const handle = await startPreviewServer(theme, port);
592
+ console.log(`watching theme runtime; previewing at ${handle.url}`);
553
593
  await new Promise(() => void 0);
554
594
  }
555
595
  function createWatchLogPlugin(label) {
@@ -573,27 +613,65 @@ function createWatchLogPlugin(label) {
573
613
  }
574
614
  };
575
615
  }
576
- function pickStarterPage(theme) {
616
+ var PREVIEW_PUBLIC_BASE_PATH = "/api/themes";
617
+ function pickStarterSlug(theme) {
577
618
  const pages = theme.module.starterPages;
578
619
  if (pages.length === 0) {
579
620
  return null;
580
621
  }
581
- return pages.find((page) => page.isHome) ?? pages[0] ?? null;
622
+ return (pages.find((page) => page.isHome) ?? pages[0])?.slug ?? null;
623
+ }
624
+ function findStarterPage(theme, slug) {
625
+ return theme.module.starterPages.find((page) => page.slug === slug) ?? null;
582
626
  }
583
627
  function escapeHtml(value) {
584
628
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
585
629
  }
586
- function renderStarterPageHtml(theme, page) {
587
- const chrome = extractLayoutChrome(theme.module.defaultLayout);
588
- const body = renderToStaticMarkup(
589
- createElement(ThemeRender, {
630
+ function createPreviewAssetResolver(resolveAssetPath) {
631
+ return (value) => {
632
+ return resolveAssetPath(value, {
633
+ legacyResolve: (key) => {
634
+ if (key.startsWith("themes/")) {
635
+ return `${PREVIEW_PUBLIC_BASE_PATH}/${key.slice("themes/".length)}`;
636
+ }
637
+ return void 0;
638
+ }
639
+ });
640
+ };
641
+ }
642
+ async function loadThemeScopedRenderer(themeRoot) {
643
+ const themeRequire = createRequire(path2.join(themeRoot, "package.json"));
644
+ const reactPath = themeRequire.resolve("react");
645
+ const reactDomServerPath = themeRequire.resolve("react-dom/server");
646
+ const themeEngineServerPath = themeRequire.resolve("@sudajs/theme-engine/server");
647
+ const [reactModule, reactDomServerModule, themeEngineServerModule] = await Promise.all([
648
+ import(pathToFileURL(reactPath).href),
649
+ import(pathToFileURL(reactDomServerPath).href),
650
+ import(pathToFileURL(themeEngineServerPath).href)
651
+ ]);
652
+ return {
653
+ ThemeRender: themeEngineServerModule.ThemeRender,
654
+ extractLayoutChrome: themeEngineServerModule.extractLayoutChrome,
655
+ resolveAssetPath: themeEngineServerModule.resolveAssetPath,
656
+ createElement: reactModule.createElement,
657
+ renderToStaticMarkup: reactDomServerModule.renderToStaticMarkup
658
+ };
659
+ }
660
+ function renderStarterPageHtml(theme, resolved, page, renderer) {
661
+ const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
662
+ const metadata = {
663
+ resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath)
664
+ };
665
+ const body = renderer.renderToStaticMarkup(
666
+ renderer.createElement(renderer.ThemeRender, {
590
667
  theme: theme.module,
591
668
  pageData: page.data,
592
- layoutData: theme.module.defaultLayout
669
+ layoutData: theme.module.defaultLayout,
670
+ metadata
593
671
  })
594
672
  );
595
673
  const cssVarStyle = Object.entries(chrome.cssVariables).map(([key, value]) => `${key}: ${value};`).join(" ");
596
- const stylesheetTag = theme.stylesheetPath ? '<link rel="stylesheet" href="/styles.css" />' : "";
674
+ const stylesheetTag = resolved.runtime?.stylesheetUrl ? `<link rel="stylesheet" href="${escapeHtml(resolved.runtime.stylesheetUrl)}" />` : "";
597
675
  const customHead = chrome.customHeadCode ?? "";
598
676
  const customBody = chrome.customBodyCode ?? "";
599
677
  return [
@@ -613,40 +691,129 @@ function renderStarterPageHtml(theme, page) {
613
691
  "</html>"
614
692
  ].join("");
615
693
  }
694
+ function renderPreviewShellHtml(theme, activeSlug) {
695
+ const items = theme.module.starterPages.map((page) => {
696
+ const slug = page.slug;
697
+ const label = `${page.title}${page.isHome ? " (home)" : ""}`;
698
+ const active = slug === activeSlug;
699
+ const href = `/?page=${encodeURIComponent(slug)}`;
700
+ const className = active ? "suda-preview-tab suda-preview-tab--active" : "suda-preview-tab";
701
+ return `<a class="${className}" href="${escapeHtml(href)}" target="_self">${escapeHtml(label)}</a>`;
702
+ }).join("");
703
+ const frameSrc = `/pages/${encodeURIComponent(activeSlug)}`;
704
+ return [
705
+ "<!doctype html>",
706
+ '<html lang="en">',
707
+ "<head>",
708
+ '<meta charset="utf-8" />',
709
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
710
+ `<title>${escapeHtml(theme.module.manifest.name)} \u2014 preview</title>`,
711
+ "<style>",
712
+ "html,body{margin:0;height:100%;font-family:system-ui,sans-serif;background:#0b0d12;color:#e6e8ee;}",
713
+ ".suda-preview-bar{display:flex;flex-wrap:wrap;gap:.25rem;padding:.5rem .75rem;background:#0b0d12;border-bottom:1px solid #1f2330;position:sticky;top:0;z-index:10;}",
714
+ ".suda-preview-tab{display:inline-flex;align-items:center;padding:.35rem .75rem;border-radius:.5rem;font-size:.875rem;color:#c9cdd6;text-decoration:none;border:1px solid transparent;}",
715
+ ".suda-preview-tab:hover{background:#1a1d27;}",
716
+ ".suda-preview-tab--active{background:#222633;color:#fff;border-color:#2c3142;}",
717
+ ".suda-preview-frame{display:block;border:0;width:100%;height:calc(100vh - 2.5rem);background:#fff;}",
718
+ "</style>",
719
+ "</head>",
720
+ "<body>",
721
+ `<nav class="suda-preview-bar">${items}</nav>`,
722
+ `<iframe class="suda-preview-frame" src="${escapeHtml(frameSrc)}" title="theme preview"></iframe>`,
723
+ "</body>",
724
+ "</html>"
725
+ ].join("");
726
+ }
616
727
  async function startPreviewServer(theme, port) {
617
728
  const { createServer } = await import('http');
618
- const starterPage = pickStarterPage(theme);
729
+ const renderer = await loadThemeScopedRenderer(theme.root);
730
+ const registry = new FileSystemThemeRegistry({
731
+ publicBasePath: PREVIEW_PUBLIC_BASE_PATH,
732
+ artifacts: [
733
+ {
734
+ rootPath: theme.root,
735
+ serverEntryPath: theme.serverEntryPath,
736
+ module: theme.module
737
+ }
738
+ ]
739
+ });
740
+ const themeKey = theme.module.manifest.key;
741
+ const themeVersion = theme.module.manifest.version;
742
+ const assetPathPrefix = `${PREVIEW_PUBLIC_BASE_PATH}/${themeKey}/${themeVersion}/assets/`;
743
+ const clientRuntimePrefix = `${PREVIEW_PUBLIC_BASE_PATH}/${themeKey}/${themeVersion}/runtime/client`;
619
744
  const server = createServer((request, response) => {
620
745
  void (async () => {
621
746
  const url = new URL(request.url ?? "/", `http://localhost:${port}`);
622
747
  const pathname = url.pathname;
623
748
  if (pathname === "/" || pathname === "/index.html") {
624
- if (!starterPage) {
749
+ const fallbackSlug = pickStarterSlug(theme);
750
+ if (!fallbackSlug) {
625
751
  response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
626
- response.end("No starter page available for preview.");
752
+ response.end("No starter pages declared by this theme.");
627
753
  return;
628
754
  }
755
+ const requested = url.searchParams.get("page");
756
+ const activeSlug = requested && findStarterPage(theme, requested) ? requested : fallbackSlug;
629
757
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
630
- response.end(renderStarterPageHtml(theme, starterPage));
758
+ response.end(renderPreviewShellHtml(theme, activeSlug));
631
759
  return;
632
760
  }
633
- if (pathname === "/styles.css" && theme.stylesheetPath) {
634
- response.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
635
- response.end(await readFile(theme.stylesheetPath));
761
+ if (pathname.startsWith("/pages/")) {
762
+ const slug = decodeURIComponent(pathname.slice("/pages/".length));
763
+ const starter = findStarterPage(theme, slug);
764
+ if (!starter) {
765
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
766
+ response.end(`Unknown starter page: ${slug}`);
767
+ return;
768
+ }
769
+ const resolved = await registry.resolve(themeKey, themeVersion);
770
+ if (!resolved) {
771
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
772
+ response.end("Failed to resolve theme via FileSystemThemeRegistry.");
773
+ return;
774
+ }
775
+ const html = renderStarterPageHtml(theme, resolved, starter, renderer);
776
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
777
+ response.end(html);
636
778
  return;
637
779
  }
638
- const relativePath = pathname.replace(/^\/+/, "");
639
- const filePath = path2.resolve(theme.root, relativePath);
640
- const normalizedRoot = path2.resolve(theme.root) + path2.sep;
641
- if (!filePath.startsWith(normalizedRoot) || !await pathExists(filePath)) {
642
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
643
- response.end("Not found");
780
+ if (pathname.startsWith(assetPathPrefix)) {
781
+ const relativePath = decodeURIComponent(pathname.slice(assetPathPrefix.length));
782
+ const filePath = await registry.resolveAssetPath(themeKey, themeVersion, relativePath);
783
+ if (!filePath) {
784
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
785
+ response.end("Not found");
786
+ return;
787
+ }
788
+ const body = await readFile(filePath);
789
+ response.writeHead(200, {
790
+ "Content-Type": contentTypeFor(relativePath),
791
+ "Cache-Control": "no-cache"
792
+ });
793
+ response.end(body);
644
794
  return;
645
795
  }
646
- response.writeHead(200, { "Content-Type": contentTypeFor(relativePath) });
647
- response.end(await readFile(filePath));
796
+ if (pathname === clientRuntimePrefix) {
797
+ const filePath = await registry.resolveClientEntryPath(themeKey, themeVersion);
798
+ if (!filePath) {
799
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
800
+ response.end("Theme has no client runtime bundle.");
801
+ return;
802
+ }
803
+ const body = await readFile(filePath);
804
+ response.writeHead(200, {
805
+ "Content-Type": "application/javascript; charset=utf-8",
806
+ "Cache-Control": "no-cache"
807
+ });
808
+ response.end(body);
809
+ return;
810
+ }
811
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
812
+ response.end("Not found");
648
813
  })().catch((error) => {
649
- response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
814
+ if (!response.headersSent) {
815
+ response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
816
+ }
650
817
  response.end(error instanceof Error ? error.message : "Internal error");
651
818
  });
652
819
  });
@@ -670,14 +837,6 @@ async function startPreviewServer(theme, port) {
670
837
  })
671
838
  };
672
839
  }
673
- async function previewTheme(root, port) {
674
- const theme = await buildTheme(root, false);
675
- const handle = await startPreviewServer(theme, port);
676
- console.log(
677
- `previewing ${theme.module.manifest.key}@${theme.module.manifest.version} at ${handle.url}`
678
- );
679
- await new Promise(() => void 0);
680
- }
681
840
  var DEFAULT_SCREENSHOT_RELATIVE = path2.join("dist", "preview", "desktop.png");
682
841
  function parsePositiveInt(value, fallback) {
683
842
  if (!value) {
@@ -1173,6 +1332,24 @@ async function checksum(files) {
1173
1332
  }
1174
1333
  return hash.digest("hex");
1175
1334
  }
1335
+ async function formatHttpErrorBody(res) {
1336
+ const fallback = `${res.status} ${res.statusText}`;
1337
+ const body = await res.json().catch(() => null);
1338
+ if (!body) {
1339
+ return fallback;
1340
+ }
1341
+ const message = body.error ?? fallback;
1342
+ if (Array.isArray(body.issues) && body.issues.length > 0) {
1343
+ const lines = body.issues.map((issue) => {
1344
+ const pathSegments = Array.isArray(issue.path) ? issue.path : [];
1345
+ const where = pathSegments.length > 0 ? pathSegments.join(".") : "<root>";
1346
+ return ` - ${where}: ${issue.message ?? "invalid"}`;
1347
+ });
1348
+ return `${message}
1349
+ ${lines.join("\n")}`;
1350
+ }
1351
+ return message;
1352
+ }
1176
1353
  async function publishTheme(root, skipBuild, skipCheck) {
1177
1354
  const theme = skipBuild ? await validateTheme(root) : await buildTheme(root, false);
1178
1355
  if (!skipCheck) {
@@ -1191,7 +1368,7 @@ async function publishTheme(root, skipBuild, skipCheck) {
1191
1368
  if (!config) {
1192
1369
  throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
1193
1370
  }
1194
- const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
1371
+ const baseUrl = `${protocolForHost2(config.host)}://${config.host}`;
1195
1372
  const { key, version } = theme.module.manifest;
1196
1373
  const intentRes = await fetch(`${baseUrl}/api/cli/themes/publish-intent`, {
1197
1374
  method: "POST",
@@ -1206,8 +1383,9 @@ async function publishTheme(root, skipBuild, skipCheck) {
1206
1383
  })
1207
1384
  });
1208
1385
  if (!intentRes.ok) {
1209
- const errorData = await intentRes.json().catch(() => ({}));
1210
- throw new Error(`Failed to get publish intent: ${errorData.error || intentRes.statusText}`);
1386
+ throw new Error(
1387
+ `Failed to get publish intent: ${await formatHttpErrorBody(intentRes)}`
1388
+ );
1211
1389
  }
1212
1390
  const { urls } = await intentRes.json();
1213
1391
  for (const file of files) {
@@ -1254,8 +1432,9 @@ async function publishTheme(root, skipBuild, skipCheck) {
1254
1432
  })
1255
1433
  });
1256
1434
  if (!completeRes.ok) {
1257
- const errorData = await completeRes.json().catch(() => ({}));
1258
- throw new Error(`Failed to complete publish: ${errorData.error || completeRes.statusText}`);
1435
+ throw new Error(
1436
+ `Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`
1437
+ );
1259
1438
  }
1260
1439
  console.log(`published ${key}@${version}`);
1261
1440
  }
@@ -1328,8 +1507,7 @@ async function initTheme(target) {
1328
1507
  build: "pnpm run build:src && suda theme build --theme-root . --skip-theme-build",
1329
1508
  dev: "suda theme dev --theme-root . --skip-theme-build",
1330
1509
  typecheck: "tsc -p tsconfig.json --noEmit",
1331
- validate: "suda theme validate --theme-root .",
1332
- preview: "suda theme preview --theme-root ."
1510
+ validate: "suda theme validate --theme-root ."
1333
1511
  },
1334
1512
  dependencies: {
1335
1513
  "@sudajs/theme-engine": cliPackageVersions.themeEngine
@@ -1931,7 +2109,7 @@ pnpm install
1931
2109
  pnpm typecheck
1932
2110
  pnpm build # tsc + suda theme build --skip-theme-build (runs AI check)
1933
2111
  pnpm validate
1934
- pnpm preview
2112
+ pnpm dev # watch and serve local theme artifact files
1935
2113
  suda theme check # AI metadata check only
1936
2114
  suda agent theme describe local --theme-root .
1937
2115
  suda agent section schema --theme local --section Hero --theme-root .
@@ -1964,8 +2142,7 @@ pnpm install
1964
2142
  \`\`\`bash
1965
2143
  pnpm typecheck # type-check sources
1966
2144
  pnpm build # tsc + suda theme build (server bundle + client runtime + manifest)
1967
- pnpm dev # watch the browser runtime
1968
- pnpm preview # build and preview the home starter page
2145
+ pnpm dev # watch the browser runtime and start preview server
1969
2146
  pnpm validate # validate the dist artifact
1970
2147
  \`\`\`
1971
2148
 
@@ -2028,12 +2205,13 @@ function buildProgram() {
2028
2205
  process.exitCode = 1;
2029
2206
  }
2030
2207
  });
2031
- theme.command("dev").description("Watch and rebuild the browser runtime.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-theme-build", "Skip the theme package build script.").action(async (options) => {
2032
- await watchTheme(resolveThemeRoot(options), options.skipThemeBuild === true);
2033
- });
2034
- theme.command("preview").description("Build and serve the local theme artifact files.").option("--theme-root <path>", "Theme source/artifact root.").option("--port <port>", "Preview server port.", "4177").action(async (options) => {
2208
+ theme.command("dev").description("Watch and rebuild the browser runtime, and serve the local theme artifact files.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-theme-build", "Skip the theme package build script.").option("--port <port>", "Preview server port.", "4177").action(async (options) => {
2035
2209
  const port = Number(options.port ?? "4177");
2036
- await previewTheme(resolveThemeRoot(options), Number.isFinite(port) ? port : 4177);
2210
+ await watchTheme(
2211
+ resolveThemeRoot(options),
2212
+ options.skipThemeBuild === true,
2213
+ Number.isFinite(port) ? port : 4177
2214
+ );
2037
2215
  });
2038
2216
  theme.command("screenshot").description(
2039
2217
  "Capture a desktop preview screenshot of the home starter page using Playwright."