@sudajs/cli 0.7.0 → 0.7.2

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
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'crypto';
3
- import fs, { mkdir, copyFile, readdir, writeFile, stat, readFile, rm } from 'fs/promises';
3
+ import fs, { writeFile, stat, readFile, readdir, mkdir, copyFile, rm } from 'fs/promises';
4
4
  import { createRequire } from 'module';
5
5
  import path2 from 'path';
6
+ import { createInterface } from 'readline/promises';
6
7
  import { fileURLToPath, pathToFileURL } from 'url';
7
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
- import { createThemeAgentManifest, createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
10
+ import { createThemeAgentManifest, createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, validateAgentPageContentWithManifest, agentValidationResultSchema, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
10
11
  import { themeManifestSchema, FileSystemThemeRegistry } from '@sudajs/theme-engine/server';
11
12
  import { Command } from 'commander';
12
13
  import { build } from 'esbuild';
@@ -171,6 +172,9 @@ async function logout() {
171
172
  }
172
173
 
173
174
  // src/index.ts
175
+ var __dirname$1 = path2.dirname(fileURLToPath(import.meta.url));
176
+ var THEME_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
177
+ var THEME_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
174
178
  var createPageDraftOutputSchema = z.discriminatedUnion("status", [
175
179
  z.object({
176
180
  status: z.literal("created").describe("The page draft was created."),
@@ -187,6 +191,51 @@ var createPageDraftOutputSchema = z.discriminatedUnion("status", [
187
191
  )
188
192
  })
189
193
  ]);
194
+ var updatePageDraftOutputSchema = z.discriminatedUnion("status", [
195
+ z.object({
196
+ status: z.literal("updated").describe("The page draft was updated."),
197
+ pageId: z.string().describe("Existing page id.")
198
+ }),
199
+ z.object({
200
+ status: z.literal("invalid").describe("The page content failed validation."),
201
+ valid: z.literal(false),
202
+ issues: z.array(
203
+ z.object({
204
+ path: z.string(),
205
+ message: z.string()
206
+ })
207
+ )
208
+ })
209
+ ]);
210
+ var destructivePageOperationOutputSchema = z.discriminatedUnion("status", [
211
+ z.object({
212
+ status: z.literal("needs_confirmation"),
213
+ projectId: z.string(),
214
+ slug: z.string(),
215
+ impact: z.string()
216
+ }),
217
+ z.object({
218
+ status: z.enum(["deleted", "published"]),
219
+ projectId: z.string(),
220
+ slug: z.string(),
221
+ pageId: z.string()
222
+ })
223
+ ]);
224
+ var activateThemeOutputSchema = z.discriminatedUnion("status", [
225
+ z.object({
226
+ status: z.literal("needs_confirmation"),
227
+ projectId: z.string(),
228
+ themeKey: z.string(),
229
+ themeVersion: z.string().nullable(),
230
+ impact: z.string()
231
+ }),
232
+ z.object({
233
+ status: z.literal("activated"),
234
+ projectId: z.string(),
235
+ themeKey: z.string(),
236
+ themeVersion: z.string().nullable()
237
+ })
238
+ ]);
190
239
  var PROJECT_NAME_MIN_LENGTH = 1;
191
240
  var PROJECT_NAME_MAX_LENGTH = 80;
192
241
  var PROJECT_DESCRIPTION_MIN_LENGTH = 1;
@@ -336,6 +385,17 @@ export const { createRoot, hydrateRoot, version } = ReactDOMClient;
336
385
  }
337
386
  };
338
387
  }
388
+ function createThemeEngineRuntimeAliasPlugin() {
389
+ return {
390
+ name: "suda-theme-engine-runtime-alias",
391
+ setup(build) {
392
+ const runtimeEntry = path2.resolve(__dirname$1, "../../theme-engine/src/runtime.ts");
393
+ build.onResolve({ filter: /^@sudajs\/theme-engine\/runtime$/ }, () => ({
394
+ path: runtimeEntry
395
+ }));
396
+ }
397
+ };
398
+ }
339
399
  function safeObjectKeyPart(value) {
340
400
  return value.replace(/[^a-zA-Z0-9._-]/g, "-");
341
401
  }
@@ -531,6 +591,14 @@ function toViteModuleUrl(root, absolutePath) {
531
591
  function isThemeExternal(id) {
532
592
  return id === "react" || id.startsWith("react/") || id === "react-dom" || id.startsWith("react-dom/") || id === "@puckeditor/core" || id.startsWith("@puckeditor/core/") || id === "@sudajs/theme-engine" || id.startsWith("@sudajs/theme-engine/");
533
593
  }
594
+ function viteAssetFileNames(assetInfo) {
595
+ const names = assetInfo.names ?? [];
596
+ const isCss = names.some((name) => name.endsWith(".css"));
597
+ if (isCss) {
598
+ return "styles.css";
599
+ }
600
+ return "assets/[name]-[hash][extname]";
601
+ }
534
602
  async function buildViteTheme(root) {
535
603
  const viteConfig = await findViteConfig(root);
536
604
  const vite = await loadThemeVite(root);
@@ -551,17 +619,22 @@ async function buildViteTheme(root) {
551
619
  emptyOutDir: false,
552
620
  outDir: path2.join(root, "dist"),
553
621
  sourcemap: false,
622
+ assetsInlineLimit: 0,
554
623
  ssr: serverEntry,
555
624
  rollupOptions: {
556
625
  external: isThemeExternal,
557
626
  output: {
558
627
  entryFileNames: "index.js",
559
628
  chunkFileNames: "chunks/[name]-[hash].js",
560
- assetFileNames: "assets/[name]-[hash][extname]"
629
+ assetFileNames: viteAssetFileNames
561
630
  }
562
631
  }
563
632
  }
564
633
  });
634
+ await copyThemeSourceAssets(root);
635
+ const stylesheetEntry = path2.join(root, ".suda-build", "styles-entry.ts");
636
+ await mkdir(path2.dirname(stylesheetEntry), { recursive: true });
637
+ await writeFile(stylesheetEntry, 'import "../src/styles.css";\n', "utf8");
565
638
  await vite.build({
566
639
  root,
567
640
  configFile: viteConfig,
@@ -570,18 +643,28 @@ async function buildViteTheme(root) {
570
643
  publicDir: false,
571
644
  build: {
572
645
  emptyOutDir: false,
573
- outDir: path2.join(root, ".suda-build", "styles"),
646
+ outDir: path2.join(root, "dist"),
574
647
  sourcemap: false,
648
+ assetsInlineLimit: 0,
649
+ cssCodeSplit: false,
575
650
  rollupOptions: {
576
- input: stylesEntry,
651
+ input: stylesheetEntry,
577
652
  output: {
578
- entryFileNames: "style-entry.js",
653
+ entryFileNames: "chunks/theme-styles.js",
579
654
  chunkFileNames: "chunks/[name]-[hash].js",
580
- assetFileNames: "assets/[name]-[hash][extname]"
655
+ assetFileNames: viteAssetFileNames
581
656
  }
582
657
  }
583
658
  }
584
659
  });
660
+ await rm(path2.join(root, "dist", "chunks", "theme-styles.js"), { force: true });
661
+ }
662
+ async function copyThemeSourceAssets(root) {
663
+ const sourceAssets = path2.join(root, "src", "assets");
664
+ if (!await pathExists(sourceAssets)) {
665
+ return;
666
+ }
667
+ await copyDirectory(sourceAssets, path2.join(root, "dist", "assets"));
585
668
  }
586
669
  async function buildClientRuntime(root, minify) {
587
670
  const entryPoint = await findClientEntry(root);
@@ -590,6 +673,7 @@ async function buildClientRuntime(root, minify) {
590
673
  }
591
674
  await mkdir(path2.join(root, "dist"), { recursive: true });
592
675
  const hostReactShimPlugin = createHostReactShimPlugin();
676
+ const themeEngineRuntimeAliasPlugin = createThemeEngineRuntimeAliasPlugin();
593
677
  await build({
594
678
  bundle: true,
595
679
  entryPoints: [entryPoint],
@@ -599,16 +683,14 @@ async function buildClientRuntime(root, minify) {
599
683
  "react-dom",
600
684
  "react-dom/*",
601
685
  "@puckeditor/core",
602
- "@puckeditor/core/*",
603
- "@sudajs/theme-engine",
604
- "@sudajs/theme-engine/*"
686
+ "@puckeditor/core/*"
605
687
  ],
606
688
  format: "esm",
607
689
  jsx: "automatic",
608
690
  minify,
609
691
  outfile: path2.join(root, "dist", "runtime.client.js"),
610
692
  platform: "browser",
611
- plugins: [hostReactShimPlugin],
693
+ plugins: [hostReactShimPlugin, themeEngineRuntimeAliasPlugin],
612
694
  sourcemap: false,
613
695
  target: "es2022",
614
696
  treeShaking: true
@@ -641,24 +723,12 @@ async function finalizeTheme(root) {
641
723
  return validateTheme(root);
642
724
  }
643
725
  async function finalizeStylesheet(root) {
644
- const builtStylesDir = path2.join(root, ".suda-build", "styles");
645
- const cssFiles = [
646
- ...await collectFiles(root, builtStylesDir),
647
- ...await collectFiles(root, path2.join(root, "dist"))
648
- ];
649
- const cssFile = cssFiles.find((file) => file.relativePath.endsWith(".css"));
650
- if (!cssFile) {
726
+ const expectedStylesheet = path2.join(root, "dist", "styles.css");
727
+ if (!await pathExists(expectedStylesheet)) {
651
728
  throw new Error(
652
- `Vite did not emit a stylesheet from src/styles.css. Check ${path2.join(root, "vite.config.ts")} and Tailwind/PostCSS configuration.`
729
+ `Vite did not emit ${expectedStylesheet} from src/styles.css. Check ${path2.join(root, "vite.config.ts")} and Tailwind/PostCSS configuration.`
653
730
  );
654
731
  }
655
- await mkdir(path2.join(root, "dist"), { recursive: true });
656
- await copyFile(cssFile.absolutePath, path2.join(root, "dist", "styles.css"));
657
- const assetsDir = path2.join(builtStylesDir, "assets");
658
- if (await pathExists(assetsDir)) {
659
- await mkdir(path2.join(root, "dist", "assets"), { recursive: true });
660
- await copyDirectory(assetsDir, path2.join(root, "dist", "assets"));
661
- }
662
732
  }
663
733
  async function copyDirectory(source, destination) {
664
734
  const children = await readdir(source, { withFileTypes: true });
@@ -757,8 +827,18 @@ function runFooterContractCheck(theme, renderer) {
757
827
  return issues;
758
828
  }
759
829
  var __testUtils = {
830
+ createRemotePageDraft,
760
831
  findAnchorTagByHref,
761
- toViteModuleUrl
832
+ loadThemeScopedRenderer,
833
+ initThemeWithKey,
834
+ performActivateTheme,
835
+ performConfirmedPageOperation,
836
+ renderDevStarterPageHtml,
837
+ slugifyThemeName,
838
+ validateThemeKey,
839
+ validateThemeName,
840
+ toViteModuleUrl,
841
+ updateRemotePageDraft
762
842
  };
763
843
  async function runThemeCheck(theme) {
764
844
  const result = checkThemeModule(theme.module);
@@ -808,7 +888,7 @@ function createSudaPreviewVitePlugin(root) {
808
888
  const url = new URL(request.url ?? "/", "http://localhost");
809
889
  const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
810
890
  if (assetPrefixMatch?.[1]) {
811
- request.url = `/${assetPrefixMatch[1]}${url.search}`;
891
+ request.url = `/src/assets/${assetPrefixMatch[1]}${url.search}`;
812
892
  next();
813
893
  return;
814
894
  }
@@ -963,19 +1043,7 @@ function renderStarterPageHtml(theme, resolved, page, renderer) {
963
1043
  function renderDevStarterPageHtml(theme, page, renderer) {
964
1044
  const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
965
1045
  const metadata = {
966
- resolveAssetUrl: (value) => {
967
- if (!value) {
968
- return value;
969
- }
970
- if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/")) {
971
- return value;
972
- }
973
- if (value.startsWith("themes/")) {
974
- const parts = value.split("/");
975
- return `/${parts.slice(3).join("/")}`;
976
- }
977
- return value;
978
- }
1046
+ resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath)
979
1047
  };
980
1048
  const body = renderer.renderToString(
981
1049
  renderer.createElement(renderer.ThemeRender, {
@@ -1336,24 +1404,115 @@ async function createAgentPage(options) {
1336
1404
  process.exitCode = 1;
1337
1405
  return;
1338
1406
  }
1339
- const pageData = createPageDataFromAgentContent(pageContent);
1340
1407
  const auth = await requireCliBaseUrl();
1341
- const response = await fetchJson(
1342
- `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(options.projectId)}/pages`,
1408
+ const response = await createRemotePageDraft(
1409
+ auth,
1410
+ {
1411
+ projectId: options.projectId,
1412
+ title: options.title,
1413
+ slug: options.slug,
1414
+ themeKey: manifest.manifest.key,
1415
+ themeVersion: manifest.manifest.version,
1416
+ data: pageContent
1417
+ },
1418
+ fetchJson
1419
+ );
1420
+ printJson({ success: true, ...response });
1421
+ }
1422
+ async function createRemotePageDraft(auth, input, fetcher = fetchJson) {
1423
+ return fetcher(
1424
+ `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/pages`,
1343
1425
  {
1344
1426
  method: "POST",
1345
1427
  token: auth.token,
1346
1428
  headers: { "Content-Type": "application/json" },
1347
1429
  body: JSON.stringify({
1348
- title: options.title,
1349
- slug: options.slug,
1350
- themeKey: manifest.manifest.key,
1351
- themeVersion: manifest.manifest.version,
1352
- data: pageData
1430
+ title: input.title,
1431
+ slug: input.slug,
1432
+ themeKey: input.themeKey,
1433
+ themeVersion: input.themeVersion,
1434
+ data: input.data
1353
1435
  })
1354
1436
  }
1355
1437
  );
1356
- printJson({ success: true, ...response });
1438
+ }
1439
+ async function updateRemotePageDraft(auth, input, fetcher = fetchJson) {
1440
+ return fetcher(
1441
+ `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/pages`,
1442
+ {
1443
+ method: "PATCH",
1444
+ token: auth.token,
1445
+ headers: { "Content-Type": "application/json" },
1446
+ body: JSON.stringify({
1447
+ slug: input.slug,
1448
+ themeKey: input.themeKey,
1449
+ themeVersion: input.themeVersion,
1450
+ data: input.data
1451
+ })
1452
+ }
1453
+ );
1454
+ }
1455
+ async function performConfirmedPageOperation(auth, input, fetcher = fetchJson) {
1456
+ const impact = input.operation === "delete" ? `This will delete page "${input.slug}" from project "${input.projectId}". Deleted pages are removed from the workspace and public site.` : `This will publish the current saved draft for page "${input.slug}" in project "${input.projectId}" and make it visible on the public site.`;
1457
+ if (input.confirm !== true) {
1458
+ return {
1459
+ status: "needs_confirmation",
1460
+ projectId: input.projectId,
1461
+ slug: input.slug,
1462
+ impact
1463
+ };
1464
+ }
1465
+ const endpoint = input.operation === "delete" ? `/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/pages` : `/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/pages/publish`;
1466
+ const response = await fetcher(`${auth.baseUrl}${endpoint}`, {
1467
+ method: input.operation === "delete" ? "DELETE" : "POST",
1468
+ token: auth.token,
1469
+ headers: { "Content-Type": "application/json" },
1470
+ body: JSON.stringify({
1471
+ slug: input.slug,
1472
+ confirm: true
1473
+ })
1474
+ });
1475
+ return {
1476
+ status: input.operation === "delete" ? "deleted" : "published",
1477
+ projectId: input.projectId,
1478
+ slug: response.slug ?? input.slug,
1479
+ pageId: response.pageId
1480
+ };
1481
+ }
1482
+ async function performActivateTheme(auth, input, fetcher = fetchJson) {
1483
+ const themeRef = input.themeVersion ? `${input.themeKey}@${input.themeVersion}` : input.themeKey;
1484
+ const impact = `This will activate theme "${themeRef}" for project "${input.projectId}". Public pages will render with this theme after activation.`;
1485
+ if (input.confirm !== true) {
1486
+ return {
1487
+ status: "needs_confirmation",
1488
+ projectId: input.projectId,
1489
+ themeKey: input.themeKey,
1490
+ themeVersion: input.themeVersion ?? null,
1491
+ impact
1492
+ };
1493
+ }
1494
+ const body = {
1495
+ themeKey: input.themeKey,
1496
+ confirm: true
1497
+ };
1498
+ if (input.themeVersion !== void 0) {
1499
+ body.themeVersion = input.themeVersion;
1500
+ }
1501
+ const response = await fetcher(
1502
+ `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/theme/activate`,
1503
+ {
1504
+ method: "POST",
1505
+ token: auth.token,
1506
+ headers: { "Content-Type": "application/json" },
1507
+ body: JSON.stringify(body)
1508
+ }
1509
+ );
1510
+ return activateThemeOutputSchema.parse({
1511
+ status: "activated",
1512
+ projectId: response.projectId,
1513
+ themeKey: response.themeKey,
1514
+ themeVersion: response.themeVersion
1515
+ });
1357
1516
  }
1358
1517
  async function listAgentProjects() {
1359
1518
  const auth = await requireCliBaseUrl();
@@ -1542,7 +1701,7 @@ async function startMcpServer() {
1542
1701
  server.registerTool(
1543
1702
  "create_page_draft",
1544
1703
  {
1545
- description: "Create a workspace page draft from AI-generated Suda page content.",
1704
+ description: "Create a new workspace page draft from AI-generated Suda page content.",
1546
1705
  inputSchema: {
1547
1706
  projectId: z.string(),
1548
1707
  title: z.string(),
@@ -1567,27 +1726,142 @@ async function startMcpServer() {
1567
1726
  );
1568
1727
  }
1569
1728
  const auth = await requireCliBaseUrl();
1570
- const response = await fetchJson(
1571
- `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/pages`,
1572
- {
1573
- method: "POST",
1574
- token: auth.token,
1575
- headers: { "Content-Type": "application/json" },
1576
- body: JSON.stringify({
1577
- title,
1578
- slug,
1579
- themeKey: manifest.manifest.key,
1580
- themeVersion: manifest.manifest.version,
1581
- data: createPageDataFromAgentContent(data)
1582
- })
1583
- }
1584
- );
1729
+ const response = await createRemotePageDraft(auth, {
1730
+ projectId,
1731
+ title,
1732
+ slug,
1733
+ themeKey: manifest.manifest.key,
1734
+ themeVersion: manifest.manifest.version,
1735
+ data
1736
+ });
1585
1737
  return mcpStructured(
1586
1738
  "Created Suda page draft.",
1587
1739
  createPageDraftOutputSchema.parse({ status: "created", ...response })
1588
1740
  );
1589
1741
  }
1590
1742
  );
1743
+ server.registerTool(
1744
+ "update_page_draft",
1745
+ {
1746
+ description: "Update an existing workspace page draft by slug from AI-generated Suda page content.",
1747
+ inputSchema: {
1748
+ projectId: z.string(),
1749
+ slug: z.string(),
1750
+ theme: z.string(),
1751
+ data: z.unknown(),
1752
+ version: z.string().optional()
1753
+ },
1754
+ outputSchema: updatePageDraftOutputSchema
1755
+ },
1756
+ async ({ projectId, slug, theme, data, version }) => {
1757
+ const manifest = await fetchAgentManifest(theme, { version, projectId });
1758
+ const validation = validateAgentPageContentWithManifest(data, manifest);
1759
+ if (!validation.valid) {
1760
+ return mcpStructured(
1761
+ "Suda page content validation failed. Fix issues before updating a draft.",
1762
+ updatePageDraftOutputSchema.parse({
1763
+ status: "invalid",
1764
+ valid: false,
1765
+ issues: validation.issues
1766
+ })
1767
+ );
1768
+ }
1769
+ const auth = await requireCliBaseUrl();
1770
+ const response = await updateRemotePageDraft(auth, {
1771
+ projectId,
1772
+ slug,
1773
+ themeKey: manifest.manifest.key,
1774
+ themeVersion: manifest.manifest.version,
1775
+ data
1776
+ });
1777
+ return mcpStructured(
1778
+ "Updated Suda page draft.",
1779
+ updatePageDraftOutputSchema.parse({ status: "updated", ...response })
1780
+ );
1781
+ }
1782
+ );
1783
+ server.registerTool(
1784
+ "delete_page",
1785
+ {
1786
+ description: "Delete a Suda page by slug. This is destructive: first call without confirm to get the exact project, slug, and impact; only call again with confirm: true after the user explicitly agrees.",
1787
+ inputSchema: {
1788
+ projectId: z.string(),
1789
+ slug: z.string(),
1790
+ confirm: z.boolean().optional()
1791
+ },
1792
+ outputSchema: destructivePageOperationOutputSchema
1793
+ },
1794
+ async ({ projectId, slug, confirm }) => {
1795
+ const auth = await requireCliBaseUrl();
1796
+ const structuredContent = destructivePageOperationOutputSchema.parse(
1797
+ await performConfirmedPageOperation(auth, {
1798
+ operation: "delete",
1799
+ projectId,
1800
+ slug,
1801
+ confirm
1802
+ })
1803
+ );
1804
+ return mcpStructured(
1805
+ structuredContent.status === "needs_confirmation" ? "Deleting this Suda page requires explicit confirmation." : "Deleted Suda page.",
1806
+ structuredContent
1807
+ );
1808
+ }
1809
+ );
1810
+ server.registerTool(
1811
+ "publish_page",
1812
+ {
1813
+ description: "Publish the current saved draft for a Suda page by slug. This affects the public site: first call without confirm to get the exact project, slug, and impact; only call again with confirm: true after the user explicitly agrees.",
1814
+ inputSchema: {
1815
+ projectId: z.string(),
1816
+ slug: z.string(),
1817
+ confirm: z.boolean().optional()
1818
+ },
1819
+ outputSchema: destructivePageOperationOutputSchema
1820
+ },
1821
+ async ({ projectId, slug, confirm }) => {
1822
+ const auth = await requireCliBaseUrl();
1823
+ const structuredContent = destructivePageOperationOutputSchema.parse(
1824
+ await performConfirmedPageOperation(auth, {
1825
+ operation: "publish",
1826
+ projectId,
1827
+ slug,
1828
+ confirm
1829
+ })
1830
+ );
1831
+ return mcpStructured(
1832
+ structuredContent.status === "needs_confirmation" ? "Publishing this Suda page requires explicit confirmation." : "Published Suda page.",
1833
+ structuredContent
1834
+ );
1835
+ }
1836
+ );
1837
+ server.registerTool(
1838
+ "activate_theme",
1839
+ {
1840
+ description: "Activate a theme for a Suda project. This affects how public pages render and may seed starter content on first activation: first call without confirm to get the exact project, theme, and impact; only call again with confirm: true after the user explicitly agrees.",
1841
+ inputSchema: {
1842
+ projectId: z.string(),
1843
+ themeKey: z.string(),
1844
+ themeVersion: z.string().optional(),
1845
+ confirm: z.boolean().optional()
1846
+ },
1847
+ outputSchema: activateThemeOutputSchema
1848
+ },
1849
+ async ({ projectId, themeKey, themeVersion, confirm }) => {
1850
+ const auth = await requireCliBaseUrl();
1851
+ const structuredContent = activateThemeOutputSchema.parse(
1852
+ await performActivateTheme(auth, {
1853
+ projectId,
1854
+ themeKey,
1855
+ themeVersion,
1856
+ confirm
1857
+ })
1858
+ );
1859
+ return mcpStructured(
1860
+ structuredContent.status === "needs_confirmation" ? "Activating this Suda theme requires explicit confirmation." : "Activated Suda theme.",
1861
+ structuredContent
1862
+ );
1863
+ }
1864
+ );
1591
1865
  await server.connect(new StdioServerTransport());
1592
1866
  }
1593
1867
  async function writeThemeArtifacts(theme) {
@@ -1898,8 +2172,92 @@ async function replaceTemplateTokens(filePath, replacements) {
1898
2172
  async function loadThemeTemplateFile(relativePath) {
1899
2173
  return readFile(path2.join(themeTemplateRoot, relativePath), "utf8");
1900
2174
  }
2175
+ function validateThemeName(name) {
2176
+ if (name.length === 0) {
2177
+ return "Theme name is required.";
2178
+ }
2179
+ if (!THEME_NAME_PATTERN.test(name)) {
2180
+ return "Theme name may only contain English letters, numbers, hyphens, and underscores.";
2181
+ }
2182
+ return null;
2183
+ }
2184
+ function validateThemeKey(key) {
2185
+ if (key.length === 0) {
2186
+ return "Theme key is required.";
2187
+ }
2188
+ if (!THEME_KEY_PATTERN.test(key)) {
2189
+ return "Theme key must be kebab-case: lowercase letters, numbers, and hyphens, starting with a letter or number.";
2190
+ }
2191
+ return null;
2192
+ }
2193
+ function slugifyThemeName(name) {
2194
+ return name.replaceAll("_", "-").toLowerCase();
2195
+ }
2196
+ function createThemeInitPrompt(nameProvided) {
2197
+ if (!process.stdin.isTTY) {
2198
+ throw new Error(
2199
+ nameProvided ? "Theme key confirmation is required. Re-run `suda theme init <name>` in an interactive terminal." : "Theme name is required. Usage: suda theme init <name>"
2200
+ );
2201
+ }
2202
+ return createInterface({
2203
+ input: process.stdin,
2204
+ output: process.stdout
2205
+ });
2206
+ }
2207
+ async function promptThemeName(rl) {
2208
+ while (true) {
2209
+ const answer = (await rl.question("Theme name: ")).trim();
2210
+ const error = validateThemeName(answer);
2211
+ if (error === null) {
2212
+ return answer;
2213
+ }
2214
+ console.error(error);
2215
+ }
2216
+ }
2217
+ async function promptThemeKey(rl, defaultKey) {
2218
+ while (true) {
2219
+ const answer = (await rl.question(`Theme key (${defaultKey}): `)).trim();
2220
+ const key = answer.length > 0 ? answer : defaultKey;
2221
+ const error = validateThemeKey(key);
2222
+ if (error === null) {
2223
+ return key;
2224
+ }
2225
+ console.error(error);
2226
+ }
2227
+ }
2228
+ async function resolveThemeInitInput(name) {
2229
+ const providedName = name?.trim();
2230
+ if (providedName !== void 0) {
2231
+ const nameError = validateThemeName(providedName);
2232
+ if (nameError !== null) {
2233
+ throw new Error(nameError);
2234
+ }
2235
+ }
2236
+ const rl = createThemeInitPrompt(providedName !== void 0);
2237
+ try {
2238
+ const themeName = providedName ?? await promptThemeName(rl);
2239
+ const defaultKey = slugifyThemeName(themeName);
2240
+ return {
2241
+ name: themeName,
2242
+ key: await promptThemeKey(rl, defaultKey)
2243
+ };
2244
+ } finally {
2245
+ rl.close();
2246
+ }
2247
+ }
1901
2248
  async function initTheme(target) {
1902
- const key = path2.basename(target).replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
2249
+ await initThemeWithKey(target, slugifyThemeName(path2.basename(target)));
2250
+ }
2251
+ async function initThemeWithKey(target, key) {
2252
+ const themeName = path2.basename(target);
2253
+ const nameError = validateThemeName(themeName);
2254
+ if (nameError !== null) {
2255
+ throw new Error(nameError);
2256
+ }
2257
+ const keyError = validateThemeKey(key);
2258
+ if (keyError !== null) {
2259
+ throw new Error(keyError);
2260
+ }
1903
2261
  const cliPackageVersions = await readCliPackageVersions();
1904
2262
  await copyDirectory(themeTemplateRoot, target);
1905
2263
  const packageJson = JSON.parse(await loadThemeTemplateFile("package.json"));
@@ -1931,8 +2289,9 @@ function buildProgram() {
1931
2289
  const program = new Command();
1932
2290
  program.name("suda").description("Suda CLI for managing themes, sites, posts and AI tooling.").version("0.0.0");
1933
2291
  const theme = program.command("theme").description("Manage Suda theme artifacts.");
1934
- theme.command("init").description("Scaffold a new theme source directory.").argument("[dir]", "Theme directory to create.", "./theme").action(async (dir) => {
1935
- await initTheme(path2.resolve(dir));
2292
+ theme.command("init").description("Scaffold a new theme source directory.").argument("[name]", "Theme name and directory to create. Use English letters, numbers, hyphens, or underscores.").action(async (name) => {
2293
+ const input = await resolveThemeInitInput(name);
2294
+ await initThemeWithKey(path2.resolve(input.name), input.key);
1936
2295
  });
1937
2296
  theme.command("validate").description("Validate a built theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
1938
2297
  const result = await validateTheme(resolveThemeRoot(options));