@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.d.ts +142 -1
- package/dist/index.js +428 -69
- 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';
|
|
@@ -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:
|
|
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, "
|
|
646
|
+
outDir: path2.join(root, "dist"),
|
|
574
647
|
sourcemap: false,
|
|
648
|
+
assetsInlineLimit: 0,
|
|
649
|
+
cssCodeSplit: false,
|
|
575
650
|
rollupOptions: {
|
|
576
|
-
input:
|
|
651
|
+
input: stylesheetEntry,
|
|
577
652
|
output: {
|
|
578
|
-
entryFileNames: "
|
|
653
|
+
entryFileNames: "chunks/theme-styles.js",
|
|
579
654
|
chunkFileNames: "chunks/[name]-[hash].js",
|
|
580
|
-
assetFileNames:
|
|
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
|
|
645
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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: (
|
|
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
|
|
1342
|
-
|
|
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:
|
|
1349
|
-
slug:
|
|
1350
|
-
themeKey:
|
|
1351
|
-
themeVersion:
|
|
1352
|
-
data:
|
|
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
|
-
|
|
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
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
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("[
|
|
1935
|
-
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);
|
|
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));
|