@sudajs/cli 0.0.2 → 0.1.0
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/bin/suda-dev.js +15 -0
- package/bin/suda.js +34 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +633 -15
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -3,11 +3,15 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
|
|
4
4
|
import path2 from 'path';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { createAgentPageSchemaOutput, createThemeAgentManifest, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
6
9
|
import { extractLayoutChrome, ThemeRender } from '@sudajs/theme-engine/server';
|
|
7
10
|
import { Command } from 'commander';
|
|
8
11
|
import { build, context } from 'esbuild';
|
|
9
12
|
import { createElement } from 'react';
|
|
10
13
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
14
|
+
import { z } from 'zod';
|
|
11
15
|
import os from 'os';
|
|
12
16
|
|
|
13
17
|
function getConfigPath() {
|
|
@@ -38,7 +42,7 @@ async function clearAuthConfig() {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
|
-
async function login(host = "
|
|
45
|
+
async function login(host = "app.sudayun.cn") {
|
|
42
46
|
const protocol = host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
43
47
|
const baseUrl = `${protocol}://${host}`;
|
|
44
48
|
console.log(`Requesting device authorization from ${baseUrl}...`);
|
|
@@ -120,6 +124,22 @@ async function logout() {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
// src/index.ts
|
|
127
|
+
var createPageDraftOutputSchema = z.discriminatedUnion("status", [
|
|
128
|
+
z.object({
|
|
129
|
+
status: z.literal("created").describe("The page draft was created."),
|
|
130
|
+
pageId: z.string().describe("New page id.")
|
|
131
|
+
}),
|
|
132
|
+
z.object({
|
|
133
|
+
status: z.literal("invalid").describe("The page content failed validation."),
|
|
134
|
+
valid: z.literal(false),
|
|
135
|
+
issues: z.array(
|
|
136
|
+
z.object({
|
|
137
|
+
path: z.string(),
|
|
138
|
+
message: z.string()
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
123
143
|
function createHostReactShimPlugin() {
|
|
124
144
|
return {
|
|
125
145
|
name: "suda-host-react-shim",
|
|
@@ -266,6 +286,43 @@ async function readJson(filePath) {
|
|
|
266
286
|
const raw = await readFile(filePath, "utf8");
|
|
267
287
|
return JSON.parse(raw);
|
|
268
288
|
}
|
|
289
|
+
async function readJsonIfExists(filePath) {
|
|
290
|
+
if (!await pathExists(filePath)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return readJson(filePath);
|
|
294
|
+
}
|
|
295
|
+
function protocolForHost(host) {
|
|
296
|
+
return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
297
|
+
}
|
|
298
|
+
async function requireCliBaseUrl() {
|
|
299
|
+
const config = await readAuthConfig();
|
|
300
|
+
if (!config) {
|
|
301
|
+
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
baseUrl: `${protocolForHost(config.host)}://${config.host}`,
|
|
305
|
+
token: config.sessionToken
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function fetchJson(url, options = {}) {
|
|
309
|
+
const headers = new Headers(options.headers);
|
|
310
|
+
if (options.token) {
|
|
311
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
312
|
+
}
|
|
313
|
+
const response = await fetch(url, { ...options, headers });
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const errorData = await response.json().catch(() => ({}));
|
|
316
|
+
throw new Error(errorData.message || errorData.error || response.statusText);
|
|
317
|
+
}
|
|
318
|
+
return await response.json();
|
|
319
|
+
}
|
|
320
|
+
function agentCacheDir() {
|
|
321
|
+
return path2.join(process.env.XDG_CACHE_HOME ?? path2.join(process.env.HOME ?? ".", ".cache"), "suda", "agent");
|
|
322
|
+
}
|
|
323
|
+
function cachedThemeManifestPath(themeKey, version) {
|
|
324
|
+
return path2.join(agentCacheDir(), "themes", safeObjectKeyPart(themeKey), safeObjectKeyPart(version), "agent-manifest.json");
|
|
325
|
+
}
|
|
269
326
|
async function loadThemeModule(serverEntryPath) {
|
|
270
327
|
const imported = await import(`${pathToFileURL(serverEntryPath).href}?t=${Date.now()}`);
|
|
271
328
|
if (!imported.default) {
|
|
@@ -617,8 +674,292 @@ async function screenshotTheme(root, options) {
|
|
|
617
674
|
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root, false);
|
|
618
675
|
await captureScreenshot(theme, resolved);
|
|
619
676
|
}
|
|
677
|
+
async function loadLocalAgentManifest(root) {
|
|
678
|
+
const built = await readJsonIfExists(path2.join(root, "dist", "agent-manifest.json"));
|
|
679
|
+
if (built) {
|
|
680
|
+
return built;
|
|
681
|
+
}
|
|
682
|
+
throw new Error(
|
|
683
|
+
`No agent manifest found at ${path2.join(root, "dist", "agent-manifest.json")}. Run "suda theme build" first.`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
async function listAgentThemes(options) {
|
|
687
|
+
if (options.themeRoot) {
|
|
688
|
+
const manifest = await loadLocalAgentManifest(resolveThemeRoot(options));
|
|
689
|
+
return [
|
|
690
|
+
{
|
|
691
|
+
key: manifest.manifest.key,
|
|
692
|
+
name: manifest.manifest.name,
|
|
693
|
+
version: manifest.manifest.version,
|
|
694
|
+
description: manifest.manifest.description ?? null,
|
|
695
|
+
categories: manifest.manifest.categories,
|
|
696
|
+
preview: manifest.manifest.preview
|
|
697
|
+
}
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
const auth = await requireCliBaseUrl();
|
|
701
|
+
const url = new URL("/api/cli/agent/themes", auth.baseUrl);
|
|
702
|
+
if (options.projectId) {
|
|
703
|
+
url.searchParams.set("projectId", options.projectId);
|
|
704
|
+
}
|
|
705
|
+
const catalog = await fetchJson(url.href, { token: auth.token });
|
|
706
|
+
await mkdir(agentCacheDir(), { recursive: true });
|
|
707
|
+
await writeFile(path2.join(agentCacheDir(), "catalog.json"), `${JSON.stringify(catalog, null, 2)}
|
|
708
|
+
`);
|
|
709
|
+
return catalog.themes;
|
|
710
|
+
}
|
|
711
|
+
async function fetchAgentManifest(themeKey, options) {
|
|
712
|
+
if (options.themeRoot) {
|
|
713
|
+
return loadLocalAgentManifest(resolveThemeRoot(options));
|
|
714
|
+
}
|
|
715
|
+
const auth = await requireCliBaseUrl();
|
|
716
|
+
const version = options.version ?? "latest";
|
|
717
|
+
const cachePath = cachedThemeManifestPath(themeKey, version);
|
|
718
|
+
const cached = version === "latest" ? null : await readJsonIfExists(cachePath);
|
|
719
|
+
if (cached) {
|
|
720
|
+
return cached;
|
|
721
|
+
}
|
|
722
|
+
const url = new URL(`/api/cli/agent/themes/${encodeURIComponent(themeKey)}/${encodeURIComponent(version)}/schema`, auth.baseUrl);
|
|
723
|
+
if (options.projectId) {
|
|
724
|
+
url.searchParams.set("projectId", options.projectId);
|
|
725
|
+
}
|
|
726
|
+
const result = await fetchJson(url.href, { token: auth.token });
|
|
727
|
+
const realVersion = result.agentManifest.manifest.version;
|
|
728
|
+
const realCachePath = cachedThemeManifestPath(themeKey, realVersion);
|
|
729
|
+
await mkdir(path2.dirname(realCachePath), { recursive: true });
|
|
730
|
+
await writeFile(realCachePath, `${JSON.stringify(result.agentManifest, null, 2)}
|
|
731
|
+
`);
|
|
732
|
+
return result.agentManifest;
|
|
733
|
+
}
|
|
734
|
+
function printJson(value) {
|
|
735
|
+
console.log(JSON.stringify(value, null, 2));
|
|
736
|
+
}
|
|
737
|
+
function createSectionSchema(manifest, sectionType) {
|
|
738
|
+
const section = findAgentSection(manifest, sectionType);
|
|
739
|
+
if (!section) {
|
|
740
|
+
throw new Error(`Unknown section type "${sectionType}".`);
|
|
741
|
+
}
|
|
742
|
+
return createAgentComponentSchemaOutput(section, "section");
|
|
743
|
+
}
|
|
744
|
+
async function validateAgentPage(themeKey, input, options) {
|
|
745
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
746
|
+
const pageContent = await readJson(path2.resolve(input));
|
|
747
|
+
const result = validateAgentPageContentWithManifest(pageContent, manifest);
|
|
748
|
+
printJson(agentValidationResultSchema.parse(result));
|
|
749
|
+
if (!result.valid) {
|
|
750
|
+
process.exitCode = 1;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function createAgentPage(options) {
|
|
754
|
+
if (!options.projectId) {
|
|
755
|
+
throw new Error("--project-id is required.");
|
|
756
|
+
}
|
|
757
|
+
const pageContent = await readJson(path2.resolve(options.input));
|
|
758
|
+
const manifest = await fetchAgentManifest(options.theme, options);
|
|
759
|
+
const validation = validateAgentPageContentWithManifest(pageContent, manifest);
|
|
760
|
+
if (!validation.valid) {
|
|
761
|
+
printJson(agentValidationResultSchema.parse(validation));
|
|
762
|
+
process.exitCode = 1;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const pageData = createPageDataFromAgentContent(pageContent);
|
|
766
|
+
const auth = await requireCliBaseUrl();
|
|
767
|
+
const response = await fetchJson(
|
|
768
|
+
`${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(options.projectId)}/pages`,
|
|
769
|
+
{
|
|
770
|
+
method: "POST",
|
|
771
|
+
token: auth.token,
|
|
772
|
+
headers: { "Content-Type": "application/json" },
|
|
773
|
+
body: JSON.stringify({
|
|
774
|
+
title: options.title,
|
|
775
|
+
slug: options.slug,
|
|
776
|
+
themeKey: manifest.manifest.key,
|
|
777
|
+
themeVersion: manifest.manifest.version,
|
|
778
|
+
data: pageData
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
printJson({ success: true, ...response });
|
|
783
|
+
}
|
|
784
|
+
function mcpStructured(summary, value) {
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: summary
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
structuredContent: value
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async function startMcpServer() {
|
|
796
|
+
const server = new McpServer({
|
|
797
|
+
name: "suda",
|
|
798
|
+
version: "0.1.0"
|
|
799
|
+
});
|
|
800
|
+
server.registerTool(
|
|
801
|
+
"list_themes",
|
|
802
|
+
{
|
|
803
|
+
description: "List themes visible to the current Suda CLI user.",
|
|
804
|
+
inputSchema: {
|
|
805
|
+
projectId: z.string().optional(),
|
|
806
|
+
themeRoot: z.string().optional()
|
|
807
|
+
},
|
|
808
|
+
outputSchema: {
|
|
809
|
+
themes: z.array(
|
|
810
|
+
z.object({
|
|
811
|
+
key: z.string(),
|
|
812
|
+
name: z.string(),
|
|
813
|
+
version: z.string(),
|
|
814
|
+
description: z.string().nullable().optional(),
|
|
815
|
+
categories: z.array(z.string()).optional(),
|
|
816
|
+
preview: z.string().optional(),
|
|
817
|
+
active: z.boolean().optional()
|
|
818
|
+
})
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
async ({ projectId, themeRoot }) => {
|
|
823
|
+
const structuredContent = { themes: await listAgentThemes({ projectId, themeRoot }) };
|
|
824
|
+
return mcpStructured("Available Suda themes.", structuredContent);
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
server.registerTool(
|
|
828
|
+
"describe_theme",
|
|
829
|
+
{
|
|
830
|
+
description: "Return the raw Suda agent manifest for a theme.",
|
|
831
|
+
inputSchema: {
|
|
832
|
+
theme: z.string(),
|
|
833
|
+
version: z.string().optional(),
|
|
834
|
+
projectId: z.string().optional(),
|
|
835
|
+
themeRoot: z.string().optional()
|
|
836
|
+
},
|
|
837
|
+
outputSchema: {
|
|
838
|
+
agentManifest: z.unknown()
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
async ({ theme, version, projectId, themeRoot }) => {
|
|
842
|
+
const structuredContent = {
|
|
843
|
+
agentManifest: await fetchAgentManifest(theme, { version, projectId, themeRoot })
|
|
844
|
+
};
|
|
845
|
+
return mcpStructured("Raw Suda theme agent manifest.", structuredContent);
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
server.registerTool(
|
|
849
|
+
"get_page_schema",
|
|
850
|
+
{
|
|
851
|
+
description: "Return the AI-first output schema for generating Suda page content, including all section schemas.",
|
|
852
|
+
inputSchema: {
|
|
853
|
+
theme: z.string(),
|
|
854
|
+
version: z.string().optional(),
|
|
855
|
+
projectId: z.string().optional(),
|
|
856
|
+
themeRoot: z.string().optional(),
|
|
857
|
+
includeExample: z.boolean().optional()
|
|
858
|
+
},
|
|
859
|
+
outputSchema: agentPageSchemaOutputSchema.shape
|
|
860
|
+
},
|
|
861
|
+
async ({ theme, version, projectId, themeRoot, includeExample }) => {
|
|
862
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
863
|
+
const structuredContent = createAgentPageSchemaOutput(manifest, {
|
|
864
|
+
includeExample: includeExample === true
|
|
865
|
+
});
|
|
866
|
+
return mcpStructured("AI-first Suda page content output schema.", structuredContent);
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
server.registerTool(
|
|
870
|
+
"get_section_schema",
|
|
871
|
+
{
|
|
872
|
+
description: "Return the AI-first output schema for one section component in a theme.",
|
|
873
|
+
inputSchema: {
|
|
874
|
+
theme: z.string(),
|
|
875
|
+
section: z.string(),
|
|
876
|
+
version: z.string().optional(),
|
|
877
|
+
projectId: z.string().optional(),
|
|
878
|
+
themeRoot: z.string().optional()
|
|
879
|
+
},
|
|
880
|
+
outputSchema: agentComponentOutputSchema.shape
|
|
881
|
+
},
|
|
882
|
+
async ({ theme, section, version, projectId, themeRoot }) => {
|
|
883
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
884
|
+
const structuredContent = createSectionSchema(manifest, section);
|
|
885
|
+
return mcpStructured("AI-first Suda section output schema.", structuredContent);
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
server.registerTool(
|
|
889
|
+
"validate_page_config",
|
|
890
|
+
{
|
|
891
|
+
description: "Validate AI-generated Suda page content against a theme.",
|
|
892
|
+
inputSchema: {
|
|
893
|
+
theme: z.string(),
|
|
894
|
+
data: z.unknown(),
|
|
895
|
+
version: z.string().optional(),
|
|
896
|
+
projectId: z.string().optional(),
|
|
897
|
+
themeRoot: z.string().optional()
|
|
898
|
+
},
|
|
899
|
+
outputSchema: agentValidationResultSchema.shape
|
|
900
|
+
},
|
|
901
|
+
async ({ theme, data, version, projectId, themeRoot }) => {
|
|
902
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
903
|
+
const structuredContent = agentValidationResultSchema.parse(
|
|
904
|
+
validateAgentPageContentWithManifest(data, manifest)
|
|
905
|
+
);
|
|
906
|
+
return mcpStructured("Suda page content validation result.", structuredContent);
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
server.registerTool(
|
|
910
|
+
"create_page_draft",
|
|
911
|
+
{
|
|
912
|
+
description: "Create a workspace page draft from AI-generated Suda page content.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
projectId: z.string(),
|
|
915
|
+
title: z.string(),
|
|
916
|
+
slug: z.string(),
|
|
917
|
+
theme: z.string(),
|
|
918
|
+
data: z.unknown(),
|
|
919
|
+
version: z.string().optional()
|
|
920
|
+
},
|
|
921
|
+
outputSchema: createPageDraftOutputSchema
|
|
922
|
+
},
|
|
923
|
+
async ({ projectId, title, slug, theme, data, version }) => {
|
|
924
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId });
|
|
925
|
+
const validation = validateAgentPageContentWithManifest(data, manifest);
|
|
926
|
+
if (!validation.valid) {
|
|
927
|
+
return mcpStructured(
|
|
928
|
+
"Suda page content validation failed. Fix issues before creating a draft.",
|
|
929
|
+
createPageDraftOutputSchema.parse({
|
|
930
|
+
status: "invalid",
|
|
931
|
+
valid: false,
|
|
932
|
+
issues: validation.issues
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const auth = await requireCliBaseUrl();
|
|
937
|
+
const response = await fetchJson(
|
|
938
|
+
`${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/pages`,
|
|
939
|
+
{
|
|
940
|
+
method: "POST",
|
|
941
|
+
token: auth.token,
|
|
942
|
+
headers: { "Content-Type": "application/json" },
|
|
943
|
+
body: JSON.stringify({
|
|
944
|
+
title,
|
|
945
|
+
slug,
|
|
946
|
+
themeKey: manifest.manifest.key,
|
|
947
|
+
themeVersion: manifest.manifest.version,
|
|
948
|
+
data: createPageDataFromAgentContent(data)
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
return mcpStructured(
|
|
953
|
+
"Created Suda page draft.",
|
|
954
|
+
createPageDraftOutputSchema.parse({ status: "created", ...response })
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
await server.connect(new StdioServerTransport());
|
|
959
|
+
}
|
|
620
960
|
async function writeThemeArtifacts(theme) {
|
|
621
961
|
const dist = path2.join(theme.root, "dist");
|
|
962
|
+
const agentManifest = createThemeAgentManifest(theme.module);
|
|
622
963
|
await writeFile(
|
|
623
964
|
path2.join(dist, "manifest.json"),
|
|
624
965
|
`${JSON.stringify(theme.module.manifest, null, 2)}
|
|
@@ -632,6 +973,11 @@ async function writeThemeArtifacts(theme) {
|
|
|
632
973
|
await writeFile(
|
|
633
974
|
path2.join(dist, "default-layout.json"),
|
|
634
975
|
`${JSON.stringify(theme.module.defaultLayout, null, 2)}
|
|
976
|
+
`
|
|
977
|
+
);
|
|
978
|
+
await writeFile(
|
|
979
|
+
path2.join(dist, "agent-manifest.json"),
|
|
980
|
+
`${JSON.stringify(agentManifest, null, 2)}
|
|
635
981
|
`
|
|
636
982
|
);
|
|
637
983
|
}
|
|
@@ -709,8 +1055,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
709
1055
|
if (!config) {
|
|
710
1056
|
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
711
1057
|
}
|
|
712
|
-
const
|
|
713
|
-
const baseUrl = `${protocol}://${config.host}`;
|
|
1058
|
+
const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
|
|
714
1059
|
const { key, version } = theme.module.manifest;
|
|
715
1060
|
const intentRes = await fetch(`${baseUrl}/api/cli/themes/publish-intent`, {
|
|
716
1061
|
method: "POST",
|
|
@@ -755,6 +1100,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
755
1100
|
}
|
|
756
1101
|
const previewArtifactRelative = "dist/preview/desktop.png";
|
|
757
1102
|
const previewArtifactExists = await pathExists(path2.join(theme.root, previewArtifactRelative));
|
|
1103
|
+
const agentManifest = createThemeAgentManifest(theme.module);
|
|
758
1104
|
const completeRes = await fetch(`${baseUrl}/api/cli/themes/publish-complete`, {
|
|
759
1105
|
method: "POST",
|
|
760
1106
|
headers: {
|
|
@@ -766,6 +1112,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
766
1112
|
version,
|
|
767
1113
|
checksum: digest,
|
|
768
1114
|
manifest: theme.module.manifest,
|
|
1115
|
+
agentManifest,
|
|
769
1116
|
bundleArtifactRelative,
|
|
770
1117
|
previewArtifactRelative: previewArtifactExists ? previewArtifactRelative : void 0
|
|
771
1118
|
})
|
|
@@ -834,22 +1181,109 @@ export const Hero: ComponentConfig = {
|
|
|
834
1181
|
eyebrow: { type: "text", label: "Eyebrow" },
|
|
835
1182
|
title: { type: "text", label: "Title" },
|
|
836
1183
|
description: { type: "textarea", label: "Description" },
|
|
1184
|
+
primaryLabel: { type: "text", label: "Primary button label" },
|
|
1185
|
+
primaryHref: { type: "text", label: "Primary button link" },
|
|
837
1186
|
},
|
|
838
1187
|
defaultProps: {
|
|
839
1188
|
eyebrow: "New theme",
|
|
840
1189
|
title: "Build with SudaCloud",
|
|
841
1190
|
description: "Edit this starter section in the visual editor.",
|
|
1191
|
+
primaryLabel: "Get started",
|
|
1192
|
+
primaryHref: "#contact",
|
|
842
1193
|
},
|
|
843
|
-
render: ({ eyebrow, title, description }) => (
|
|
844
|
-
<section className="${key}-hero">
|
|
845
|
-
<p>{eyebrow}</p>
|
|
1194
|
+
render: ({ eyebrow, title, description, primaryLabel, primaryHref }) => (
|
|
1195
|
+
<section className="${key}-section ${key}-hero">
|
|
1196
|
+
<p className="${key}-eyebrow">{eyebrow}</p>
|
|
846
1197
|
<h1>{title}</h1>
|
|
847
|
-
<
|
|
1198
|
+
<p>{description}</p>
|
|
1199
|
+
<a className="${key}-button" href={primaryHref}>{primaryLabel}</a>
|
|
1200
|
+
</section>
|
|
1201
|
+
),
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
export const FeatureGrid: ComponentConfig = {
|
|
1205
|
+
label: "Feature grid",
|
|
1206
|
+
fields: {
|
|
1207
|
+
title: { type: "text", label: "Title" },
|
|
1208
|
+
description: { type: "textarea", label: "Description" },
|
|
1209
|
+
features: {
|
|
1210
|
+
type: "array",
|
|
1211
|
+
label: "Features",
|
|
1212
|
+
arrayFields: {
|
|
1213
|
+
title: { type: "text", label: "Title" },
|
|
1214
|
+
description: { type: "textarea", label: "Description" },
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
defaultProps: {
|
|
1219
|
+
title: "Everything you need to launch",
|
|
1220
|
+
description: "Use this section to explain the core value of the project.",
|
|
1221
|
+
features: [
|
|
1222
|
+
{ title: "Fast setup", description: "Start from a clean theme contract." },
|
|
1223
|
+
{ title: "Visual editing", description: "Expose content fields through Puck." },
|
|
1224
|
+
{ title: "Publish ready", description: "Build and publish with the Suda CLI." },
|
|
1225
|
+
],
|
|
1226
|
+
},
|
|
1227
|
+
render: ({ title, description, features = [] }) => (
|
|
1228
|
+
<section className="${key}-section">
|
|
1229
|
+
<h2>{title}</h2>
|
|
1230
|
+
<p>{description}</p>
|
|
1231
|
+
<div className="${key}-grid">
|
|
1232
|
+
{features.map((feature: { title?: string; description?: string }, index: number) => (
|
|
1233
|
+
<article className="${key}-card" key={index}>
|
|
1234
|
+
<h3>{feature.title}</h3>
|
|
1235
|
+
<p>{feature.description}</p>
|
|
1236
|
+
</article>
|
|
1237
|
+
))}
|
|
1238
|
+
</div>
|
|
848
1239
|
</section>
|
|
849
1240
|
),
|
|
850
1241
|
};
|
|
851
1242
|
|
|
852
|
-
export const
|
|
1243
|
+
export const Testimonial: ComponentConfig = {
|
|
1244
|
+
label: "Testimonial",
|
|
1245
|
+
fields: {
|
|
1246
|
+
quote: { type: "textarea", label: "Quote" },
|
|
1247
|
+
author: { type: "text", label: "Author" },
|
|
1248
|
+
role: { type: "text", label: "Role" },
|
|
1249
|
+
},
|
|
1250
|
+
defaultProps: {
|
|
1251
|
+
quote: "SudaCloud gives our team a practical editing workflow without giving up theme control.",
|
|
1252
|
+
author: "Alex Chen",
|
|
1253
|
+
role: "Founder",
|
|
1254
|
+
},
|
|
1255
|
+
render: ({ quote, author, role }) => (
|
|
1256
|
+
<section className="${key}-section ${key}-quote">
|
|
1257
|
+
<blockquote>{quote}</blockquote>
|
|
1258
|
+
<p>{author} \xB7 {role}</p>
|
|
1259
|
+
</section>
|
|
1260
|
+
),
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
export const CallToAction: ComponentConfig = {
|
|
1264
|
+
label: "Call to action",
|
|
1265
|
+
fields: {
|
|
1266
|
+
title: { type: "text", label: "Title" },
|
|
1267
|
+
description: { type: "textarea", label: "Description" },
|
|
1268
|
+
buttonLabel: { type: "text", label: "Button label" },
|
|
1269
|
+
buttonHref: { type: "text", label: "Button link" },
|
|
1270
|
+
},
|
|
1271
|
+
defaultProps: {
|
|
1272
|
+
title: "Ready to build your next page?",
|
|
1273
|
+
description: "Use this section as the final conversion block.",
|
|
1274
|
+
buttonLabel: "Contact us",
|
|
1275
|
+
buttonHref: "#contact",
|
|
1276
|
+
},
|
|
1277
|
+
render: ({ title, description, buttonLabel, buttonHref }) => (
|
|
1278
|
+
<section className="${key}-section ${key}-cta" id="contact">
|
|
1279
|
+
<h2>{title}</h2>
|
|
1280
|
+
<p>{description}</p>
|
|
1281
|
+
<a className="${key}-button" href={buttonHref}>{buttonLabel}</a>
|
|
1282
|
+
</section>
|
|
1283
|
+
),
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
export const SECTION_COMPONENTS = { Hero, FeatureGrid, Testimonial, CallToAction };
|
|
853
1287
|
`
|
|
854
1288
|
);
|
|
855
1289
|
await writeFile(
|
|
@@ -939,6 +1373,90 @@ export const starterPages: ThemeStarterPage[] = [
|
|
|
939
1373
|
eyebrow: "Starter page",
|
|
940
1374
|
title: "Welcome to ${key}",
|
|
941
1375
|
description: "This page was generated by suda theme init.",
|
|
1376
|
+
primaryLabel: "Explore features",
|
|
1377
|
+
primaryHref: "#features",
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
type: "FeatureGrid",
|
|
1382
|
+
props: {
|
|
1383
|
+
id: "FeatureGrid-1",
|
|
1384
|
+
title: "Designed for editable sites",
|
|
1385
|
+
description: "Starter sections show agents and editors how this theme is structured.",
|
|
1386
|
+
features: [
|
|
1387
|
+
{ title: "Typed fields", description: "Each section exposes a clear field schema." },
|
|
1388
|
+
{ title: "Starter pages", description: "Templates show realistic section composition." },
|
|
1389
|
+
{ title: "CLI workflow", description: "Build, validate, preview, and publish from one tool." },
|
|
1390
|
+
],
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
type: "CallToAction",
|
|
1395
|
+
props: {
|
|
1396
|
+
id: "CallToAction-1",
|
|
1397
|
+
title: "Launch your first page",
|
|
1398
|
+
description: "Customize this starter template or let an agent generate a new draft.",
|
|
1399
|
+
buttonLabel: "Get in touch",
|
|
1400
|
+
buttonHref: "/contact",
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
],
|
|
1404
|
+
},
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
slug: "about",
|
|
1408
|
+
title: "About",
|
|
1409
|
+
data: {
|
|
1410
|
+
root: { props: {} },
|
|
1411
|
+
content: [
|
|
1412
|
+
{
|
|
1413
|
+
type: "Hero",
|
|
1414
|
+
props: {
|
|
1415
|
+
id: "Hero-About",
|
|
1416
|
+
eyebrow: "About",
|
|
1417
|
+
title: "A clean starting point for your story",
|
|
1418
|
+
description: "Use this page to introduce the project, audience, and promise.",
|
|
1419
|
+
primaryLabel: "Contact us",
|
|
1420
|
+
primaryHref: "/contact",
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
type: "Testimonial",
|
|
1425
|
+
props: {
|
|
1426
|
+
id: "Testimonial-About",
|
|
1427
|
+
quote: "This starter theme keeps the editable surface focused and predictable.",
|
|
1428
|
+
author: "SudaCloud",
|
|
1429
|
+
role: "Theme team",
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
],
|
|
1433
|
+
},
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
slug: "contact",
|
|
1437
|
+
title: "Contact",
|
|
1438
|
+
data: {
|
|
1439
|
+
root: { props: {} },
|
|
1440
|
+
content: [
|
|
1441
|
+
{
|
|
1442
|
+
type: "Hero",
|
|
1443
|
+
props: {
|
|
1444
|
+
id: "Hero-Contact",
|
|
1445
|
+
eyebrow: "Contact",
|
|
1446
|
+
title: "Let's talk",
|
|
1447
|
+
description: "Tell visitors how to reach you and what happens next.",
|
|
1448
|
+
primaryLabel: "Email us",
|
|
1449
|
+
primaryHref: "mailto:hello@example.com",
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
type: "CallToAction",
|
|
1454
|
+
props: {
|
|
1455
|
+
id: "CallToAction-Contact",
|
|
1456
|
+
title: "Start the conversation",
|
|
1457
|
+
description: "Replace this copy with your preferred contact details or form link.",
|
|
1458
|
+
buttonLabel: "Send an email",
|
|
1459
|
+
buttonHref: "mailto:hello@example.com",
|
|
942
1460
|
},
|
|
943
1461
|
},
|
|
944
1462
|
],
|
|
@@ -971,8 +1489,67 @@ export { manifest, pageConfig, layoutConfig, defaultLayout, starterPages };
|
|
|
971
1489
|
path2.join(target, "src", "runtime.client.ts"),
|
|
972
1490
|
'"use client";\n\nimport theme from "./index.js";\n\nexport default theme;\n'
|
|
973
1491
|
);
|
|
974
|
-
await writeFile(
|
|
1492
|
+
await writeFile(
|
|
1493
|
+
path2.join(target, "styles.css"),
|
|
1494
|
+
`.${key}-root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #111827; background: #ffffff; }
|
|
1495
|
+
.${key}-header, .${key}-footer { padding: 20px clamp(20px, 5vw, 64px); border-bottom: 1px solid #e5e7eb; }
|
|
1496
|
+
.${key}-footer { border-top: 1px solid #e5e7eb; border-bottom: 0; color: #6b7280; }
|
|
1497
|
+
.${key}-section { padding: 64px clamp(20px, 5vw, 64px); }
|
|
1498
|
+
.${key}-hero { background: #f8fafc; }
|
|
1499
|
+
.${key}-eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 12px; color: #2563eb; font-weight: 700; }
|
|
1500
|
+
.${key}-section h1 { max-width: 780px; font-size: clamp(40px, 7vw, 72px); line-height: 0.95; margin: 0 0 20px; }
|
|
1501
|
+
.${key}-section h2 { max-width: 720px; font-size: 36px; line-height: 1.05; margin: 0 0 16px; }
|
|
1502
|
+
.${key}-section p { max-width: 680px; line-height: 1.7; color: #4b5563; }
|
|
1503
|
+
.${key}-button { display: inline-flex; align-items: center; min-height: 42px; padding: 0 18px; border-radius: 8px; background: #111827; color: #ffffff; text-decoration: none; font-weight: 700; }
|
|
1504
|
+
.${key}-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 28px; }
|
|
1505
|
+
.${key}-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; }
|
|
1506
|
+
.${key}-quote blockquote { max-width: 780px; font-size: 28px; line-height: 1.25; margin: 0 0 16px; }
|
|
1507
|
+
.${key}-cta { background: #111827; color: #ffffff; }
|
|
1508
|
+
.${key}-cta p { color: #d1d5db; }
|
|
1509
|
+
.${key}-cta .${key}-button { background: #ffffff; color: #111827; }
|
|
1510
|
+
`
|
|
1511
|
+
);
|
|
975
1512
|
await writeFile(path2.join(target, ".gitignore"), "node_modules\ndist\n*.tsbuildinfo\n");
|
|
1513
|
+
await writeFile(
|
|
1514
|
+
path2.join(target, "AGENTS.md"),
|
|
1515
|
+
`# Suda Theme Agent Guide
|
|
1516
|
+
|
|
1517
|
+
This directory is a Suda theme source project generated by \`suda theme init\`.
|
|
1518
|
+
|
|
1519
|
+
## What to edit
|
|
1520
|
+
|
|
1521
|
+
- Add page sections in \`src/sections.tsx\` and register them in \`SECTION_COMPONENTS\`.
|
|
1522
|
+
- Add starter pages in \`src/templates.ts\` to show realistic section combinations.
|
|
1523
|
+
- Keep shared site chrome in \`src/layout.tsx\`; \`PageOutlet\` is where page content renders.
|
|
1524
|
+
- Keep \`src/index.tsx\` exporting the source \`ThemeModule\`. The final artifact is produced by \`suda theme build\`.
|
|
1525
|
+
|
|
1526
|
+
## Page content rules
|
|
1527
|
+
|
|
1528
|
+
- Agent page files passed to \`suda agent page validate/create\` contain \`content\` and optional \`zones\` only.
|
|
1529
|
+
- Every \`content[]\` item must use a section type from \`SECTION_COMPONENTS\`.
|
|
1530
|
+
- Every \`content[]\` item must include \`props.id\`.
|
|
1531
|
+
- Prefer editing section props over changing render code when generating pages.
|
|
1532
|
+
- Do not edit files under \`dist/\`; they are generated.
|
|
1533
|
+
|
|
1534
|
+
## Useful commands
|
|
1535
|
+
|
|
1536
|
+
\`\`\`bash
|
|
1537
|
+
suda theme build
|
|
1538
|
+
suda theme validate
|
|
1539
|
+
suda theme preview
|
|
1540
|
+
suda agent theme describe local --theme-root .
|
|
1541
|
+
suda agent section schema --theme local --section Hero --theme-root .
|
|
1542
|
+
suda agent page validate --theme local --input ./page.json --theme-root .
|
|
1543
|
+
\`\`\`
|
|
1544
|
+
`
|
|
1545
|
+
);
|
|
1546
|
+
await writeFile(
|
|
1547
|
+
path2.join(target, "CLAUDE.md"),
|
|
1548
|
+
`# Claude Code
|
|
1549
|
+
|
|
1550
|
+
Read \`AGENTS.md\` first. It is the canonical guide for this Suda theme project.
|
|
1551
|
+
`
|
|
1552
|
+
);
|
|
976
1553
|
await writeFile(
|
|
977
1554
|
path2.join(target, "README.md"),
|
|
978
1555
|
`# ${key}
|
|
@@ -982,10 +1559,19 @@ A Suda theme scaffolded with \`suda theme init\`.
|
|
|
982
1559
|
## Develop
|
|
983
1560
|
|
|
984
1561
|
\`\`\`bash
|
|
1562
|
+
suda theme build
|
|
985
1563
|
suda theme dev # rebuild the browser runtime on change
|
|
986
1564
|
suda theme preview # build and preview the home starter page
|
|
987
1565
|
\`\`\`
|
|
988
1566
|
|
|
1567
|
+
## Agent tooling
|
|
1568
|
+
|
|
1569
|
+
\`\`\`bash
|
|
1570
|
+
suda agent theme describe local --theme-root .
|
|
1571
|
+
suda agent page schema local --theme-root .
|
|
1572
|
+
suda agent section schema --theme local --section Hero --theme-root .
|
|
1573
|
+
\`\`\`
|
|
1574
|
+
|
|
989
1575
|
## Publish
|
|
990
1576
|
|
|
991
1577
|
\`\`\`bash
|
|
@@ -997,7 +1583,7 @@ suda theme publish
|
|
|
997
1583
|
);
|
|
998
1584
|
console.log(`created theme scaffold at ${target}`);
|
|
999
1585
|
}
|
|
1000
|
-
|
|
1586
|
+
function buildProgram() {
|
|
1001
1587
|
const program = new Command();
|
|
1002
1588
|
program.name("suda").description("Suda CLI for managing themes, sites, posts and AI tooling.").version("0.0.0");
|
|
1003
1589
|
const theme = program.command("theme").description("Manage Suda theme artifacts.");
|
|
@@ -1032,8 +1618,38 @@ async function main() {
|
|
|
1032
1618
|
theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").action(async (options) => {
|
|
1033
1619
|
await publishTheme(resolveThemeRoot(options), options.skipBuild === true);
|
|
1034
1620
|
});
|
|
1621
|
+
const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
|
|
1622
|
+
const agentThemes = agent.command("themes").description("Manage agent-visible themes.");
|
|
1623
|
+
agentThemes.command("list").description("List themes visible to the current CLI user.").option("--project-id <projectId>", "Scope results to a project.").option("--theme-root <path>", "Describe a local theme instead of the remote catalog.").action(async (options) => {
|
|
1624
|
+
const themes = await listAgentThemes(options);
|
|
1625
|
+
printJson({ themes });
|
|
1626
|
+
});
|
|
1627
|
+
const agentTheme = agent.command("theme").description("Inspect a single theme.");
|
|
1628
|
+
agentTheme.command("describe").description("Describe a theme's AI-first page schema.").argument("<theme>", "Theme key.").option("--version <version>", "Theme version.").option("--project-id <projectId>", "Project scope.").option("--theme-root <path>", "Read a local theme.").option("--example", "Include example page content.").action(async (themeKey, options) => {
|
|
1629
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
1630
|
+
printJson(createAgentPageSchemaOutput(manifest, { includeExample: options.example === true }));
|
|
1631
|
+
});
|
|
1632
|
+
const agentPage = agent.command("page").description("AI-first page content tooling.");
|
|
1633
|
+
agentPage.command("schema").description("Print the AI-first page content output schema for a theme.").argument("<theme>", "Theme key.").option("--version <version>", "Theme version.").option("--project-id <projectId>", "Project scope.").option("--theme-root <path>", "Read a local theme.").option("--example", "Include example page content.").action(async (themeKey, options) => {
|
|
1634
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
1635
|
+
printJson(createAgentPageSchemaOutput(manifest, { includeExample: options.example === true }));
|
|
1636
|
+
});
|
|
1637
|
+
agentPage.command("validate").description("Validate an AI-generated Suda page content JSON document.").requiredOption("--theme <theme>", "Theme key.").requiredOption("--input <file>", "JSON file containing Suda page content.").option("--version <version>", "Theme version.").option("--project-id <projectId>", "Project scope.").option("--theme-root <path>", "Read a local theme.").action(async (options) => {
|
|
1638
|
+
await validateAgentPage(options.theme, options.input, options);
|
|
1639
|
+
});
|
|
1640
|
+
agentPage.command("create").description("Create a workspace page draft from AI-generated Suda page content JSON.").requiredOption("--project-id <projectId>", "Project id.").requiredOption("--title <title>", "Page title.").requiredOption("--slug <slug>", "Page slug.").requiredOption("--theme <theme>", "Theme key.").requiredOption("--input <file>", "JSON file containing Suda page content.").option("--version <version>", "Theme version.").action(async (options) => {
|
|
1641
|
+
await createAgentPage(options);
|
|
1642
|
+
});
|
|
1643
|
+
const agentSection = agent.command("section").description("Inspect a single section.");
|
|
1644
|
+
agentSection.command("schema").description("Print one AI-first section output schema for a theme.").requiredOption("--theme <theme>", "Theme key.").requiredOption("--section <section>", "Section type.").option("--version <version>", "Theme version.").option("--project-id <projectId>", "Project scope.").option("--theme-root <path>", "Read a local theme.").action(async (options) => {
|
|
1645
|
+
const manifest = await fetchAgentManifest(options.theme, options);
|
|
1646
|
+
printJson(createSectionSchema(manifest, options.section));
|
|
1647
|
+
});
|
|
1648
|
+
program.command("mcp").description("Run the Suda local MCP server over stdio.").action(async () => {
|
|
1649
|
+
await startMcpServer();
|
|
1650
|
+
});
|
|
1035
1651
|
const authCmd = program.command("auth").description("Manage Suda authentication.");
|
|
1036
|
-
authCmd.command("login").description("Authenticate Suda CLI with a SudaCloud workspace.").option("--host <host>", "The SudaCloud workspace host to authenticate against.", "
|
|
1652
|
+
authCmd.command("login").description("Authenticate Suda CLI with a SudaCloud workspace.").option("--host <host>", "The SudaCloud workspace host to authenticate against.", "app.sudayun.cn").action(async (options) => {
|
|
1037
1653
|
await login(options.host);
|
|
1038
1654
|
});
|
|
1039
1655
|
authCmd.command("status").description("Check current authentication status.").action(async () => {
|
|
@@ -1043,12 +1659,14 @@ async function main() {
|
|
|
1043
1659
|
await logout();
|
|
1044
1660
|
});
|
|
1045
1661
|
program.showHelpAfterError();
|
|
1662
|
+
return program;
|
|
1663
|
+
}
|
|
1664
|
+
async function main() {
|
|
1665
|
+
const program = buildProgram();
|
|
1046
1666
|
const argv = process.argv.filter((arg, index) => index < 2 || arg !== "--");
|
|
1047
1667
|
await program.parseAsync(argv);
|
|
1048
1668
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
process.exitCode = 1;
|
|
1052
|
-
});
|
|
1669
|
+
|
|
1670
|
+
export { buildProgram, main };
|
|
1053
1671
|
//# sourceMappingURL=index.js.map
|
|
1054
1672
|
//# sourceMappingURL=index.js.map
|