@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 +250 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 {
|
|
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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
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: `${
|
|
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
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
585
629
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
758
|
+
response.end(renderPreviewShellHtml(theme, activeSlug));
|
|
631
759
|
return;
|
|
632
760
|
}
|
|
633
|
-
if (pathname
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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.
|
|
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 = `${
|
|
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
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
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
|
|
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
|
|
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."
|