@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/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 = "workspace.sudacloud.com") {
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 deadline = Date.now() + (expiresIn || 900) * 1e3;
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("\u2705 Successfully authorized!");
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 protocol = config.host.includes("localhost") || config.host.includes("127.0.0.1") ? "http" : "https";
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: "tsc -p tsconfig.json",
794
- typecheck: "tsc -p tsconfig.json --noEmit"
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
- '{\n "extends": "@suda/tsconfig/node.json",\n "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "jsx": "react-jsx", "rootDir": "src", "outDir": "dist" },\n "include": ["src"]\n}\n'
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
- <div>{description}</div>
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 SECTION_COMPONENTS = { Hero };
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(path2.join(target, "styles.css"), "\n");
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
- suda theme dev # rebuild the browser runtime on change
986
- suda theme preview # build and preview the home starter page
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
- suda theme build
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
- async function main() {
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.", "workspace.sudacloud.com").action(async (options) => {
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
- main().catch((error) => {
1050
- console.error(error instanceof Error ? error.message : error);
1051
- process.exitCode = 1;
1052
- });
1760
+
1761
+ export { buildProgram, main };
1053
1762
  //# sourceMappingURL=index.js.map
1054
1763
  //# sourceMappingURL=index.js.map