@sudajs/cli 0.2.1 → 0.4.0
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 +311 -39
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import path2 from 'path';
|
|
5
|
-
import { pathToFileURL } from 'url';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
-
import { createAgentPageSchemaOutput, createThemeAgentManifest, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
9
|
+
import { createAgentPageSchemaOutput, checkThemeModule, formatThemeCheckResult, createThemeAgentManifest, validateAgentPageContentWithManifest, agentValidationResultSchema, createPageDataFromAgentContent, findAgentSection, createAgentComponentSchemaOutput, agentPageSchemaOutputSchema, agentComponentOutputSchema } from '@sudajs/theme-engine';
|
|
9
10
|
import { extractLayoutChrome, ThemeRender } from '@sudajs/theme-engine/server';
|
|
10
11
|
import { Command } from 'commander';
|
|
11
12
|
import { build, context } from 'esbuild';
|
|
@@ -42,9 +43,42 @@ async function clearAuthConfig() {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
function protocolForHost(host) {
|
|
47
|
+
return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
48
|
+
}
|
|
49
|
+
var CliAuthExpiredError = class extends Error {
|
|
50
|
+
code;
|
|
51
|
+
constructor(code, message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "CliAuthExpiredError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
async function cliAuthFetch(pathname, init = {}) {
|
|
58
|
+
const config = await readAuthConfig();
|
|
59
|
+
if (!config) {
|
|
60
|
+
throw new CliAuthExpiredError(
|
|
61
|
+
"unauthorized",
|
|
62
|
+
"Not logged in. Run `suda auth login` to authenticate first."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
|
|
66
|
+
const headers = new Headers(init.headers);
|
|
67
|
+
headers.set("Authorization", `Bearer ${config.sessionToken}`);
|
|
68
|
+
const res = await fetch(`${baseUrl}${pathname}`, { ...init, headers });
|
|
69
|
+
if (res.status === 401) {
|
|
70
|
+
const body = await res.clone().json().catch(() => ({}));
|
|
71
|
+
const code = ["token_invalid", "token_revoked", "token_expired"].includes(body.error ?? "") ? body.error : "unauthorized";
|
|
72
|
+
await clearAuthConfig();
|
|
73
|
+
throw new CliAuthExpiredError(
|
|
74
|
+
code,
|
|
75
|
+
body.message ?? "Authentication is no longer valid. Run `suda auth login` to authenticate again."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return res;
|
|
79
|
+
}
|
|
45
80
|
async function login(host = "app.sudayun.cn") {
|
|
46
|
-
const
|
|
47
|
-
const baseUrl = `${protocol}://${host}`;
|
|
81
|
+
const baseUrl = `${protocolForHost(host)}://${host}`;
|
|
48
82
|
console.log(`Requesting device authorization from ${baseUrl}...`);
|
|
49
83
|
const deviceRes = await fetch(`${baseUrl}/api/cli-auth/device`, {
|
|
50
84
|
method: "POST"
|
|
@@ -96,30 +130,26 @@ async function status() {
|
|
|
96
130
|
console.log("Not logged in. Run `suda auth login` to authenticate.");
|
|
97
131
|
return;
|
|
98
132
|
}
|
|
99
|
-
const protocol = config.host.includes("localhost") || config.host.includes("127.0.0.1") ? "http" : "https";
|
|
100
133
|
let res;
|
|
101
134
|
try {
|
|
102
|
-
res = await
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
res = await cliAuthFetch("/api/cli-auth/whoami");
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof CliAuthExpiredError) {
|
|
138
|
+
console.log(err.message);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
108
141
|
console.error(`Failed to connect to ${config.host}.`);
|
|
109
142
|
return;
|
|
110
143
|
}
|
|
111
144
|
if (!res.ok) {
|
|
112
|
-
console.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const session = await res.json();
|
|
117
|
-
if (!session?.user) {
|
|
118
|
-
console.log("Session expired or invalid. Please run `suda auth login` again.");
|
|
119
|
-
await clearAuthConfig();
|
|
145
|
+
console.error(
|
|
146
|
+
`Failed to verify session (${res.status} ${res.statusText}). Try again later.`
|
|
147
|
+
);
|
|
120
148
|
return;
|
|
121
149
|
}
|
|
122
|
-
|
|
150
|
+
const data = await res.json();
|
|
151
|
+
const label = data.user?.email ?? data.user?.name ?? "your account";
|
|
152
|
+
console.log(`Logged in as ${label} on ${config.host}`);
|
|
123
153
|
}
|
|
124
154
|
async function logout() {
|
|
125
155
|
const config = await readAuthConfig();
|
|
@@ -127,6 +157,17 @@ async function logout() {
|
|
|
127
157
|
console.log("Not logged in.");
|
|
128
158
|
return;
|
|
129
159
|
}
|
|
160
|
+
try {
|
|
161
|
+
await cliAuthFetch("/api/cli-auth/revoke", { method: "POST" });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err instanceof CliAuthExpiredError) {
|
|
164
|
+
console.log("Logged out (token was already invalid).");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
console.warn(
|
|
168
|
+
`Could not contact ${config.host} to revoke token. The token will be cleared locally only.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
130
171
|
await clearAuthConfig();
|
|
131
172
|
console.log("Logged out successfully.");
|
|
132
173
|
}
|
|
@@ -326,7 +367,7 @@ async function readJsonIfExists(filePath) {
|
|
|
326
367
|
}
|
|
327
368
|
return readJson(filePath);
|
|
328
369
|
}
|
|
329
|
-
function
|
|
370
|
+
function protocolForHost2(host) {
|
|
330
371
|
return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
331
372
|
}
|
|
332
373
|
async function requireCliBaseUrl() {
|
|
@@ -335,7 +376,7 @@ async function requireCliBaseUrl() {
|
|
|
335
376
|
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
336
377
|
}
|
|
337
378
|
return {
|
|
338
|
-
baseUrl: `${
|
|
379
|
+
baseUrl: `${protocolForHost2(config.host)}://${config.host}`,
|
|
339
380
|
token: config.sessionToken
|
|
340
381
|
};
|
|
341
382
|
}
|
|
@@ -515,6 +556,16 @@ async function buildTheme(root, skipThemeBuild) {
|
|
|
515
556
|
await writeThemeArtifacts(validated);
|
|
516
557
|
return validateTheme(root);
|
|
517
558
|
}
|
|
559
|
+
function runThemeCheck(theme) {
|
|
560
|
+
const result = checkThemeModule(theme.module);
|
|
561
|
+
const text = formatThemeCheckResult(result);
|
|
562
|
+
if (result.ok) {
|
|
563
|
+
console.log(text);
|
|
564
|
+
} else {
|
|
565
|
+
console.error(text);
|
|
566
|
+
}
|
|
567
|
+
return result.ok;
|
|
568
|
+
}
|
|
518
569
|
async function watchTheme(root, skipThemeBuild) {
|
|
519
570
|
if (!skipThemeBuild) {
|
|
520
571
|
await runThemePackageBuild(root);
|
|
@@ -1162,8 +1213,16 @@ async function checksum(files) {
|
|
|
1162
1213
|
}
|
|
1163
1214
|
return hash.digest("hex");
|
|
1164
1215
|
}
|
|
1165
|
-
async function publishTheme(root, skipBuild) {
|
|
1216
|
+
async function publishTheme(root, skipBuild, skipCheck) {
|
|
1166
1217
|
const theme = skipBuild ? await validateTheme(root) : await buildTheme(root, false);
|
|
1218
|
+
if (!skipCheck) {
|
|
1219
|
+
const ok = runThemeCheck(theme);
|
|
1220
|
+
if (!ok) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
"AI metadata check failed. Fix the errors above or rerun with --skip-check."
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1167
1226
|
const files = await collectThemeArtifactFiles(theme);
|
|
1168
1227
|
if (!theme.clientEntryPath) {
|
|
1169
1228
|
throw new Error("Missing dist/runtime.client.js. Run `suda theme build` first.");
|
|
@@ -1172,7 +1231,7 @@ async function publishTheme(root, skipBuild) {
|
|
|
1172
1231
|
if (!config) {
|
|
1173
1232
|
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
1174
1233
|
}
|
|
1175
|
-
const baseUrl = `${
|
|
1234
|
+
const baseUrl = `${protocolForHost2(config.host)}://${config.host}`;
|
|
1176
1235
|
const { key, version } = theme.module.manifest;
|
|
1177
1236
|
const intentRes = await fetch(`${baseUrl}/api/cli/themes/publish-intent`, {
|
|
1178
1237
|
method: "POST",
|
|
@@ -1240,8 +1299,54 @@ async function publishTheme(root, skipBuild) {
|
|
|
1240
1299
|
}
|
|
1241
1300
|
console.log(`published ${key}@${version}`);
|
|
1242
1301
|
}
|
|
1302
|
+
var cachedCliPackageVersions;
|
|
1303
|
+
async function readCliPackageVersions() {
|
|
1304
|
+
if (cachedCliPackageVersions) {
|
|
1305
|
+
return cachedCliPackageVersions;
|
|
1306
|
+
}
|
|
1307
|
+
const here = fileURLToPath(import.meta.url);
|
|
1308
|
+
const cliPkgPath = path2.resolve(path2.dirname(here), "..", "package.json");
|
|
1309
|
+
let cliRaw;
|
|
1310
|
+
try {
|
|
1311
|
+
cliRaw = await readFile(cliPkgPath, "utf8");
|
|
1312
|
+
} catch {
|
|
1313
|
+
cachedCliPackageVersions = { themeEngine: "*", cli: "*" };
|
|
1314
|
+
return cachedCliPackageVersions;
|
|
1315
|
+
}
|
|
1316
|
+
const cliPkg = JSON.parse(cliRaw);
|
|
1317
|
+
const cliVersion = typeof cliPkg.version === "string" ? cliPkg.version : "0.0.0";
|
|
1318
|
+
const declaredEngine = cliPkg.dependencies?.["@sudajs/theme-engine"];
|
|
1319
|
+
let themeEngine;
|
|
1320
|
+
if (declaredEngine && !declaredEngine.startsWith("workspace:")) {
|
|
1321
|
+
themeEngine = declaredEngine;
|
|
1322
|
+
} else {
|
|
1323
|
+
let resolved;
|
|
1324
|
+
try {
|
|
1325
|
+
const require2 = createRequire(import.meta.url);
|
|
1326
|
+
const enginePkgPath = require2.resolve("@sudajs/theme-engine/package.json");
|
|
1327
|
+
const engineRaw = await readFile(enginePkgPath, "utf8");
|
|
1328
|
+
const engineVersion = JSON.parse(engineRaw).version;
|
|
1329
|
+
if (typeof engineVersion === "string" && engineVersion.length > 0) {
|
|
1330
|
+
resolved = `^${engineVersion}`;
|
|
1331
|
+
}
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
if (!resolved) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
"suda theme init: cannot determine the @sudajs/theme-engine version to scaffold. The CLI's package.json declares it as `workspace:*` (monorepo dev mode) but `@sudajs/theme-engine/package.json` could not be resolved from the CLI's runtime. Run `pnpm install` and re-run from the monorepo root, or install a published @sudajs/cli tarball (which pins a real semver range)."
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
themeEngine = resolved;
|
|
1340
|
+
}
|
|
1341
|
+
cachedCliPackageVersions = {
|
|
1342
|
+
themeEngine,
|
|
1343
|
+
cli: `^${cliVersion}`
|
|
1344
|
+
};
|
|
1345
|
+
return cachedCliPackageVersions;
|
|
1346
|
+
}
|
|
1243
1347
|
async function initTheme(target) {
|
|
1244
1348
|
const key = path2.basename(target).replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
|
|
1349
|
+
const cliPackageVersions = await readCliPackageVersions();
|
|
1245
1350
|
await mkdir(path2.join(target, "src"), { recursive: true });
|
|
1246
1351
|
await writeFile(
|
|
1247
1352
|
path2.join(target, "package.json"),
|
|
@@ -1267,11 +1372,11 @@ async function initTheme(target) {
|
|
|
1267
1372
|
preview: "suda theme preview --theme-root ."
|
|
1268
1373
|
},
|
|
1269
1374
|
dependencies: {
|
|
1270
|
-
"@sudajs/theme-engine":
|
|
1375
|
+
"@sudajs/theme-engine": cliPackageVersions.themeEngine
|
|
1271
1376
|
},
|
|
1272
1377
|
devDependencies: {
|
|
1273
1378
|
"@puckeditor/core": "^0.21.2",
|
|
1274
|
-
"@sudajs/cli":
|
|
1379
|
+
"@sudajs/cli": cliPackageVersions.cli,
|
|
1275
1380
|
"@types/node": "^22.0.0",
|
|
1276
1381
|
"@types/react": "^19.0.0",
|
|
1277
1382
|
"@types/react-dom": "^19.0.0",
|
|
@@ -1354,10 +1459,17 @@ export const sourceManifest: ThemeSourceManifest = {
|
|
|
1354
1459
|
);
|
|
1355
1460
|
await writeFile(
|
|
1356
1461
|
path2.join(target, "src", "sections.tsx"),
|
|
1357
|
-
`import type {
|
|
1462
|
+
`import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1358
1463
|
|
|
1359
|
-
export const Hero:
|
|
1464
|
+
export const Hero: SudaComponentConfig = {
|
|
1360
1465
|
label: "Hero",
|
|
1466
|
+
ai: {
|
|
1467
|
+
instructions:
|
|
1468
|
+
"Primary page introduction used to communicate the main value proposition. " +
|
|
1469
|
+
"Place near the beginning of landing, product, service, or campaign pages. " +
|
|
1470
|
+
"Include one concise headline, supporting copy, and one primary call to action. " +
|
|
1471
|
+
"Use at most once per page. Do not use for ordinary section headings or article content.",
|
|
1472
|
+
},
|
|
1361
1473
|
fields: {
|
|
1362
1474
|
eyebrow: { type: "text", label: "Eyebrow" },
|
|
1363
1475
|
title: { type: "text", label: "Title" },
|
|
@@ -1382,8 +1494,14 @@ export const Hero: ComponentConfig = {
|
|
|
1382
1494
|
),
|
|
1383
1495
|
};
|
|
1384
1496
|
|
|
1385
|
-
export const FeatureGrid:
|
|
1497
|
+
export const FeatureGrid: SudaComponentConfig = {
|
|
1386
1498
|
label: "Feature grid",
|
|
1499
|
+
ai: {
|
|
1500
|
+
instructions:
|
|
1501
|
+
"Section that lists 3\u20136 short feature or benefit cards explaining what the product or service offers. " +
|
|
1502
|
+
"Place after the hero / introduction and before the final call-to-action. " +
|
|
1503
|
+
"Use when the page needs to communicate multiple distinct value points; do not use for testimonials, FAQs, or step-by-step processes.",
|
|
1504
|
+
},
|
|
1387
1505
|
fields: {
|
|
1388
1506
|
title: { type: "text", label: "Title" },
|
|
1389
1507
|
description: { type: "textarea", label: "Description" },
|
|
@@ -1421,8 +1539,14 @@ export const FeatureGrid: ComponentConfig = {
|
|
|
1421
1539
|
),
|
|
1422
1540
|
};
|
|
1423
1541
|
|
|
1424
|
-
export const Testimonial:
|
|
1542
|
+
export const Testimonial: SudaComponentConfig = {
|
|
1425
1543
|
label: "Testimonial",
|
|
1544
|
+
ai: {
|
|
1545
|
+
instructions:
|
|
1546
|
+
"Section that quotes a single customer or expert as social proof. " +
|
|
1547
|
+
"Place mid-page after a feature or value section, or near the end before the final CTA. " +
|
|
1548
|
+
"Do not invent quotes, names, or roles; only use content the user provides.",
|
|
1549
|
+
},
|
|
1426
1550
|
fields: {
|
|
1427
1551
|
quote: { type: "textarea", label: "Quote" },
|
|
1428
1552
|
author: { type: "text", label: "Author" },
|
|
@@ -1441,8 +1565,14 @@ export const Testimonial: ComponentConfig = {
|
|
|
1441
1565
|
),
|
|
1442
1566
|
};
|
|
1443
1567
|
|
|
1444
|
-
export const CallToAction:
|
|
1568
|
+
export const CallToAction: SudaComponentConfig = {
|
|
1445
1569
|
label: "Call to action",
|
|
1570
|
+
ai: {
|
|
1571
|
+
instructions:
|
|
1572
|
+
"Closing conversion section that invites the visitor to take a specific action (sign up, contact, buy, etc.). " +
|
|
1573
|
+
"Place near the end of the page, after supporting content. " +
|
|
1574
|
+
"Use at most once per page. Do not use as the page's first introduction \u2014 the Hero component fills that role.",
|
|
1575
|
+
},
|
|
1446
1576
|
fields: {
|
|
1447
1577
|
title: { type: "text", label: "Title" },
|
|
1448
1578
|
description: { type: "textarea", label: "Description" },
|
|
@@ -1470,7 +1600,8 @@ export const SECTION_COMPONENTS = { Hero, FeatureGrid, Testimonial, CallToAction
|
|
|
1470
1600
|
await writeFile(
|
|
1471
1601
|
path2.join(target, "src", "layout.tsx"),
|
|
1472
1602
|
`import { getPageSlot } from "@sudajs/theme-engine/runtime";
|
|
1473
|
-
import type {
|
|
1603
|
+
import type { Config } from "@puckeditor/core";
|
|
1604
|
+
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1474
1605
|
import type { ReactElement, ReactNode } from "react";
|
|
1475
1606
|
|
|
1476
1607
|
type PuckExtras = { puck?: { metadata?: Record<string, unknown> } };
|
|
@@ -1485,22 +1616,40 @@ export const rootConfig: NonNullable<Config["root"]> = {
|
|
|
1485
1616
|
),
|
|
1486
1617
|
};
|
|
1487
1618
|
|
|
1488
|
-
export const Header:
|
|
1619
|
+
export const Header: SudaComponentConfig = {
|
|
1489
1620
|
label: "Header",
|
|
1621
|
+
ai: {
|
|
1622
|
+
instructions:
|
|
1623
|
+
"Site-wide header rendered at the top of every page. " +
|
|
1624
|
+
"Place once at the top of the layout (not inside page content). " +
|
|
1625
|
+
"Used for branding and primary navigation, not for promotional content.",
|
|
1626
|
+
},
|
|
1490
1627
|
fields: { siteName: { type: "text", label: "Site name" } },
|
|
1491
1628
|
defaultProps: { siteName: "${key}" },
|
|
1492
1629
|
render: ({ siteName }) => <header className="${key}-header">{siteName}</header>,
|
|
1493
1630
|
};
|
|
1494
1631
|
|
|
1495
|
-
export const PageOutlet:
|
|
1632
|
+
export const PageOutlet: SudaComponentConfig = {
|
|
1496
1633
|
label: "Page outlet",
|
|
1634
|
+
ai: {
|
|
1635
|
+
exclude: true,
|
|
1636
|
+
instructions:
|
|
1637
|
+
"Structural slot where each page's content is injected. " +
|
|
1638
|
+
"Managed by the layout, never created or placed by AI.",
|
|
1639
|
+
},
|
|
1497
1640
|
fields: {},
|
|
1498
1641
|
defaultProps: {},
|
|
1499
1642
|
render: (props: PuckExtras): ReactElement => <>{getPageSlot(props.puck?.metadata)}</>,
|
|
1500
1643
|
};
|
|
1501
1644
|
|
|
1502
|
-
export const Footer:
|
|
1645
|
+
export const Footer: SudaComponentConfig = {
|
|
1503
1646
|
label: "Footer",
|
|
1647
|
+
ai: {
|
|
1648
|
+
instructions:
|
|
1649
|
+
"Site-wide footer rendered at the bottom of every page. " +
|
|
1650
|
+
"Place once at the end of the layout. " +
|
|
1651
|
+
"Used for legal text, copyright, and secondary links \u2014 not for primary CTAs.",
|
|
1652
|
+
},
|
|
1504
1653
|
fields: { text: { type: "text", label: "Text" } },
|
|
1505
1654
|
defaultProps: { text: "\xA9 ${key}" },
|
|
1506
1655
|
render: ({ text }) => <footer className="${key}-footer">{text}</footer>,
|
|
@@ -1720,14 +1869,110 @@ It is a **standalone** npm package \u2014 it does not depend on the SudaCloud mo
|
|
|
1720
1869
|
- Prefer editing section props over changing render code when generating pages.
|
|
1721
1870
|
- Do not edit files under \`dist/\`; they are generated.
|
|
1722
1871
|
|
|
1872
|
+
## AI metadata (required)
|
|
1873
|
+
|
|
1874
|
+
Every component (and zone-level layout component) **must** declare \`ai.instructions\`.
|
|
1875
|
+
Without it the page-generation agent has no way to know what the component is for,
|
|
1876
|
+
and \`suda theme check\` (run automatically before publish) will fail.
|
|
1877
|
+
|
|
1878
|
+
### Component-level \`ai\`
|
|
1879
|
+
|
|
1880
|
+
Each \`ComponentConfig\` must include:
|
|
1881
|
+
|
|
1882
|
+
\`\`\`ts
|
|
1883
|
+
import type { SudaComponentConfig } from "@sudajs/theme-engine";
|
|
1884
|
+
|
|
1885
|
+
export const Hero: SudaComponentConfig = {
|
|
1886
|
+
label: "Hero",
|
|
1887
|
+
ai: {
|
|
1888
|
+
instructions:
|
|
1889
|
+
"Primary page introduction used to communicate the main value proposition. " +
|
|
1890
|
+
"Place near the beginning of landing or product pages. " +
|
|
1891
|
+
"Include one primary CTA. Use at most once per page. " +
|
|
1892
|
+
"Do not use for ordinary section headings or article content.",
|
|
1893
|
+
},
|
|
1894
|
+
fields: { /* ... */ },
|
|
1895
|
+
defaultProps: { /* ... */ },
|
|
1896
|
+
render: (props) => /* ... */,
|
|
1897
|
+
};
|
|
1898
|
+
\`\`\`
|
|
1899
|
+
|
|
1900
|
+
A good \`instructions\` covers, in plain English:
|
|
1901
|
+
|
|
1902
|
+
1. **Purpose** \u2014 what role this component plays on a page.
|
|
1903
|
+
2. **Use when** \u2014 page types or scenarios where it should be selected.
|
|
1904
|
+
3. **Avoid when** \u2014 situations that look similar but call for a different component.
|
|
1905
|
+
4. **Placement** \u2014 where in the page it normally belongs (top, after features, near end, etc.).
|
|
1906
|
+
5. **Frequency / composition** \u2014 how many times it can appear, what it must contain, and any adjacency rules.
|
|
1907
|
+
|
|
1908
|
+
Write executable, specific sentences (\`Use when...\`, \`Place after...\`, \`Use at most once per page.\`).
|
|
1909
|
+
Do **not** describe styling (colors, fonts, spacing) \u2014 those belong to the design system.
|
|
1910
|
+
Do **not** write vague filler like "modern", "engaging", "beautiful".
|
|
1911
|
+
|
|
1912
|
+
If two components look similar (e.g. Hero vs. PageHeader vs. SectionHeader),
|
|
1913
|
+
their \`instructions\` must explicitly differentiate them, otherwise the agent
|
|
1914
|
+
will mix them up.
|
|
1915
|
+
|
|
1916
|
+
### Hiding a component from AI
|
|
1917
|
+
|
|
1918
|
+
Use \`ai.exclude: true\` for internal/debug components or anything the agent
|
|
1919
|
+
should never auto-create. Such components remain editable by humans:
|
|
1920
|
+
|
|
1921
|
+
\`\`\`ts
|
|
1922
|
+
ai: { exclude: true, instructions: "Internal debug block; AI must not create it." }
|
|
1923
|
+
\`\`\`
|
|
1924
|
+
|
|
1925
|
+
### Field-level \`ai\` (recommended, optional)
|
|
1926
|
+
|
|
1927
|
+
Field-level metadata sharpens the generated content but is **not required**.
|
|
1928
|
+
Use it when a field's name and type alone don't fully convey the constraints:
|
|
1929
|
+
|
|
1930
|
+
\`\`\`ts
|
|
1931
|
+
fields: {
|
|
1932
|
+
title: {
|
|
1933
|
+
type: "text",
|
|
1934
|
+
ai: {
|
|
1935
|
+
instructions:
|
|
1936
|
+
"Main value-proposition headline. 4\u201312 words, focus on the visitor benefit, " +
|
|
1937
|
+
"do not repeat the eyebrow or description.",
|
|
1938
|
+
required: true,
|
|
1939
|
+
},
|
|
1940
|
+
},
|
|
1941
|
+
imageUrl: {
|
|
1942
|
+
type: "text",
|
|
1943
|
+
ai: {
|
|
1944
|
+
// Atomic values that cannot render correctly while half-streamed.
|
|
1945
|
+
stream: false,
|
|
1946
|
+
instructions: "Image that supports the section topic. Avoid logos, screenshots, or unrelated decoration.",
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
}
|
|
1950
|
+
\`\`\`
|
|
1951
|
+
|
|
1952
|
+
Supported field-level keys: \`instructions\`, \`required\`, \`exclude\`, \`stream\`,
|
|
1953
|
+
\`bind\` (delegate to a tool), and \`schema\` (only needed for \`custom\` / \`external\` /
|
|
1954
|
+
\`user\` fields whose runtime shape can't be inferred).
|
|
1955
|
+
|
|
1956
|
+
### Verifying
|
|
1957
|
+
|
|
1958
|
+
\`\`\`bash
|
|
1959
|
+
pnpm build # also runs the AI metadata check
|
|
1960
|
+
suda theme check # run the check on its own
|
|
1961
|
+
\`\`\`
|
|
1962
|
+
|
|
1963
|
+
Missing component-level \`ai.instructions\` is reported as an **error** and blocks
|
|
1964
|
+
the build. Very short or label-repeating instructions are reported as
|
|
1965
|
+
**warnings**.
|
|
1966
|
+
|
|
1723
1967
|
## Useful commands
|
|
1724
1968
|
|
|
1725
1969
|
\`\`\`bash
|
|
1726
1970
|
pnpm install
|
|
1727
1971
|
pnpm typecheck
|
|
1728
|
-
pnpm build # tsc + suda theme build --skip-theme-build
|
|
1972
|
+
pnpm build # tsc + suda theme build --skip-theme-build (runs AI check)
|
|
1729
1973
|
pnpm validate
|
|
1730
1974
|
pnpm preview
|
|
1975
|
+
suda theme check # AI metadata check only
|
|
1731
1976
|
suda agent theme describe local --theme-root .
|
|
1732
1977
|
suda agent section schema --theme local --section Hero --theme-root .
|
|
1733
1978
|
suda agent page validate --theme local --input ./page.json --theme-root .
|
|
@@ -1799,9 +2044,29 @@ function buildProgram() {
|
|
|
1799
2044
|
);
|
|
1800
2045
|
}
|
|
1801
2046
|
});
|
|
1802
|
-
theme.command("build").description("Build server dist (via package script) and browser runtime.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-theme-build", "Skip the theme package build script.").
|
|
2047
|
+
theme.command("build").description("Build server dist (via package script) and browser runtime.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-theme-build", "Skip the theme package build script.").option(
|
|
2048
|
+
"--skip-check",
|
|
2049
|
+
"Skip the AI metadata check that normally runs after build."
|
|
2050
|
+
).action(async (options) => {
|
|
1803
2051
|
const result = await buildTheme(resolveThemeRoot(options), options.skipThemeBuild === true);
|
|
1804
2052
|
console.log(`built ${result.module.manifest.key}@${result.module.manifest.version}`);
|
|
2053
|
+
if (options.skipCheck !== true) {
|
|
2054
|
+
const ok = runThemeCheck(result);
|
|
2055
|
+
if (!ok) {
|
|
2056
|
+
throw new Error(
|
|
2057
|
+
"AI metadata check failed. Fix the errors above or rerun with --skip-check."
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
theme.command("check").description(
|
|
2063
|
+
"Run AI metadata checks on a built theme without rebuilding. Reports missing component-level ai.instructions as errors and weak content as warnings."
|
|
2064
|
+
).option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
|
|
2065
|
+
const theme2 = await validateTheme(resolveThemeRoot(options));
|
|
2066
|
+
const ok = runThemeCheck(theme2);
|
|
2067
|
+
if (!ok) {
|
|
2068
|
+
process.exitCode = 1;
|
|
2069
|
+
}
|
|
1805
2070
|
});
|
|
1806
2071
|
theme.command("dev").description("Watch and rebuild the browser runtime.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-theme-build", "Skip the theme package build script.").action(async (options) => {
|
|
1807
2072
|
await watchTheme(resolveThemeRoot(options), options.skipThemeBuild === true);
|
|
@@ -1815,8 +2080,15 @@ function buildProgram() {
|
|
|
1815
2080
|
).option("--theme-root <path>", "Theme source/artifact root.").option("--output <path>", "Output PNG path relative to theme root.").option("--width <px>", "Viewport width in pixels.", "1280").option("--height <px>", "Viewport height in pixels.", "800").option("--port <port>", "Preview server port used during capture.", "4178").option("--skip-build", "Skip rebuilding the theme before capturing.").action(async (options) => {
|
|
1816
2081
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
1817
2082
|
});
|
|
1818
|
-
theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").
|
|
1819
|
-
|
|
2083
|
+
theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").option(
|
|
2084
|
+
"--skip-check",
|
|
2085
|
+
"Skip the AI metadata check that normally runs before upload."
|
|
2086
|
+
).action(async (options) => {
|
|
2087
|
+
await publishTheme(
|
|
2088
|
+
resolveThemeRoot(options),
|
|
2089
|
+
options.skipBuild === true,
|
|
2090
|
+
options.skipCheck === true
|
|
2091
|
+
);
|
|
1820
2092
|
});
|
|
1821
2093
|
const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
|
|
1822
2094
|
const agentProject = agent.command("project").description("Manage Suda projects from the CLI.");
|