@sudajs/cli 0.7.1 → 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';
@@ -172,6 +173,8 @@ async function logout() {
172
173
 
173
174
  // src/index.ts
174
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-]*$/;
175
178
  var createPageDraftOutputSchema = z.discriminatedUnion("status", [
176
179
  z.object({
177
180
  status: z.literal("created").describe("The page draft was created."),
@@ -204,6 +207,35 @@ var updatePageDraftOutputSchema = z.discriminatedUnion("status", [
204
207
  )
205
208
  })
206
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
+ ]);
207
239
  var PROJECT_NAME_MIN_LENGTH = 1;
208
240
  var PROJECT_NAME_MAX_LENGTH = 80;
209
241
  var PROJECT_DESCRIPTION_MIN_LENGTH = 1;
@@ -559,6 +591,14 @@ function toViteModuleUrl(root, absolutePath) {
559
591
  function isThemeExternal(id) {
560
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/");
561
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
+ }
562
602
  async function buildViteTheme(root) {
563
603
  const viteConfig = await findViteConfig(root);
564
604
  const vite = await loadThemeVite(root);
@@ -579,17 +619,22 @@ async function buildViteTheme(root) {
579
619
  emptyOutDir: false,
580
620
  outDir: path2.join(root, "dist"),
581
621
  sourcemap: false,
622
+ assetsInlineLimit: 0,
582
623
  ssr: serverEntry,
583
624
  rollupOptions: {
584
625
  external: isThemeExternal,
585
626
  output: {
586
627
  entryFileNames: "index.js",
587
628
  chunkFileNames: "chunks/[name]-[hash].js",
588
- assetFileNames: "assets/[name]-[hash][extname]"
629
+ assetFileNames: viteAssetFileNames
589
630
  }
590
631
  }
591
632
  }
592
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");
593
638
  await vite.build({
594
639
  root,
595
640
  configFile: viteConfig,
@@ -598,18 +643,28 @@ async function buildViteTheme(root) {
598
643
  publicDir: false,
599
644
  build: {
600
645
  emptyOutDir: false,
601
- outDir: path2.join(root, ".suda-build", "styles"),
646
+ outDir: path2.join(root, "dist"),
602
647
  sourcemap: false,
648
+ assetsInlineLimit: 0,
649
+ cssCodeSplit: false,
603
650
  rollupOptions: {
604
- input: stylesEntry,
651
+ input: stylesheetEntry,
605
652
  output: {
606
- entryFileNames: "style-entry.js",
653
+ entryFileNames: "chunks/theme-styles.js",
607
654
  chunkFileNames: "chunks/[name]-[hash].js",
608
- assetFileNames: "assets/[name]-[hash][extname]"
655
+ assetFileNames: viteAssetFileNames
609
656
  }
610
657
  }
611
658
  }
612
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"));
613
668
  }
614
669
  async function buildClientRuntime(root, minify) {
615
670
  const entryPoint = await findClientEntry(root);
@@ -668,24 +723,12 @@ async function finalizeTheme(root) {
668
723
  return validateTheme(root);
669
724
  }
670
725
  async function finalizeStylesheet(root) {
671
- const builtStylesDir = path2.join(root, ".suda-build", "styles");
672
- const cssFiles = [
673
- ...await collectFiles(root, builtStylesDir),
674
- ...await collectFiles(root, path2.join(root, "dist"))
675
- ];
676
- const cssFile = cssFiles.find((file) => file.relativePath.endsWith(".css"));
677
- if (!cssFile) {
726
+ const expectedStylesheet = path2.join(root, "dist", "styles.css");
727
+ if (!await pathExists(expectedStylesheet)) {
678
728
  throw new Error(
679
- `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.`
680
730
  );
681
731
  }
682
- await mkdir(path2.join(root, "dist"), { recursive: true });
683
- await copyFile(cssFile.absolutePath, path2.join(root, "dist", "styles.css"));
684
- const assetsDir = path2.join(builtStylesDir, "assets");
685
- if (await pathExists(assetsDir)) {
686
- await mkdir(path2.join(root, "dist", "assets"), { recursive: true });
687
- await copyDirectory(assetsDir, path2.join(root, "dist", "assets"));
688
- }
689
732
  }
690
733
  async function copyDirectory(source, destination) {
691
734
  const children = await readdir(source, { withFileTypes: true });
@@ -784,8 +827,18 @@ function runFooterContractCheck(theme, renderer) {
784
827
  return issues;
785
828
  }
786
829
  var __testUtils = {
830
+ createRemotePageDraft,
787
831
  findAnchorTagByHref,
788
- toViteModuleUrl
832
+ loadThemeScopedRenderer,
833
+ initThemeWithKey,
834
+ performActivateTheme,
835
+ performConfirmedPageOperation,
836
+ renderDevStarterPageHtml,
837
+ slugifyThemeName,
838
+ validateThemeKey,
839
+ validateThemeName,
840
+ toViteModuleUrl,
841
+ updateRemotePageDraft
789
842
  };
790
843
  async function runThemeCheck(theme) {
791
844
  const result = checkThemeModule(theme.module);
@@ -835,7 +888,7 @@ function createSudaPreviewVitePlugin(root) {
835
888
  const url = new URL(request.url ?? "/", "http://localhost");
836
889
  const assetPrefixMatch = url.pathname.match(/^\/api\/themes\/[^/]+\/[^/]+\/assets\/(.+)$/);
837
890
  if (assetPrefixMatch?.[1]) {
838
- request.url = `/${assetPrefixMatch[1]}${url.search}`;
891
+ request.url = `/src/assets/${assetPrefixMatch[1]}${url.search}`;
839
892
  next();
840
893
  return;
841
894
  }
@@ -990,19 +1043,7 @@ function renderStarterPageHtml(theme, resolved, page, renderer) {
990
1043
  function renderDevStarterPageHtml(theme, page, renderer) {
991
1044
  const chrome = renderer.extractLayoutChrome(theme.module.defaultLayout);
992
1045
  const metadata = {
993
- resolveAssetUrl: (value) => {
994
- if (!value) {
995
- return value;
996
- }
997
- if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/")) {
998
- return value;
999
- }
1000
- if (value.startsWith("themes/")) {
1001
- const parts = value.split("/");
1002
- return `/${parts.slice(3).join("/")}`;
1003
- }
1004
- return value;
1005
- }
1046
+ resolveAssetUrl: createPreviewAssetResolver(renderer.resolveAssetPath)
1006
1047
  };
1007
1048
  const body = renderer.renderToString(
1008
1049
  renderer.createElement(renderer.ThemeRender, {
@@ -1363,24 +1404,115 @@ async function createAgentPage(options) {
1363
1404
  process.exitCode = 1;
1364
1405
  return;
1365
1406
  }
1366
- const pageData = createPageDataFromAgentContent(pageContent);
1367
1407
  const auth = await requireCliBaseUrl();
1368
- const response = await fetchJson(
1369
- `${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`,
1370
1425
  {
1371
1426
  method: "POST",
1372
1427
  token: auth.token,
1373
1428
  headers: { "Content-Type": "application/json" },
1374
1429
  body: JSON.stringify({
1375
- title: options.title,
1376
- slug: options.slug,
1377
- themeKey: manifest.manifest.key,
1378
- themeVersion: manifest.manifest.version,
1379
- data: pageData
1430
+ title: input.title,
1431
+ slug: input.slug,
1432
+ themeKey: input.themeKey,
1433
+ themeVersion: input.themeVersion,
1434
+ data: input.data
1380
1435
  })
1381
1436
  }
1382
1437
  );
1383
- 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
+ });
1384
1516
  }
1385
1517
  async function listAgentProjects() {
1386
1518
  const auth = await requireCliBaseUrl();
@@ -1594,21 +1726,14 @@ async function startMcpServer() {
1594
1726
  );
1595
1727
  }
1596
1728
  const auth = await requireCliBaseUrl();
1597
- const response = await fetchJson(
1598
- `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/pages`,
1599
- {
1600
- method: "POST",
1601
- token: auth.token,
1602
- headers: { "Content-Type": "application/json" },
1603
- body: JSON.stringify({
1604
- title,
1605
- slug,
1606
- themeKey: manifest.manifest.key,
1607
- themeVersion: manifest.manifest.version,
1608
- data: createPageDataFromAgentContent(data)
1609
- })
1610
- }
1611
- );
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
+ });
1612
1737
  return mcpStructured(
1613
1738
  "Created Suda page draft.",
1614
1739
  createPageDraftOutputSchema.parse({ status: "created", ...response })
@@ -1642,26 +1767,101 @@ async function startMcpServer() {
1642
1767
  );
1643
1768
  }
1644
1769
  const auth = await requireCliBaseUrl();
1645
- const response = await fetchJson(
1646
- `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/pages`,
1647
- {
1648
- method: "PATCH",
1649
- token: auth.token,
1650
- headers: { "Content-Type": "application/json" },
1651
- body: JSON.stringify({
1652
- slug,
1653
- themeKey: manifest.manifest.key,
1654
- themeVersion: manifest.manifest.version,
1655
- data: createPageDataFromAgentContent(data)
1656
- })
1657
- }
1658
- );
1770
+ const response = await updateRemotePageDraft(auth, {
1771
+ projectId,
1772
+ slug,
1773
+ themeKey: manifest.manifest.key,
1774
+ themeVersion: manifest.manifest.version,
1775
+ data
1776
+ });
1659
1777
  return mcpStructured(
1660
1778
  "Updated Suda page draft.",
1661
1779
  updatePageDraftOutputSchema.parse({ status: "updated", ...response })
1662
1780
  );
1663
1781
  }
1664
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
+ );
1665
1865
  await server.connect(new StdioServerTransport());
1666
1866
  }
1667
1867
  async function writeThemeArtifacts(theme) {
@@ -1972,8 +2172,92 @@ async function replaceTemplateTokens(filePath, replacements) {
1972
2172
  async function loadThemeTemplateFile(relativePath) {
1973
2173
  return readFile(path2.join(themeTemplateRoot, relativePath), "utf8");
1974
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
+ }
1975
2248
  async function initTheme(target) {
1976
- 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
+ }
1977
2261
  const cliPackageVersions = await readCliPackageVersions();
1978
2262
  await copyDirectory(themeTemplateRoot, target);
1979
2263
  const packageJson = JSON.parse(await loadThemeTemplateFile("package.json"));
@@ -2005,8 +2289,9 @@ function buildProgram() {
2005
2289
  const program = new Command();
2006
2290
  program.name("suda").description("Suda CLI for managing themes, sites, posts and AI tooling.").version("0.0.0");
2007
2291
  const theme = program.command("theme").description("Manage Suda theme artifacts.");
2008
- theme.command("init").description("Scaffold a new theme source directory.").argument("[dir]", "Theme directory to create.", "./theme").action(async (dir) => {
2009
- 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);
2010
2295
  });
2011
2296
  theme.command("validate").description("Validate a built theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2012
2297
  const result = await validateTheme(resolveThemeRoot(options));