@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.d.ts +142 -1
- package/dist/index.js +363 -78
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/templates/theme/.prettierrc +7 -0
- package/templates/theme/AGENTS.md +3 -0
- package/templates/theme/README.md +4 -0
- package/templates/theme/eslint.config.js +48 -0
- package/templates/theme/package.json +24 -13
- package/templates/theme/src/assets/suda-logo.svg +17 -0
- package/templates/theme/src/config.ts +5 -5
- package/templates/theme/src/layout.tsx +17 -12
- package/templates/theme/src/sections.tsx +37 -18
- package/templates/theme/src/styles.css +6 -0
- package/templates/theme/src/templates.ts +3 -0
- package/templates/theme/src/vite-env.d.ts +3 -0
- package/templates/theme/tsconfig.json +18 -13
- package/templates/theme/vite.config.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import fs, {
|
|
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,
|
|
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:
|
|
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, "
|
|
646
|
+
outDir: path2.join(root, "dist"),
|
|
602
647
|
sourcemap: false,
|
|
648
|
+
assetsInlineLimit: 0,
|
|
649
|
+
cssCodeSplit: false,
|
|
603
650
|
rollupOptions: {
|
|
604
|
-
input:
|
|
651
|
+
input: stylesheetEntry,
|
|
605
652
|
output: {
|
|
606
|
-
entryFileNames: "
|
|
653
|
+
entryFileNames: "chunks/theme-styles.js",
|
|
607
654
|
chunkFileNames: "chunks/[name]-[hash].js",
|
|
608
|
-
assetFileNames:
|
|
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
|
|
672
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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: (
|
|
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
|
|
1369
|
-
|
|
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:
|
|
1376
|
-
slug:
|
|
1377
|
-
themeKey:
|
|
1378
|
-
themeVersion:
|
|
1379
|
-
data:
|
|
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
|
-
|
|
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
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
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("[
|
|
2009
|
-
await
|
|
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));
|