@sudajs/cli 0.9.4 → 0.10.1

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/bin/suda-dev.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import pc from "picocolors";
5
6
 
6
7
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
7
8
  const sourceEntry = path.resolve(scriptDir, "../src/index.ts");
@@ -10,6 +11,6 @@ try {
10
11
  const mod = await import(pathToFileURL(sourceEntry).href);
11
12
  await mod.main();
12
13
  } catch (error) {
13
- console.error(error instanceof Error ? error.message : error);
14
+ console.error(pc.red(error instanceof Error ? error.message : String(error)));
14
15
  process.exitCode = 1;
15
16
  }
package/bin/suda.js CHANGED
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
5
5
  import { existsSync } from "node:fs";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import pc from "picocolors";
8
9
 
9
10
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
10
11
  const sourceEntry = path.resolve(scriptDir, "../src/index.ts");
@@ -30,7 +31,7 @@ if (existsSync(sourceEntry)) {
30
31
  );
31
32
 
32
33
  if (result.error) {
33
- console.error(result.error.message);
34
+ console.error(pc.red(result.error.message));
34
35
  process.exit(1);
35
36
  }
36
37
 
@@ -42,7 +43,7 @@ if (existsSync(sourceEntry)) {
42
43
  }
43
44
 
44
45
  if (!existsSync(distEntry)) {
45
- console.error("@sudajs/cli is not built yet. Run `pnpm --filter @sudajs/cli build` first.");
46
+ console.error(pc.red("@sudajs/cli is not built yet. Run `pnpm --filter @sudajs/cli build` first."));
46
47
  process.exit(1);
47
48
  }
48
49
 
@@ -50,6 +51,6 @@ const mod = await import(pathToFileURL(distEntry).href);
50
51
  try {
51
52
  await mod.main();
52
53
  } catch (error) {
53
- console.error(error instanceof Error ? error.message : error);
54
+ console.error(pc.red(error instanceof Error ? error.message : String(error)));
54
55
  process.exitCode = 1;
55
56
  }
package/dist/index.d.ts CHANGED
@@ -90,6 +90,7 @@ declare function fetchJson<T>(url: string, options?: RequestInit & {
90
90
  token?: string;
91
91
  }): Promise<T>;
92
92
  declare function validateTheme(root: string): Promise<ValidatedTheme>;
93
+ declare function validateThemeLocales(root: string): Promise<void>;
93
94
  declare function toViteModuleUrl(root: string, absolutePath: string): string;
94
95
  declare function buildTheme(root: string): Promise<ValidatedTheme>;
95
96
  declare function finalizeTheme(root: string): Promise<ValidatedTheme>;
@@ -110,6 +111,7 @@ declare const __testUtils: {
110
111
  slugifyThemeName: typeof slugifyThemeName;
111
112
  validateThemeKey: typeof validateThemeKey;
112
113
  validateThemeName: typeof validateThemeName;
114
+ validateThemeLocales: typeof validateThemeLocales;
113
115
  toViteModuleUrl: typeof toViteModuleUrl;
114
116
  updateRemotePageDraft: typeof updateRemotePageDraft;
115
117
  };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'crypto';
3
- import fs, { writeFile, stat, readFile, readdir, mkdir, copyFile, rm } from 'fs/promises';
3
+ import fs, { writeFile, stat, readdir, readFile, mkdir, copyFile, rm } from 'fs/promises';
4
4
  import { createRequire } from 'module';
5
5
  import path2 from 'path';
6
6
  import { createInterface } from 'readline/promises';
@@ -13,7 +13,35 @@ import { Command } from 'commander';
13
13
  import { build } from 'esbuild';
14
14
  import { z } from 'zod';
15
15
  import os from 'os';
16
+ import pc from 'picocolors';
16
17
 
18
+ var style = {
19
+ success: pc.green,
20
+ warning: pc.yellow,
21
+ error: pc.red,
22
+ info: pc.cyan,
23
+ url: pc.cyan,
24
+ code: pc.bold,
25
+ value: pc.bold,
26
+ path: pc.magenta
27
+ };
28
+ function success(message) {
29
+ return style.success(message);
30
+ }
31
+ function warning(message) {
32
+ return style.warning(message);
33
+ }
34
+ function error(message) {
35
+ return style.error(message);
36
+ }
37
+ function info(message) {
38
+ return style.info(message);
39
+ }
40
+ function themeRef(key, version) {
41
+ return style.value(`${key}@${version}`);
42
+ }
43
+
44
+ // src/auth.ts
17
45
  function getConfigPath() {
18
46
  return path2.join(os.homedir(), ".config", "suda", "config.json");
19
47
  }
@@ -78,7 +106,7 @@ async function cliAuthFetch(pathname, init = {}) {
78
106
  }
79
107
  async function login(host = "app.sudayun.cn") {
80
108
  const baseUrl = `${protocolForHost(host)}://${host}`;
81
- console.log(`Requesting device authorization from ${baseUrl}...`);
109
+ console.log(`Requesting device authorization from ${style.url(baseUrl)}...`);
82
110
  const deviceRes = await fetch(`${baseUrl}/api/cli-auth/device`, {
83
111
  method: "POST"
84
112
  });
@@ -90,9 +118,9 @@ async function login(host = "app.sudayun.cn") {
90
118
  console.log(`
91
119
  Please open the following URL in your browser to authorize Suda CLI:
92
120
  `);
93
- console.log(` ${authUrl}
121
+ console.log(` ${style.url(authUrl)}
94
122
  `);
95
- console.log(`Your confirmation code is: ${userCode}
123
+ console.log(`Your confirmation code is: ${style.code(userCode)}
96
124
  `);
97
125
  try {
98
126
  const open = (await import('open')).default;
@@ -102,7 +130,7 @@ Please open the following URL in your browser to authorize Suda CLI:
102
130
  const pollInterval = (interval || 5) * 1e3;
103
131
  const timeoutSeconds = expiresIn || 300;
104
132
  const deadline = Date.now() + timeoutSeconds * 1e3;
105
- console.log("Waiting for authorization...");
133
+ console.log(info("Waiting for authorization..."));
106
134
  while (Date.now() < deadline) {
107
135
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
108
136
  const pollRes = await fetch(`${baseUrl}/api/cli-auth/poll`, {
@@ -113,7 +141,7 @@ Please open the following URL in your browser to authorize Suda CLI:
113
141
  const data = await pollRes.json();
114
142
  if (pollRes.ok && data.status === "approved" && data.token) {
115
143
  await writeAuthConfig({ sessionToken: data.token, host });
116
- console.log("Successfully authorized!");
144
+ console.log(success("Successfully authorized!"));
117
145
  return;
118
146
  }
119
147
  if (data.error === "authorization_pending") {
@@ -126,7 +154,7 @@ Please open the following URL in your browser to authorize Suda CLI:
126
154
  async function status() {
127
155
  const config = await readAuthConfig();
128
156
  if (!config) {
129
- console.log("Not logged in. Run `suda auth login` to authenticate.");
157
+ console.log(warning("Not logged in. Run `suda auth login` to authenticate."));
130
158
  return;
131
159
  }
132
160
  let res;
@@ -134,41 +162,43 @@ async function status() {
134
162
  res = await cliAuthFetch("/api/cli-auth/whoami");
135
163
  } catch (err) {
136
164
  if (err instanceof CliAuthExpiredError) {
137
- console.log(err.message);
165
+ console.log(warning(err.message));
138
166
  return;
139
167
  }
140
- console.error(`Failed to connect to ${config.host}.`);
168
+ console.error(error(`Failed to connect to ${config.host}.`));
141
169
  return;
142
170
  }
143
171
  if (!res.ok) {
144
172
  console.error(
145
- `Failed to verify session (${res.status} ${res.statusText}). Try again later.`
173
+ error(`Failed to verify session (${res.status} ${res.statusText}). Try again later.`)
146
174
  );
147
175
  return;
148
176
  }
149
177
  const data = await res.json();
150
178
  const label = data.user?.email ?? data.user?.name ?? "your account";
151
- console.log(`Logged in as ${label} on ${config.host}`);
179
+ console.log(success(`Logged in as ${style.value(label)} on ${style.url(config.host)}`));
152
180
  }
153
181
  async function logout() {
154
182
  const config = await readAuthConfig();
155
183
  if (!config) {
156
- console.log("Not logged in.");
184
+ console.log(warning("Not logged in."));
157
185
  return;
158
186
  }
159
187
  try {
160
188
  await cliAuthFetch("/api/cli-auth/revoke", { method: "POST" });
161
189
  } catch (err) {
162
190
  if (err instanceof CliAuthExpiredError) {
163
- console.log("Logged out (token was already invalid).");
191
+ console.log(warning("Logged out (token was already invalid)."));
164
192
  return;
165
193
  }
166
194
  console.warn(
167
- `Could not contact ${config.host} to revoke token. The token will be cleared locally only.`
195
+ warning(
196
+ `Could not contact ${config.host} to revoke token. The token will be cleared locally only.`
197
+ )
168
198
  );
169
199
  }
170
200
  await clearAuthConfig();
171
- console.log("Logged out successfully.");
201
+ console.log(success("Logged out successfully."));
172
202
  }
173
203
 
174
204
  // src/index.ts
@@ -538,6 +568,7 @@ async function validateTheme(root) {
538
568
  }
539
569
  const module = await loadThemeModule(serverEntryPath);
540
570
  validateThemeModule(module);
571
+ await validateThemeLocales(root);
541
572
  const result = {
542
573
  root,
543
574
  module,
@@ -553,6 +584,73 @@ async function validateTheme(root) {
553
584
  }
554
585
  return result;
555
586
  }
587
+ function isRecord(value) {
588
+ return typeof value === "object" && value !== null && !Array.isArray(value);
589
+ }
590
+ function collectLocaleKeys(value, pathSegments = [], keys = []) {
591
+ if (typeof value === "string") {
592
+ keys.push(pathSegments.join("."));
593
+ return keys;
594
+ }
595
+ if (!isRecord(value)) {
596
+ throw new Error(
597
+ `Theme locale value at ${pathSegments.join(".") || "<root>"} must be a string or object.`
598
+ );
599
+ }
600
+ for (const [key, child] of Object.entries(value)) {
601
+ collectLocaleKeys(child, [...pathSegments, key], keys);
602
+ }
603
+ return keys;
604
+ }
605
+ async function readLocaleJson(filePath) {
606
+ try {
607
+ return JSON.parse(await readFile(filePath, "utf8"));
608
+ } catch (error2) {
609
+ const reason = error2 instanceof Error ? error2.message : "invalid JSON";
610
+ throw new Error(`Invalid theme locale JSON at ${filePath}: ${reason}`);
611
+ }
612
+ }
613
+ async function listLocaleFiles(localesDir) {
614
+ if (!await pathExists(localesDir)) {
615
+ return [];
616
+ }
617
+ const entries = await readdir(localesDir, { withFileTypes: true });
618
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
619
+ }
620
+ async function validateThemeLocales(root) {
621
+ const localesDir = path2.join(root, "dist", "locales");
622
+ const files = await listLocaleFiles(localesDir);
623
+ if (!files.includes("en.json")) {
624
+ throw new Error("Theme locales must include dist/locales/en.json.");
625
+ }
626
+ const referenceFile = "en.json";
627
+ const referenceMessages = await readLocaleJson(path2.join(localesDir, referenceFile));
628
+ const reference = {
629
+ file: referenceFile,
630
+ keys: new Set(collectLocaleKeys(referenceMessages).sort())
631
+ };
632
+ for (const file of files) {
633
+ if (file === referenceFile) {
634
+ continue;
635
+ }
636
+ const filePath = path2.join(localesDir, file);
637
+ const messages = await readLocaleJson(filePath);
638
+ const keys = new Set(collectLocaleKeys(messages).sort());
639
+ const expected = reference;
640
+ const missing = [...expected.keys].filter((key) => !keys.has(key));
641
+ const extra = [...keys].filter((key) => !expected.keys.has(key));
642
+ if (missing.length > 0 || extra.length > 0) {
643
+ const details = [
644
+ ...missing.map((key) => ` - missing ${key}`),
645
+ ...extra.map((key) => ` - extra ${key}`)
646
+ ].join("\n");
647
+ throw new Error(
648
+ `Theme locale ${file} does not match ${expected.file} key set:
649
+ ${details}`
650
+ );
651
+ }
652
+ }
653
+ }
556
654
  var VITE_CONFIG_FILES = [
557
655
  "vite.config.ts",
558
656
  "vite.config.mts",
@@ -651,6 +749,7 @@ async function buildViteTheme(root) {
651
749
  }
652
750
  });
653
751
  await copyThemeAssets(root);
752
+ await copyThemeLocales(root);
654
753
  const stylesheetEntry = path2.join(root, ".suda-build", "styles-entry.ts");
655
754
  await mkdir(path2.dirname(stylesheetEntry), { recursive: true });
656
755
  await writeFile(stylesheetEntry, 'import "../src/styles.css";\n', "utf8");
@@ -685,11 +784,20 @@ async function copyThemeAssets(root) {
685
784
  }
686
785
  await copyDirectory(sourceAssets, path2.join(root, "dist", "assets"));
687
786
  }
787
+ async function copyThemeLocales(root) {
788
+ const sourceLocales = path2.join(root, "src", "locales");
789
+ if (!await pathExists(sourceLocales)) {
790
+ return;
791
+ }
792
+ await copyDirectory(sourceLocales, path2.join(root, "dist", "locales"));
793
+ }
688
794
  async function buildClientRuntime(root, minify) {
689
795
  const entryPoint = await findClientEntry(root);
690
796
  if (!await pathExists(entryPoint)) {
691
797
  throw new Error(`Missing client entry: ${entryPoint}`);
692
798
  }
799
+ const packageJson = JSON.parse(await readFile(path2.join(root, "package.json"), "utf8"));
800
+ const themeVersion = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
693
801
  await mkdir(path2.join(root, "dist"), { recursive: true });
694
802
  const hostReactShimPlugin = createHostReactShimPlugin();
695
803
  const themeEngineRuntimeAliasPlugin = createThemeEngineRuntimeAliasPlugin(root);
@@ -710,6 +818,9 @@ async function buildClientRuntime(root, minify) {
710
818
  minify,
711
819
  outfile: path2.join(root, "dist", "runtime.client.js"),
712
820
  platform: "browser",
821
+ define: {
822
+ __SUDA_THEME_VERSION__: JSON.stringify(themeVersion)
823
+ },
713
824
  plugins: [hostReactShimPlugin, themeEngineRuntimeAliasPlugin, themeDependencyResolverPlugin],
714
825
  sourcemap: false,
715
826
  target: "es2022",
@@ -862,6 +973,7 @@ var __testUtils = {
862
973
  slugifyThemeName,
863
974
  validateThemeKey,
864
975
  validateThemeName,
976
+ validateThemeLocales,
865
977
  toViteModuleUrl,
866
978
  updateRemotePageDraft
867
979
  };
@@ -872,20 +984,24 @@ async function runThemeCheck(theme) {
872
984
  const ok = result.ok && footerIssues.length === 0 && previewIssues.length === 0;
873
985
  const lines = [formatThemeCheckResult(result)];
874
986
  if (footerIssues.length === 0 && result.ok) {
875
- lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
987
+ lines.push(
988
+ success("Footer contract check passed: whiteLabel and ICP metadata render correctly.")
989
+ );
876
990
  } else if (footerIssues.length > 0) {
877
991
  for (const issue of footerIssues) {
878
- lines.push(` error layoutConfig.footerContract: ${issue}`);
992
+ lines.push(` ${error("error")} layoutConfig.footerContract: ${issue}`);
879
993
  }
880
- lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
994
+ lines.push(error(`Footer contract check: ${footerIssues.length} error(s).`));
881
995
  }
882
996
  if (previewIssues.length === 0) {
883
- lines.push("Preview contract check passed: desktop, tablet, and mobile screenshots exist.");
997
+ lines.push(
998
+ success("Preview contract check passed: desktop, tablet, and mobile screenshots exist.")
999
+ );
884
1000
  } else {
885
1001
  for (const issue of previewIssues) {
886
- lines.push(` error assets.preview: ${issue}`);
1002
+ lines.push(` ${error("error")} assets.preview: ${issue}`);
887
1003
  }
888
- lines.push(`Preview contract check: ${previewIssues.length} error(s).`);
1004
+ lines.push(error(`Preview contract check: ${previewIssues.length} error(s).`));
889
1005
  }
890
1006
  const text = lines.join("\n");
891
1007
  if (ok) {
@@ -900,8 +1016,8 @@ async function runPreviewContractCheck(theme) {
900
1016
  for (const relativePath of REQUIRED_PREVIEW_ARTIFACTS) {
901
1017
  const filePath = path2.join(theme.root, "dist", relativePath);
902
1018
  try {
903
- const info = await stat(filePath);
904
- if (info.size <= 0) {
1019
+ const info2 = await stat(filePath);
1020
+ if (info2.size <= 0) {
905
1021
  issues.push(`${relativePath} is empty. Run \`suda theme capture\` and rebuild.`);
906
1022
  }
907
1023
  } catch {
@@ -912,7 +1028,7 @@ async function runPreviewContractCheck(theme) {
912
1028
  }
913
1029
  async function watchTheme(root, port) {
914
1030
  const handle = await startViteDevPreviewServer(root, port);
915
- console.log(`previewing Vite theme at ${handle.url}`);
1031
+ console.log(`previewing Vite theme at ${style.url(handle.url)}`);
916
1032
  await new Promise(() => void 0);
917
1033
  }
918
1034
  async function startViteDevPreviewServer(root, port) {
@@ -990,9 +1106,9 @@ function createSudaPreviewVitePlugin(root) {
990
1106
  );
991
1107
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
992
1108
  response.end(html);
993
- } catch (error) {
1109
+ } catch (error2) {
994
1110
  response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
995
- response.end(error instanceof Error ? error.stack : String(error));
1111
+ response.end(error2 instanceof Error ? error2.stack : String(error2));
996
1112
  }
997
1113
  })();
998
1114
  });
@@ -1238,7 +1354,7 @@ async function optimizePngIfAvailable(themeRoot, filePath) {
1238
1354
  const sharp = await loadOptionalSharp(themeRoot);
1239
1355
  if (!sharp) {
1240
1356
  if (!warnedMissingSharp) {
1241
- console.warn(formatMissingSharpWarning());
1357
+ console.warn(warning(formatMissingSharpWarning()));
1242
1358
  warnedMissingSharp = true;
1243
1359
  }
1244
1360
  return;
@@ -1249,7 +1365,7 @@ async function optimizePngIfAvailable(themeRoot, filePath) {
1249
1365
  } catch {
1250
1366
  if (!warnedSharpOptimizeFailure) {
1251
1367
  console.warn(
1252
- "warning: PNG optimization failed. The screenshot was saved uncompressed."
1368
+ warning("warning: PNG optimization failed. The screenshot was saved uncompressed.")
1253
1369
  );
1254
1370
  warnedSharpOptimizeFailure = true;
1255
1371
  }
@@ -1275,7 +1391,9 @@ async function captureScreenshot(root, options) {
1275
1391
  await context.close();
1276
1392
  }
1277
1393
  await optimizePngIfAvailable(root, target.outputPath);
1278
- console.log(`captured ${target.device} ${path2.relative(root, target.outputPath)}`);
1394
+ console.log(
1395
+ success(`captured ${target.device} ${style.path(path2.relative(root, target.outputPath))}`)
1396
+ );
1279
1397
  }
1280
1398
  } finally {
1281
1399
  await browser.close();
@@ -1491,8 +1609,8 @@ async function performConfirmedPageOperation(auth, input, fetcher = fetchJson) {
1491
1609
  };
1492
1610
  }
1493
1611
  async function performActivateTheme(auth, input, fetcher = fetchJson) {
1494
- const themeRef = input.themeVersion ? `${input.themeKey}@${input.themeVersion}` : input.themeKey;
1495
- const impact = `This will activate theme "${themeRef}" for project "${input.projectId}". Public pages will render with this theme after activation.`;
1612
+ const themeRef2 = input.themeVersion ? `${input.themeKey}@${input.themeVersion}` : input.themeKey;
1613
+ const impact = `This will activate theme "${themeRef2}" for project "${input.projectId}". Public pages will render with this theme after activation.`;
1496
1614
  if (input.confirm !== true) {
1497
1615
  return {
1498
1616
  status: "needs_confirmation",
@@ -2059,7 +2177,9 @@ async function publishTheme(root, skipBuild, force) {
2059
2177
  if (forceRes.ok) {
2060
2178
  const { deletedObjects } = await forceRes.json();
2061
2179
  console.log(
2062
- `force-cleared ${key}@${version} (${deletedObjects ?? 0} objects removed). Republishing...`
2180
+ warning(
2181
+ `force-cleared ${themeRef(key, version)} (${deletedObjects ?? 0} objects removed). Republishing...`
2182
+ )
2063
2183
  );
2064
2184
  }
2065
2185
  }
@@ -2098,7 +2218,9 @@ async function publishTheme(root, skipBuild, force) {
2098
2218
  if (!putRes.ok) {
2099
2219
  throw new Error(`Failed to upload ${file.relativePath}: ${putRes.statusText}`);
2100
2220
  }
2101
- console.log(`uploaded ${createThemeObjectKey(key, version, file.relativePath)}`);
2221
+ console.log(
2222
+ success(`uploaded ${style.path(createThemeObjectKey(key, version, file.relativePath))}`)
2223
+ );
2102
2224
  }
2103
2225
  const bundleArtifactRelative = "index.js";
2104
2226
  const previewArtifactRelative = "assets/preview/desktop.png";
@@ -2122,7 +2244,7 @@ async function publishTheme(root, skipBuild, force) {
2122
2244
  if (!completeRes.ok) {
2123
2245
  throw new Error(`Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`);
2124
2246
  }
2125
- console.log(`published ${key}@${version}`);
2247
+ console.log(success(`published ${themeRef(key, version)}`));
2126
2248
  }
2127
2249
  var cachedCliPackageVersions;
2128
2250
  async function readCliPackageVersions() {
@@ -2222,22 +2344,22 @@ function createThemeInitPrompt(nameProvided) {
2222
2344
  async function promptThemeName(rl) {
2223
2345
  while (true) {
2224
2346
  const answer = (await rl.question("Theme name: ")).trim();
2225
- const error = validateThemeName(answer);
2226
- if (error === null) {
2347
+ const error2 = validateThemeName(answer);
2348
+ if (error2 === null) {
2227
2349
  return answer;
2228
2350
  }
2229
- console.error(error);
2351
+ console.error(error(error2));
2230
2352
  }
2231
2353
  }
2232
2354
  async function promptThemeKey(rl, defaultKey) {
2233
2355
  while (true) {
2234
2356
  const answer = (await rl.question(`Theme key (${defaultKey}): `)).trim();
2235
2357
  const key = answer.length > 0 ? answer : defaultKey;
2236
- const error = validateThemeKey(key);
2237
- if (error === null) {
2358
+ const error2 = validateThemeKey(key);
2359
+ if (error2 === null) {
2238
2360
  return key;
2239
2361
  }
2240
- console.error(error);
2362
+ console.error(error(error2));
2241
2363
  }
2242
2364
  }
2243
2365
  async function resolveThemeInitInput(name) {
@@ -2298,7 +2420,7 @@ async function initThemeWithKey(target, key) {
2298
2420
  for (const relativePath of filesToReplace) {
2299
2421
  await replaceTemplateTokens(path2.join(target, relativePath), replacements);
2300
2422
  }
2301
- console.log(`created theme scaffold at ${target}`);
2423
+ console.log(success(`created theme scaffold at ${style.path(target)}`));
2302
2424
  }
2303
2425
  function buildProgram() {
2304
2426
  const program = new Command();
@@ -2310,16 +2432,22 @@ function buildProgram() {
2310
2432
  });
2311
2433
  theme.command("validate").description("Validate a built theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2312
2434
  const result = await validateTheme(resolveThemeRoot(options));
2313
- console.log(`valid ${result.module.manifest.key}@${result.module.manifest.version}`);
2435
+ console.log(
2436
+ success(`valid ${themeRef(result.module.manifest.key, result.module.manifest.version)}`)
2437
+ );
2314
2438
  if (!result.clientEntryPath) {
2315
2439
  console.warn(
2316
- "warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
2440
+ warning(
2441
+ "warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
2442
+ )
2317
2443
  );
2318
2444
  }
2319
2445
  });
2320
2446
  theme.command("build").description("Build a Vite Suda theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2321
2447
  const result = await buildTheme(resolveThemeRoot(options));
2322
- console.log(`built ${result.module.manifest.key}@${result.module.manifest.version}`);
2448
+ console.log(
2449
+ success(`built ${themeRef(result.module.manifest.key, result.module.manifest.version)}`)
2450
+ );
2323
2451
  const ok = await runThemeCheck(result);
2324
2452
  if (!ok) {
2325
2453
  throw new Error("AI metadata check failed. Fix the errors above.");
@@ -2327,7 +2455,11 @@ function buildProgram() {
2327
2455
  });
2328
2456
  theme.command("finalize").description("Finalize an existing Vite build into the Suda theme artifact contract.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2329
2457
  const result = await finalizeTheme(resolveThemeRoot(options));
2330
- console.log(`finalized ${result.module.manifest.key}@${result.module.manifest.version}`);
2458
+ console.log(
2459
+ success(
2460
+ `finalized ${themeRef(result.module.manifest.key, result.module.manifest.version)}`
2461
+ )
2462
+ );
2331
2463
  });
2332
2464
  theme.command("check").description(
2333
2465
  "Run AI metadata checks on a built theme without rebuilding. Reports missing component-level ai.instructions as errors and weak content as warnings."