@sudajs/cli 0.0.2 → 0.1.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/bin/suda-dev.js +15 -0
- package/bin/suda.js +41 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +736 -27
- 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}...`);
|
|
@@ -57,14 +61,15 @@ Please open the following URL in your browser to authorize Suda CLI:
|
|
|
57
61
|
`);
|
|
58
62
|
console.log(`Your confirmation code is: ${userCode}
|
|
59
63
|
`);
|
|
60
|
-
console.log("Waiting for authorization...");
|
|
61
64
|
try {
|
|
62
65
|
const open = (await import('open')).default;
|
|
63
66
|
await open(authUrl);
|
|
64
67
|
} catch {
|
|
65
68
|
}
|
|
66
69
|
const pollInterval = (interval || 5) * 1e3;
|
|
67
|
-
const
|
|
70
|
+
const timeoutSeconds = expiresIn || 300;
|
|
71
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
72
|
+
console.log("Waiting for authorization...");
|
|
68
73
|
while (Date.now() < deadline) {
|
|
69
74
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
70
75
|
const pollRes = await fetch(`${baseUrl}/api/cli-auth/poll`, {
|
|
@@ -75,7 +80,7 @@ Please open the following URL in your browser to authorize Suda CLI:
|
|
|
75
80
|
const data = await pollRes.json();
|
|
76
81
|
if (pollRes.ok && data.status === "approved" && data.token) {
|
|
77
82
|
await writeAuthConfig({ sessionToken: data.token, host });
|
|
78
|
-
console.log("
|
|
83
|
+
console.log("Successfully authorized!");
|
|
79
84
|
return;
|
|
80
85
|
}
|
|
81
86
|
if (data.error === "authorization_pending") {
|
|
@@ -120,6 +125,22 @@ async function logout() {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
// src/index.ts
|
|
128
|
+
var createPageDraftOutputSchema = z.discriminatedUnion("status", [
|
|
129
|
+
z.object({
|
|
130
|
+
status: z.literal("created").describe("The page draft was created."),
|
|
131
|
+
pageId: z.string().describe("New page id.")
|
|
132
|
+
}),
|
|
133
|
+
z.object({
|
|
134
|
+
status: z.literal("invalid").describe("The page content failed validation."),
|
|
135
|
+
valid: z.literal(false),
|
|
136
|
+
issues: z.array(
|
|
137
|
+
z.object({
|
|
138
|
+
path: z.string(),
|
|
139
|
+
message: z.string()
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
]);
|
|
123
144
|
function createHostReactShimPlugin() {
|
|
124
145
|
return {
|
|
125
146
|
name: "suda-host-react-shim",
|
|
@@ -266,6 +287,43 @@ async function readJson(filePath) {
|
|
|
266
287
|
const raw = await readFile(filePath, "utf8");
|
|
267
288
|
return JSON.parse(raw);
|
|
268
289
|
}
|
|
290
|
+
async function readJsonIfExists(filePath) {
|
|
291
|
+
if (!await pathExists(filePath)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return readJson(filePath);
|
|
295
|
+
}
|
|
296
|
+
function protocolForHost(host) {
|
|
297
|
+
return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
298
|
+
}
|
|
299
|
+
async function requireCliBaseUrl() {
|
|
300
|
+
const config = await readAuthConfig();
|
|
301
|
+
if (!config) {
|
|
302
|
+
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
baseUrl: `${protocolForHost(config.host)}://${config.host}`,
|
|
306
|
+
token: config.sessionToken
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function fetchJson(url, options = {}) {
|
|
310
|
+
const headers = new Headers(options.headers);
|
|
311
|
+
if (options.token) {
|
|
312
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
313
|
+
}
|
|
314
|
+
const response = await fetch(url, { ...options, headers });
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
const errorData = await response.json().catch(() => ({}));
|
|
317
|
+
throw new Error(errorData.message || errorData.error || response.statusText);
|
|
318
|
+
}
|
|
319
|
+
return await response.json();
|
|
320
|
+
}
|
|
321
|
+
function agentCacheDir() {
|
|
322
|
+
return path2.join(process.env.XDG_CACHE_HOME ?? path2.join(process.env.HOME ?? ".", ".cache"), "suda", "agent");
|
|
323
|
+
}
|
|
324
|
+
function cachedThemeManifestPath(themeKey, version) {
|
|
325
|
+
return path2.join(agentCacheDir(), "themes", safeObjectKeyPart(themeKey), safeObjectKeyPart(version), "agent-manifest.json");
|
|
326
|
+
}
|
|
269
327
|
async function loadThemeModule(serverEntryPath) {
|
|
270
328
|
const imported = await import(`${pathToFileURL(serverEntryPath).href}?t=${Date.now()}`);
|
|
271
329
|
if (!imported.default) {
|
|
@@ -432,6 +490,7 @@ async function watchTheme(root, skipThemeBuild) {
|
|
|
432
490
|
const entryPoint = await findClientEntry(root);
|
|
433
491
|
await mkdir(path2.join(root, "dist"), { recursive: true });
|
|
434
492
|
const hostReactShimPlugin = createHostReactShimPlugin();
|
|
493
|
+
const watchLogPlugin = createWatchLogPlugin("runtime.client.js");
|
|
435
494
|
const context$1 = await context({
|
|
436
495
|
bundle: true,
|
|
437
496
|
entryPoints: [entryPoint],
|
|
@@ -440,7 +499,7 @@ async function watchTheme(root, skipThemeBuild) {
|
|
|
440
499
|
minify: false,
|
|
441
500
|
outfile: path2.join(root, "dist", "runtime.client.js"),
|
|
442
501
|
platform: "browser",
|
|
443
|
-
plugins: [hostReactShimPlugin],
|
|
502
|
+
plugins: [hostReactShimPlugin, watchLogPlugin],
|
|
444
503
|
sourcemap: true,
|
|
445
504
|
target: "es2022",
|
|
446
505
|
treeShaking: true
|
|
@@ -449,6 +508,27 @@ async function watchTheme(root, skipThemeBuild) {
|
|
|
449
508
|
console.log("watching theme runtime; press Ctrl+C to stop");
|
|
450
509
|
await new Promise(() => void 0);
|
|
451
510
|
}
|
|
511
|
+
function createWatchLogPlugin(label) {
|
|
512
|
+
return {
|
|
513
|
+
name: "suda-theme-watch-log",
|
|
514
|
+
setup(build) {
|
|
515
|
+
let firstBuild = true;
|
|
516
|
+
build.onEnd((result) => {
|
|
517
|
+
if (firstBuild) {
|
|
518
|
+
firstBuild = false;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const errorCount = result.errors.length;
|
|
522
|
+
if (errorCount > 0) {
|
|
523
|
+
console.log(`rebuild failed (${errorCount} error${errorCount === 1 ? "" : "s"}): ${label}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const stamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
527
|
+
console.log(`[${stamp}] rebuilt ${label}`);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
452
532
|
function pickStarterPage(theme) {
|
|
453
533
|
const pages = theme.module.starterPages;
|
|
454
534
|
if (pages.length === 0) {
|
|
@@ -617,8 +697,292 @@ async function screenshotTheme(root, options) {
|
|
|
617
697
|
const theme = resolved.skipBuild ? await validateTheme(root) : await buildTheme(root, false);
|
|
618
698
|
await captureScreenshot(theme, resolved);
|
|
619
699
|
}
|
|
700
|
+
async function loadLocalAgentManifest(root) {
|
|
701
|
+
const built = await readJsonIfExists(path2.join(root, "dist", "agent-manifest.json"));
|
|
702
|
+
if (built) {
|
|
703
|
+
return built;
|
|
704
|
+
}
|
|
705
|
+
throw new Error(
|
|
706
|
+
`No agent manifest found at ${path2.join(root, "dist", "agent-manifest.json")}. Run "suda theme build" first.`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
async function listAgentThemes(options) {
|
|
710
|
+
if (options.themeRoot) {
|
|
711
|
+
const manifest = await loadLocalAgentManifest(resolveThemeRoot(options));
|
|
712
|
+
return [
|
|
713
|
+
{
|
|
714
|
+
key: manifest.manifest.key,
|
|
715
|
+
name: manifest.manifest.name,
|
|
716
|
+
version: manifest.manifest.version,
|
|
717
|
+
description: manifest.manifest.description ?? null,
|
|
718
|
+
categories: manifest.manifest.categories,
|
|
719
|
+
preview: manifest.manifest.preview
|
|
720
|
+
}
|
|
721
|
+
];
|
|
722
|
+
}
|
|
723
|
+
const auth = await requireCliBaseUrl();
|
|
724
|
+
const url = new URL("/api/cli/agent/themes", auth.baseUrl);
|
|
725
|
+
if (options.projectId) {
|
|
726
|
+
url.searchParams.set("projectId", options.projectId);
|
|
727
|
+
}
|
|
728
|
+
const catalog = await fetchJson(url.href, { token: auth.token });
|
|
729
|
+
await mkdir(agentCacheDir(), { recursive: true });
|
|
730
|
+
await writeFile(path2.join(agentCacheDir(), "catalog.json"), `${JSON.stringify(catalog, null, 2)}
|
|
731
|
+
`);
|
|
732
|
+
return catalog.themes;
|
|
733
|
+
}
|
|
734
|
+
async function fetchAgentManifest(themeKey, options) {
|
|
735
|
+
if (options.themeRoot) {
|
|
736
|
+
return loadLocalAgentManifest(resolveThemeRoot(options));
|
|
737
|
+
}
|
|
738
|
+
const auth = await requireCliBaseUrl();
|
|
739
|
+
const version = options.version ?? "latest";
|
|
740
|
+
const cachePath = cachedThemeManifestPath(themeKey, version);
|
|
741
|
+
const cached = version === "latest" ? null : await readJsonIfExists(cachePath);
|
|
742
|
+
if (cached) {
|
|
743
|
+
return cached;
|
|
744
|
+
}
|
|
745
|
+
const url = new URL(`/api/cli/agent/themes/${encodeURIComponent(themeKey)}/${encodeURIComponent(version)}/schema`, auth.baseUrl);
|
|
746
|
+
if (options.projectId) {
|
|
747
|
+
url.searchParams.set("projectId", options.projectId);
|
|
748
|
+
}
|
|
749
|
+
const result = await fetchJson(url.href, { token: auth.token });
|
|
750
|
+
const realVersion = result.agentManifest.manifest.version;
|
|
751
|
+
const realCachePath = cachedThemeManifestPath(themeKey, realVersion);
|
|
752
|
+
await mkdir(path2.dirname(realCachePath), { recursive: true });
|
|
753
|
+
await writeFile(realCachePath, `${JSON.stringify(result.agentManifest, null, 2)}
|
|
754
|
+
`);
|
|
755
|
+
return result.agentManifest;
|
|
756
|
+
}
|
|
757
|
+
function printJson(value) {
|
|
758
|
+
console.log(JSON.stringify(value, null, 2));
|
|
759
|
+
}
|
|
760
|
+
function createSectionSchema(manifest, sectionType) {
|
|
761
|
+
const section = findAgentSection(manifest, sectionType);
|
|
762
|
+
if (!section) {
|
|
763
|
+
throw new Error(`Unknown section type "${sectionType}".`);
|
|
764
|
+
}
|
|
765
|
+
return createAgentComponentSchemaOutput(section, "section");
|
|
766
|
+
}
|
|
767
|
+
async function validateAgentPage(themeKey, input, options) {
|
|
768
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
769
|
+
const pageContent = await readJson(path2.resolve(input));
|
|
770
|
+
const result = validateAgentPageContentWithManifest(pageContent, manifest);
|
|
771
|
+
printJson(agentValidationResultSchema.parse(result));
|
|
772
|
+
if (!result.valid) {
|
|
773
|
+
process.exitCode = 1;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function createAgentPage(options) {
|
|
777
|
+
if (!options.projectId) {
|
|
778
|
+
throw new Error("--project-id is required.");
|
|
779
|
+
}
|
|
780
|
+
const pageContent = await readJson(path2.resolve(options.input));
|
|
781
|
+
const manifest = await fetchAgentManifest(options.theme, options);
|
|
782
|
+
const validation = validateAgentPageContentWithManifest(pageContent, manifest);
|
|
783
|
+
if (!validation.valid) {
|
|
784
|
+
printJson(agentValidationResultSchema.parse(validation));
|
|
785
|
+
process.exitCode = 1;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const pageData = createPageDataFromAgentContent(pageContent);
|
|
789
|
+
const auth = await requireCliBaseUrl();
|
|
790
|
+
const response = await fetchJson(
|
|
791
|
+
`${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(options.projectId)}/pages`,
|
|
792
|
+
{
|
|
793
|
+
method: "POST",
|
|
794
|
+
token: auth.token,
|
|
795
|
+
headers: { "Content-Type": "application/json" },
|
|
796
|
+
body: JSON.stringify({
|
|
797
|
+
title: options.title,
|
|
798
|
+
slug: options.slug,
|
|
799
|
+
themeKey: manifest.manifest.key,
|
|
800
|
+
themeVersion: manifest.manifest.version,
|
|
801
|
+
data: pageData
|
|
802
|
+
})
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
printJson({ success: true, ...response });
|
|
806
|
+
}
|
|
807
|
+
function mcpStructured(summary, value) {
|
|
808
|
+
return {
|
|
809
|
+
content: [
|
|
810
|
+
{
|
|
811
|
+
type: "text",
|
|
812
|
+
text: summary
|
|
813
|
+
}
|
|
814
|
+
],
|
|
815
|
+
structuredContent: value
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
async function startMcpServer() {
|
|
819
|
+
const server = new McpServer({
|
|
820
|
+
name: "suda",
|
|
821
|
+
version: "0.1.0"
|
|
822
|
+
});
|
|
823
|
+
server.registerTool(
|
|
824
|
+
"list_themes",
|
|
825
|
+
{
|
|
826
|
+
description: "List themes visible to the current Suda CLI user.",
|
|
827
|
+
inputSchema: {
|
|
828
|
+
projectId: z.string().optional(),
|
|
829
|
+
themeRoot: z.string().optional()
|
|
830
|
+
},
|
|
831
|
+
outputSchema: {
|
|
832
|
+
themes: z.array(
|
|
833
|
+
z.object({
|
|
834
|
+
key: z.string(),
|
|
835
|
+
name: z.string(),
|
|
836
|
+
version: z.string(),
|
|
837
|
+
description: z.string().nullable().optional(),
|
|
838
|
+
categories: z.array(z.string()).optional(),
|
|
839
|
+
preview: z.string().optional(),
|
|
840
|
+
active: z.boolean().optional()
|
|
841
|
+
})
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
async ({ projectId, themeRoot }) => {
|
|
846
|
+
const structuredContent = { themes: await listAgentThemes({ projectId, themeRoot }) };
|
|
847
|
+
return mcpStructured("Available Suda themes.", structuredContent);
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
server.registerTool(
|
|
851
|
+
"describe_theme",
|
|
852
|
+
{
|
|
853
|
+
description: "Return the raw Suda agent manifest for a theme.",
|
|
854
|
+
inputSchema: {
|
|
855
|
+
theme: z.string(),
|
|
856
|
+
version: z.string().optional(),
|
|
857
|
+
projectId: z.string().optional(),
|
|
858
|
+
themeRoot: z.string().optional()
|
|
859
|
+
},
|
|
860
|
+
outputSchema: {
|
|
861
|
+
agentManifest: z.unknown()
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
async ({ theme, version, projectId, themeRoot }) => {
|
|
865
|
+
const structuredContent = {
|
|
866
|
+
agentManifest: await fetchAgentManifest(theme, { version, projectId, themeRoot })
|
|
867
|
+
};
|
|
868
|
+
return mcpStructured("Raw Suda theme agent manifest.", structuredContent);
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
server.registerTool(
|
|
872
|
+
"get_page_schema",
|
|
873
|
+
{
|
|
874
|
+
description: "Return the AI-first output schema for generating Suda page content, including all section schemas.",
|
|
875
|
+
inputSchema: {
|
|
876
|
+
theme: z.string(),
|
|
877
|
+
version: z.string().optional(),
|
|
878
|
+
projectId: z.string().optional(),
|
|
879
|
+
themeRoot: z.string().optional(),
|
|
880
|
+
includeExample: z.boolean().optional()
|
|
881
|
+
},
|
|
882
|
+
outputSchema: agentPageSchemaOutputSchema.shape
|
|
883
|
+
},
|
|
884
|
+
async ({ theme, version, projectId, themeRoot, includeExample }) => {
|
|
885
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
886
|
+
const structuredContent = createAgentPageSchemaOutput(manifest, {
|
|
887
|
+
includeExample: includeExample === true
|
|
888
|
+
});
|
|
889
|
+
return mcpStructured("AI-first Suda page content output schema.", structuredContent);
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
server.registerTool(
|
|
893
|
+
"get_section_schema",
|
|
894
|
+
{
|
|
895
|
+
description: "Return the AI-first output schema for one section component in a theme.",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
theme: z.string(),
|
|
898
|
+
section: z.string(),
|
|
899
|
+
version: z.string().optional(),
|
|
900
|
+
projectId: z.string().optional(),
|
|
901
|
+
themeRoot: z.string().optional()
|
|
902
|
+
},
|
|
903
|
+
outputSchema: agentComponentOutputSchema.shape
|
|
904
|
+
},
|
|
905
|
+
async ({ theme, section, version, projectId, themeRoot }) => {
|
|
906
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
907
|
+
const structuredContent = createSectionSchema(manifest, section);
|
|
908
|
+
return mcpStructured("AI-first Suda section output schema.", structuredContent);
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
server.registerTool(
|
|
912
|
+
"validate_page_config",
|
|
913
|
+
{
|
|
914
|
+
description: "Validate AI-generated Suda page content against a theme.",
|
|
915
|
+
inputSchema: {
|
|
916
|
+
theme: z.string(),
|
|
917
|
+
data: z.unknown(),
|
|
918
|
+
version: z.string().optional(),
|
|
919
|
+
projectId: z.string().optional(),
|
|
920
|
+
themeRoot: z.string().optional()
|
|
921
|
+
},
|
|
922
|
+
outputSchema: agentValidationResultSchema.shape
|
|
923
|
+
},
|
|
924
|
+
async ({ theme, data, version, projectId, themeRoot }) => {
|
|
925
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId, themeRoot });
|
|
926
|
+
const structuredContent = agentValidationResultSchema.parse(
|
|
927
|
+
validateAgentPageContentWithManifest(data, manifest)
|
|
928
|
+
);
|
|
929
|
+
return mcpStructured("Suda page content validation result.", structuredContent);
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
server.registerTool(
|
|
933
|
+
"create_page_draft",
|
|
934
|
+
{
|
|
935
|
+
description: "Create a workspace page draft from AI-generated Suda page content.",
|
|
936
|
+
inputSchema: {
|
|
937
|
+
projectId: z.string(),
|
|
938
|
+
title: z.string(),
|
|
939
|
+
slug: z.string(),
|
|
940
|
+
theme: z.string(),
|
|
941
|
+
data: z.unknown(),
|
|
942
|
+
version: z.string().optional()
|
|
943
|
+
},
|
|
944
|
+
outputSchema: createPageDraftOutputSchema
|
|
945
|
+
},
|
|
946
|
+
async ({ projectId, title, slug, theme, data, version }) => {
|
|
947
|
+
const manifest = await fetchAgentManifest(theme, { version, projectId });
|
|
948
|
+
const validation = validateAgentPageContentWithManifest(data, manifest);
|
|
949
|
+
if (!validation.valid) {
|
|
950
|
+
return mcpStructured(
|
|
951
|
+
"Suda page content validation failed. Fix issues before creating a draft.",
|
|
952
|
+
createPageDraftOutputSchema.parse({
|
|
953
|
+
status: "invalid",
|
|
954
|
+
valid: false,
|
|
955
|
+
issues: validation.issues
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const auth = await requireCliBaseUrl();
|
|
960
|
+
const response = await fetchJson(
|
|
961
|
+
`${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/pages`,
|
|
962
|
+
{
|
|
963
|
+
method: "POST",
|
|
964
|
+
token: auth.token,
|
|
965
|
+
headers: { "Content-Type": "application/json" },
|
|
966
|
+
body: JSON.stringify({
|
|
967
|
+
title,
|
|
968
|
+
slug,
|
|
969
|
+
themeKey: manifest.manifest.key,
|
|
970
|
+
themeVersion: manifest.manifest.version,
|
|
971
|
+
data: createPageDataFromAgentContent(data)
|
|
972
|
+
})
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
return mcpStructured(
|
|
976
|
+
"Created Suda page draft.",
|
|
977
|
+
createPageDraftOutputSchema.parse({ status: "created", ...response })
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
);
|
|
981
|
+
await server.connect(new StdioServerTransport());
|
|
982
|
+
}
|
|
620
983
|
async function writeThemeArtifacts(theme) {
|
|
621
984
|
const dist = path2.join(theme.root, "dist");
|
|
985
|
+
const agentManifest = createThemeAgentManifest(theme.module);
|
|
622
986
|
await writeFile(
|
|
623
987
|
path2.join(dist, "manifest.json"),
|
|
624
988
|
`${JSON.stringify(theme.module.manifest, null, 2)}
|
|
@@ -632,6 +996,11 @@ async function writeThemeArtifacts(theme) {
|
|
|
632
996
|
await writeFile(
|
|
633
997
|
path2.join(dist, "default-layout.json"),
|
|
634
998
|
`${JSON.stringify(theme.module.defaultLayout, null, 2)}
|
|
999
|
+
`
|
|
1000
|
+
);
|
|
1001
|
+
await writeFile(
|
|
1002
|
+
path2.join(dist, "agent-manifest.json"),
|
|
1003
|
+
`${JSON.stringify(agentManifest, null, 2)}
|
|
635
1004
|
`
|
|
636
1005
|
);
|
|
637
1006
|
}
|
|
@@ -709,8 +1078,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
709
1078
|
if (!config) {
|
|
710
1079
|
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
711
1080
|
}
|
|
712
|
-
const
|
|
713
|
-
const baseUrl = `${protocol}://${config.host}`;
|
|
1081
|
+
const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
|
|
714
1082
|
const { key, version } = theme.module.manifest;
|
|
715
1083
|
const intentRes = await fetch(`${baseUrl}/api/cli/themes/publish-intent`, {
|
|
716
1084
|
method: "POST",
|
|
@@ -755,6 +1123,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
755
1123
|
}
|
|
756
1124
|
const previewArtifactRelative = "dist/preview/desktop.png";
|
|
757
1125
|
const previewArtifactExists = await pathExists(path2.join(theme.root, previewArtifactRelative));
|
|
1126
|
+
const agentManifest = createThemeAgentManifest(theme.module);
|
|
758
1127
|
const completeRes = await fetch(`${baseUrl}/api/cli/themes/publish-complete`, {
|
|
759
1128
|
method: "POST",
|
|
760
1129
|
headers: {
|
|
@@ -766,6 +1135,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
766
1135
|
version,
|
|
767
1136
|
checksum: digest,
|
|
768
1137
|
manifest: theme.module.manifest,
|
|
1138
|
+
agentManifest,
|
|
769
1139
|
bundleArtifactRelative,
|
|
770
1140
|
previewArtifactRelative: previewArtifactExists ? previewArtifactRelative : void 0
|
|
771
1141
|
})
|
|
@@ -786,14 +1156,35 @@ async function initTheme(target) {
|
|
|
786
1156
|
name: `@suda-themes/${key}`,
|
|
787
1157
|
version: "0.1.0",
|
|
788
1158
|
private: true,
|
|
1159
|
+
packageManager: "pnpm@11.6.0",
|
|
789
1160
|
type: "module",
|
|
790
1161
|
main: "./dist/index.js",
|
|
791
1162
|
types: "./dist/index.d.ts",
|
|
792
1163
|
scripts: {
|
|
793
|
-
build:
|
|
794
|
-
|
|
1164
|
+
// `build:src` compiles TypeScript sources to dist/. `build` then runs
|
|
1165
|
+
// `suda theme build --skip-theme-build` so the CLI does not recurse
|
|
1166
|
+
// back into this script (it would otherwise see scripts.build and
|
|
1167
|
+
// call `pnpm run build` again, causing infinite recursion).
|
|
1168
|
+
"build:src": "tsc -p tsconfig.json",
|
|
1169
|
+
build: "pnpm run build:src && suda theme build --theme-root . --skip-theme-build",
|
|
1170
|
+
dev: "suda theme dev --theme-root . --skip-theme-build",
|
|
1171
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
1172
|
+
validate: "suda theme validate --theme-root .",
|
|
1173
|
+
preview: "suda theme preview --theme-root ."
|
|
1174
|
+
},
|
|
1175
|
+
dependencies: {
|
|
1176
|
+
"@sudajs/theme-engine": "^0.1.1"
|
|
1177
|
+
},
|
|
1178
|
+
devDependencies: {
|
|
1179
|
+
"@puckeditor/core": "^0.21.2",
|
|
1180
|
+
"@sudajs/cli": "^0.1.0",
|
|
1181
|
+
"@types/node": "^22.0.0",
|
|
1182
|
+
"@types/react": "^19.0.0",
|
|
1183
|
+
"@types/react-dom": "^19.0.0",
|
|
1184
|
+
react: "^19.0.0",
|
|
1185
|
+
"react-dom": "^19.0.0",
|
|
1186
|
+
typescript: "^5.6.0"
|
|
795
1187
|
},
|
|
796
|
-
dependencies: { "@sudajs/theme-engine": "workspace:*" },
|
|
797
1188
|
peerDependencies: {
|
|
798
1189
|
"@puckeditor/core": "^0.21.2",
|
|
799
1190
|
react: "^19.0.0",
|
|
@@ -803,11 +1194,45 @@ async function initTheme(target) {
|
|
|
803
1194
|
null,
|
|
804
1195
|
2
|
|
805
1196
|
)}
|
|
1197
|
+
`
|
|
1198
|
+
);
|
|
1199
|
+
await writeFile(
|
|
1200
|
+
path2.join(target, "pnpm-workspace.yaml"),
|
|
1201
|
+
`# Single-package workspace so pnpm 11 can apply per-project settings.
|
|
1202
|
+
# allowBuilds replaces the pre-v11 onlyBuiltDependencies list.
|
|
1203
|
+
allowBuilds:
|
|
1204
|
+
esbuild: true
|
|
806
1205
|
`
|
|
807
1206
|
);
|
|
808
1207
|
await writeFile(
|
|
809
1208
|
path2.join(target, "tsconfig.json"),
|
|
810
|
-
|
|
1209
|
+
`${JSON.stringify(
|
|
1210
|
+
{
|
|
1211
|
+
compilerOptions: {
|
|
1212
|
+
target: "ES2022",
|
|
1213
|
+
lib: ["DOM", "DOM.Iterable", "ES2022"],
|
|
1214
|
+
module: "NodeNext",
|
|
1215
|
+
moduleResolution: "NodeNext",
|
|
1216
|
+
jsx: "react-jsx",
|
|
1217
|
+
rootDir: "src",
|
|
1218
|
+
outDir: "dist",
|
|
1219
|
+
declaration: true,
|
|
1220
|
+
declarationMap: true,
|
|
1221
|
+
sourceMap: true,
|
|
1222
|
+
strict: true,
|
|
1223
|
+
noUncheckedIndexedAccess: true,
|
|
1224
|
+
esModuleInterop: true,
|
|
1225
|
+
skipLibCheck: true,
|
|
1226
|
+
forceConsistentCasingInFileNames: true,
|
|
1227
|
+
resolveJsonModule: true,
|
|
1228
|
+
types: ["node"]
|
|
1229
|
+
},
|
|
1230
|
+
include: ["src"]
|
|
1231
|
+
},
|
|
1232
|
+
null,
|
|
1233
|
+
2
|
|
1234
|
+
)}
|
|
1235
|
+
`
|
|
811
1236
|
);
|
|
812
1237
|
await writeFile(
|
|
813
1238
|
path2.join(target, "src", "manifest.ts"),
|
|
@@ -834,22 +1259,109 @@ export const Hero: ComponentConfig = {
|
|
|
834
1259
|
eyebrow: { type: "text", label: "Eyebrow" },
|
|
835
1260
|
title: { type: "text", label: "Title" },
|
|
836
1261
|
description: { type: "textarea", label: "Description" },
|
|
1262
|
+
primaryLabel: { type: "text", label: "Primary button label" },
|
|
1263
|
+
primaryHref: { type: "text", label: "Primary button link" },
|
|
837
1264
|
},
|
|
838
1265
|
defaultProps: {
|
|
839
1266
|
eyebrow: "New theme",
|
|
840
1267
|
title: "Build with SudaCloud",
|
|
841
1268
|
description: "Edit this starter section in the visual editor.",
|
|
1269
|
+
primaryLabel: "Get started",
|
|
1270
|
+
primaryHref: "#contact",
|
|
842
1271
|
},
|
|
843
|
-
render: ({ eyebrow, title, description }) => (
|
|
844
|
-
<section className="${key}-hero">
|
|
845
|
-
<p>{eyebrow}</p>
|
|
1272
|
+
render: ({ eyebrow, title, description, primaryLabel, primaryHref }) => (
|
|
1273
|
+
<section className="${key}-section ${key}-hero">
|
|
1274
|
+
<p className="${key}-eyebrow">{eyebrow}</p>
|
|
846
1275
|
<h1>{title}</h1>
|
|
847
|
-
<
|
|
1276
|
+
<p>{description}</p>
|
|
1277
|
+
<a className="${key}-button" href={primaryHref}>{primaryLabel}</a>
|
|
848
1278
|
</section>
|
|
849
1279
|
),
|
|
850
1280
|
};
|
|
851
1281
|
|
|
852
|
-
export const
|
|
1282
|
+
export const FeatureGrid: ComponentConfig = {
|
|
1283
|
+
label: "Feature grid",
|
|
1284
|
+
fields: {
|
|
1285
|
+
title: { type: "text", label: "Title" },
|
|
1286
|
+
description: { type: "textarea", label: "Description" },
|
|
1287
|
+
features: {
|
|
1288
|
+
type: "array",
|
|
1289
|
+
label: "Features",
|
|
1290
|
+
arrayFields: {
|
|
1291
|
+
title: { type: "text", label: "Title" },
|
|
1292
|
+
description: { type: "textarea", label: "Description" },
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
defaultProps: {
|
|
1297
|
+
title: "Everything you need to launch",
|
|
1298
|
+
description: "Use this section to explain the core value of the project.",
|
|
1299
|
+
features: [
|
|
1300
|
+
{ title: "Fast setup", description: "Start from a clean theme contract." },
|
|
1301
|
+
{ title: "Visual editing", description: "Expose content fields through Puck." },
|
|
1302
|
+
{ title: "Publish ready", description: "Build and publish with the Suda CLI." },
|
|
1303
|
+
],
|
|
1304
|
+
},
|
|
1305
|
+
render: ({ title, description, features = [] }) => (
|
|
1306
|
+
<section className="${key}-section">
|
|
1307
|
+
<h2>{title}</h2>
|
|
1308
|
+
<p>{description}</p>
|
|
1309
|
+
<div className="${key}-grid">
|
|
1310
|
+
{features.map((feature: { title?: string; description?: string }, index: number) => (
|
|
1311
|
+
<article className="${key}-card" key={index}>
|
|
1312
|
+
<h3>{feature.title}</h3>
|
|
1313
|
+
<p>{feature.description}</p>
|
|
1314
|
+
</article>
|
|
1315
|
+
))}
|
|
1316
|
+
</div>
|
|
1317
|
+
</section>
|
|
1318
|
+
),
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
export const Testimonial: ComponentConfig = {
|
|
1322
|
+
label: "Testimonial",
|
|
1323
|
+
fields: {
|
|
1324
|
+
quote: { type: "textarea", label: "Quote" },
|
|
1325
|
+
author: { type: "text", label: "Author" },
|
|
1326
|
+
role: { type: "text", label: "Role" },
|
|
1327
|
+
},
|
|
1328
|
+
defaultProps: {
|
|
1329
|
+
quote: "SudaCloud gives our team a practical editing workflow without giving up theme control.",
|
|
1330
|
+
author: "Alex Chen",
|
|
1331
|
+
role: "Founder",
|
|
1332
|
+
},
|
|
1333
|
+
render: ({ quote, author, role }) => (
|
|
1334
|
+
<section className="${key}-section ${key}-quote">
|
|
1335
|
+
<blockquote>{quote}</blockquote>
|
|
1336
|
+
<p>{author} \xB7 {role}</p>
|
|
1337
|
+
</section>
|
|
1338
|
+
),
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
export const CallToAction: ComponentConfig = {
|
|
1342
|
+
label: "Call to action",
|
|
1343
|
+
fields: {
|
|
1344
|
+
title: { type: "text", label: "Title" },
|
|
1345
|
+
description: { type: "textarea", label: "Description" },
|
|
1346
|
+
buttonLabel: { type: "text", label: "Button label" },
|
|
1347
|
+
buttonHref: { type: "text", label: "Button link" },
|
|
1348
|
+
},
|
|
1349
|
+
defaultProps: {
|
|
1350
|
+
title: "Ready to build your next page?",
|
|
1351
|
+
description: "Use this section as the final conversion block.",
|
|
1352
|
+
buttonLabel: "Contact us",
|
|
1353
|
+
buttonHref: "#contact",
|
|
1354
|
+
},
|
|
1355
|
+
render: ({ title, description, buttonLabel, buttonHref }) => (
|
|
1356
|
+
<section className="${key}-section ${key}-cta" id="contact">
|
|
1357
|
+
<h2>{title}</h2>
|
|
1358
|
+
<p>{description}</p>
|
|
1359
|
+
<a className="${key}-button" href={buttonHref}>{buttonLabel}</a>
|
|
1360
|
+
</section>
|
|
1361
|
+
),
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
export const SECTION_COMPONENTS = { Hero, FeatureGrid, Testimonial, CallToAction };
|
|
853
1365
|
`
|
|
854
1366
|
);
|
|
855
1367
|
await writeFile(
|
|
@@ -939,6 +1451,90 @@ export const starterPages: ThemeStarterPage[] = [
|
|
|
939
1451
|
eyebrow: "Starter page",
|
|
940
1452
|
title: "Welcome to ${key}",
|
|
941
1453
|
description: "This page was generated by suda theme init.",
|
|
1454
|
+
primaryLabel: "Explore features",
|
|
1455
|
+
primaryHref: "#features",
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
type: "FeatureGrid",
|
|
1460
|
+
props: {
|
|
1461
|
+
id: "FeatureGrid-1",
|
|
1462
|
+
title: "Designed for editable sites",
|
|
1463
|
+
description: "Starter sections show agents and editors how this theme is structured.",
|
|
1464
|
+
features: [
|
|
1465
|
+
{ title: "Typed fields", description: "Each section exposes a clear field schema." },
|
|
1466
|
+
{ title: "Starter pages", description: "Templates show realistic section composition." },
|
|
1467
|
+
{ title: "CLI workflow", description: "Build, validate, preview, and publish from one tool." },
|
|
1468
|
+
],
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
type: "CallToAction",
|
|
1473
|
+
props: {
|
|
1474
|
+
id: "CallToAction-1",
|
|
1475
|
+
title: "Launch your first page",
|
|
1476
|
+
description: "Customize this starter template or let an agent generate a new draft.",
|
|
1477
|
+
buttonLabel: "Get in touch",
|
|
1478
|
+
buttonHref: "/contact",
|
|
1479
|
+
},
|
|
1480
|
+
},
|
|
1481
|
+
],
|
|
1482
|
+
},
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
slug: "about",
|
|
1486
|
+
title: "About",
|
|
1487
|
+
data: {
|
|
1488
|
+
root: { props: {} },
|
|
1489
|
+
content: [
|
|
1490
|
+
{
|
|
1491
|
+
type: "Hero",
|
|
1492
|
+
props: {
|
|
1493
|
+
id: "Hero-About",
|
|
1494
|
+
eyebrow: "About",
|
|
1495
|
+
title: "A clean starting point for your story",
|
|
1496
|
+
description: "Use this page to introduce the project, audience, and promise.",
|
|
1497
|
+
primaryLabel: "Contact us",
|
|
1498
|
+
primaryHref: "/contact",
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
type: "Testimonial",
|
|
1503
|
+
props: {
|
|
1504
|
+
id: "Testimonial-About",
|
|
1505
|
+
quote: "This starter theme keeps the editable surface focused and predictable.",
|
|
1506
|
+
author: "SudaCloud",
|
|
1507
|
+
role: "Theme team",
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
],
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
slug: "contact",
|
|
1515
|
+
title: "Contact",
|
|
1516
|
+
data: {
|
|
1517
|
+
root: { props: {} },
|
|
1518
|
+
content: [
|
|
1519
|
+
{
|
|
1520
|
+
type: "Hero",
|
|
1521
|
+
props: {
|
|
1522
|
+
id: "Hero-Contact",
|
|
1523
|
+
eyebrow: "Contact",
|
|
1524
|
+
title: "Let's talk",
|
|
1525
|
+
description: "Tell visitors how to reach you and what happens next.",
|
|
1526
|
+
primaryLabel: "Email us",
|
|
1527
|
+
primaryHref: "mailto:hello@example.com",
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
type: "CallToAction",
|
|
1532
|
+
props: {
|
|
1533
|
+
id: "CallToAction-Contact",
|
|
1534
|
+
title: "Start the conversation",
|
|
1535
|
+
description: "Replace this copy with your preferred contact details or form link.",
|
|
1536
|
+
buttonLabel: "Send an email",
|
|
1537
|
+
buttonHref: "mailto:hello@example.com",
|
|
942
1538
|
},
|
|
943
1539
|
},
|
|
944
1540
|
],
|
|
@@ -971,25 +1567,106 @@ export { manifest, pageConfig, layoutConfig, defaultLayout, starterPages };
|
|
|
971
1567
|
path2.join(target, "src", "runtime.client.ts"),
|
|
972
1568
|
'"use client";\n\nimport theme from "./index.js";\n\nexport default theme;\n'
|
|
973
1569
|
);
|
|
974
|
-
await writeFile(
|
|
1570
|
+
await writeFile(
|
|
1571
|
+
path2.join(target, "styles.css"),
|
|
1572
|
+
`.${key}-root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #111827; background: #ffffff; }
|
|
1573
|
+
.${key}-header, .${key}-footer { padding: 20px clamp(20px, 5vw, 64px); border-bottom: 1px solid #e5e7eb; }
|
|
1574
|
+
.${key}-footer { border-top: 1px solid #e5e7eb; border-bottom: 0; color: #6b7280; }
|
|
1575
|
+
.${key}-section { padding: 64px clamp(20px, 5vw, 64px); }
|
|
1576
|
+
.${key}-hero { background: #f8fafc; }
|
|
1577
|
+
.${key}-eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 12px; color: #2563eb; font-weight: 700; }
|
|
1578
|
+
.${key}-section h1 { max-width: 780px; font-size: clamp(40px, 7vw, 72px); line-height: 0.95; margin: 0 0 20px; }
|
|
1579
|
+
.${key}-section h2 { max-width: 720px; font-size: 36px; line-height: 1.05; margin: 0 0 16px; }
|
|
1580
|
+
.${key}-section p { max-width: 680px; line-height: 1.7; color: #4b5563; }
|
|
1581
|
+
.${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; }
|
|
1582
|
+
.${key}-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 28px; }
|
|
1583
|
+
.${key}-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; }
|
|
1584
|
+
.${key}-quote blockquote { max-width: 780px; font-size: 28px; line-height: 1.25; margin: 0 0 16px; }
|
|
1585
|
+
.${key}-cta { background: #111827; color: #ffffff; }
|
|
1586
|
+
.${key}-cta p { color: #d1d5db; }
|
|
1587
|
+
.${key}-cta .${key}-button { background: #ffffff; color: #111827; }
|
|
1588
|
+
`
|
|
1589
|
+
);
|
|
975
1590
|
await writeFile(path2.join(target, ".gitignore"), "node_modules\ndist\n*.tsbuildinfo\n");
|
|
1591
|
+
await writeFile(
|
|
1592
|
+
path2.join(target, "AGENTS.md"),
|
|
1593
|
+
`# Suda Theme Agent Guide
|
|
1594
|
+
|
|
1595
|
+
This directory is a Suda theme source project generated by \`suda theme init\`.
|
|
1596
|
+
It is a **standalone** npm package \u2014 it does not depend on the SudaCloud monorepo.
|
|
1597
|
+
|
|
1598
|
+
## What to edit
|
|
1599
|
+
|
|
1600
|
+
- Add page sections in \`src/sections.tsx\` and register them in \`SECTION_COMPONENTS\`.
|
|
1601
|
+
- Add starter pages in \`src/templates.ts\` to show realistic section combinations.
|
|
1602
|
+
- Keep shared site chrome in \`src/layout.tsx\`; \`PageOutlet\` is where page content renders.
|
|
1603
|
+
- Keep \`src/index.tsx\` exporting the source \`ThemeModule\`. The final artifact is produced by \`suda theme build\`.
|
|
1604
|
+
- All relative imports under \`src/\` MUST include the \`.js\` extension (NodeNext ESM resolution).
|
|
1605
|
+
|
|
1606
|
+
## Page content rules
|
|
1607
|
+
|
|
1608
|
+
- Agent page files passed to \`suda agent page validate/create\` contain \`content\` and optional \`zones\` only.
|
|
1609
|
+
- Every \`content[]\` item must use a section type from \`SECTION_COMPONENTS\`.
|
|
1610
|
+
- Every \`content[]\` item must include \`props.id\`.
|
|
1611
|
+
- Prefer editing section props over changing render code when generating pages.
|
|
1612
|
+
- Do not edit files under \`dist/\`; they are generated.
|
|
1613
|
+
|
|
1614
|
+
## Useful commands
|
|
1615
|
+
|
|
1616
|
+
\`\`\`bash
|
|
1617
|
+
pnpm install
|
|
1618
|
+
pnpm typecheck
|
|
1619
|
+
pnpm build # tsc + suda theme build --skip-theme-build
|
|
1620
|
+
pnpm validate
|
|
1621
|
+
pnpm preview
|
|
1622
|
+
suda agent theme describe local --theme-root .
|
|
1623
|
+
suda agent section schema --theme local --section Hero --theme-root .
|
|
1624
|
+
suda agent page validate --theme local --input ./page.json --theme-root .
|
|
1625
|
+
\`\`\`
|
|
1626
|
+
`
|
|
1627
|
+
);
|
|
1628
|
+
await writeFile(
|
|
1629
|
+
path2.join(target, "CLAUDE.md"),
|
|
1630
|
+
`# Claude Code
|
|
1631
|
+
|
|
1632
|
+
Read \`AGENTS.md\` first. It is the canonical guide for this Suda theme project.
|
|
1633
|
+
`
|
|
1634
|
+
);
|
|
976
1635
|
await writeFile(
|
|
977
1636
|
path2.join(target, "README.md"),
|
|
978
1637
|
`# ${key}
|
|
979
1638
|
|
|
980
|
-
A Suda theme scaffolded with \`suda theme init\`.
|
|
1639
|
+
A Suda theme scaffolded with \`suda theme init\`. This is a standalone
|
|
1640
|
+
package; it does not need to live inside the SudaCloud monorepo.
|
|
1641
|
+
|
|
1642
|
+
## Setup
|
|
1643
|
+
|
|
1644
|
+
\`\`\`bash
|
|
1645
|
+
pnpm install
|
|
1646
|
+
\`\`\`
|
|
981
1647
|
|
|
982
1648
|
## Develop
|
|
983
1649
|
|
|
984
1650
|
\`\`\`bash
|
|
985
|
-
|
|
986
|
-
suda theme
|
|
1651
|
+
pnpm typecheck # type-check sources
|
|
1652
|
+
pnpm build # tsc + suda theme build (server bundle + client runtime + manifest)
|
|
1653
|
+
pnpm dev # watch the browser runtime
|
|
1654
|
+
pnpm preview # build and preview the home starter page
|
|
1655
|
+
pnpm validate # validate the dist artifact
|
|
1656
|
+
\`\`\`
|
|
1657
|
+
|
|
1658
|
+
## Agent tooling
|
|
1659
|
+
|
|
1660
|
+
\`\`\`bash
|
|
1661
|
+
suda agent theme describe local --theme-root .
|
|
1662
|
+
suda agent page schema local --theme-root .
|
|
1663
|
+
suda agent section schema --theme local --section Hero --theme-root .
|
|
987
1664
|
\`\`\`
|
|
988
1665
|
|
|
989
1666
|
## Publish
|
|
990
1667
|
|
|
991
1668
|
\`\`\`bash
|
|
992
|
-
|
|
1669
|
+
pnpm build
|
|
993
1670
|
suda theme screenshot # optional: capture dist/preview/desktop.png
|
|
994
1671
|
suda theme publish
|
|
995
1672
|
\`\`\`
|
|
@@ -997,7 +1674,7 @@ suda theme publish
|
|
|
997
1674
|
);
|
|
998
1675
|
console.log(`created theme scaffold at ${target}`);
|
|
999
1676
|
}
|
|
1000
|
-
|
|
1677
|
+
function buildProgram() {
|
|
1001
1678
|
const program = new Command();
|
|
1002
1679
|
program.name("suda").description("Suda CLI for managing themes, sites, posts and AI tooling.").version("0.0.0");
|
|
1003
1680
|
const theme = program.command("theme").description("Manage Suda theme artifacts.");
|
|
@@ -1032,8 +1709,38 @@ async function main() {
|
|
|
1032
1709
|
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
1710
|
await publishTheme(resolveThemeRoot(options), options.skipBuild === true);
|
|
1034
1711
|
});
|
|
1712
|
+
const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
|
|
1713
|
+
const agentThemes = agent.command("themes").description("Manage agent-visible themes.");
|
|
1714
|
+
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) => {
|
|
1715
|
+
const themes = await listAgentThemes(options);
|
|
1716
|
+
printJson({ themes });
|
|
1717
|
+
});
|
|
1718
|
+
const agentTheme = agent.command("theme").description("Inspect a single theme.");
|
|
1719
|
+
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) => {
|
|
1720
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
1721
|
+
printJson(createAgentPageSchemaOutput(manifest, { includeExample: options.example === true }));
|
|
1722
|
+
});
|
|
1723
|
+
const agentPage = agent.command("page").description("AI-first page content tooling.");
|
|
1724
|
+
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) => {
|
|
1725
|
+
const manifest = await fetchAgentManifest(themeKey, options);
|
|
1726
|
+
printJson(createAgentPageSchemaOutput(manifest, { includeExample: options.example === true }));
|
|
1727
|
+
});
|
|
1728
|
+
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) => {
|
|
1729
|
+
await validateAgentPage(options.theme, options.input, options);
|
|
1730
|
+
});
|
|
1731
|
+
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) => {
|
|
1732
|
+
await createAgentPage(options);
|
|
1733
|
+
});
|
|
1734
|
+
const agentSection = agent.command("section").description("Inspect a single section.");
|
|
1735
|
+
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) => {
|
|
1736
|
+
const manifest = await fetchAgentManifest(options.theme, options);
|
|
1737
|
+
printJson(createSectionSchema(manifest, options.section));
|
|
1738
|
+
});
|
|
1739
|
+
program.command("mcp").description("Run the Suda local MCP server over stdio.").action(async () => {
|
|
1740
|
+
await startMcpServer();
|
|
1741
|
+
});
|
|
1035
1742
|
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.", "
|
|
1743
|
+
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
1744
|
await login(options.host);
|
|
1038
1745
|
});
|
|
1039
1746
|
authCmd.command("status").description("Check current authentication status.").action(async () => {
|
|
@@ -1043,12 +1750,14 @@ async function main() {
|
|
|
1043
1750
|
await logout();
|
|
1044
1751
|
});
|
|
1045
1752
|
program.showHelpAfterError();
|
|
1753
|
+
return program;
|
|
1754
|
+
}
|
|
1755
|
+
async function main() {
|
|
1756
|
+
const program = buildProgram();
|
|
1046
1757
|
const argv = process.argv.filter((arg, index) => index < 2 || arg !== "--");
|
|
1047
1758
|
await program.parseAsync(argv);
|
|
1048
1759
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
process.exitCode = 1;
|
|
1052
|
-
});
|
|
1760
|
+
|
|
1761
|
+
export { buildProgram, main };
|
|
1053
1762
|
//# sourceMappingURL=index.js.map
|
|
1054
1763
|
//# sourceMappingURL=index.js.map
|