@sudajs/cli 0.9.4 → 0.10.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/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,6 +784,13 @@ 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)) {
@@ -862,6 +968,7 @@ var __testUtils = {
862
968
  slugifyThemeName,
863
969
  validateThemeKey,
864
970
  validateThemeName,
971
+ validateThemeLocales,
865
972
  toViteModuleUrl,
866
973
  updateRemotePageDraft
867
974
  };
@@ -872,20 +979,24 @@ async function runThemeCheck(theme) {
872
979
  const ok = result.ok && footerIssues.length === 0 && previewIssues.length === 0;
873
980
  const lines = [formatThemeCheckResult(result)];
874
981
  if (footerIssues.length === 0 && result.ok) {
875
- lines.push("Footer contract check passed: whiteLabel and ICP metadata render correctly.");
982
+ lines.push(
983
+ success("Footer contract check passed: whiteLabel and ICP metadata render correctly.")
984
+ );
876
985
  } else if (footerIssues.length > 0) {
877
986
  for (const issue of footerIssues) {
878
- lines.push(` error layoutConfig.footerContract: ${issue}`);
987
+ lines.push(` ${error("error")} layoutConfig.footerContract: ${issue}`);
879
988
  }
880
- lines.push(`Footer contract check: ${footerIssues.length} error(s).`);
989
+ lines.push(error(`Footer contract check: ${footerIssues.length} error(s).`));
881
990
  }
882
991
  if (previewIssues.length === 0) {
883
- lines.push("Preview contract check passed: desktop, tablet, and mobile screenshots exist.");
992
+ lines.push(
993
+ success("Preview contract check passed: desktop, tablet, and mobile screenshots exist.")
994
+ );
884
995
  } else {
885
996
  for (const issue of previewIssues) {
886
- lines.push(` error assets.preview: ${issue}`);
997
+ lines.push(` ${error("error")} assets.preview: ${issue}`);
887
998
  }
888
- lines.push(`Preview contract check: ${previewIssues.length} error(s).`);
999
+ lines.push(error(`Preview contract check: ${previewIssues.length} error(s).`));
889
1000
  }
890
1001
  const text = lines.join("\n");
891
1002
  if (ok) {
@@ -900,8 +1011,8 @@ async function runPreviewContractCheck(theme) {
900
1011
  for (const relativePath of REQUIRED_PREVIEW_ARTIFACTS) {
901
1012
  const filePath = path2.join(theme.root, "dist", relativePath);
902
1013
  try {
903
- const info = await stat(filePath);
904
- if (info.size <= 0) {
1014
+ const info2 = await stat(filePath);
1015
+ if (info2.size <= 0) {
905
1016
  issues.push(`${relativePath} is empty. Run \`suda theme capture\` and rebuild.`);
906
1017
  }
907
1018
  } catch {
@@ -912,7 +1023,7 @@ async function runPreviewContractCheck(theme) {
912
1023
  }
913
1024
  async function watchTheme(root, port) {
914
1025
  const handle = await startViteDevPreviewServer(root, port);
915
- console.log(`previewing Vite theme at ${handle.url}`);
1026
+ console.log(`previewing Vite theme at ${style.url(handle.url)}`);
916
1027
  await new Promise(() => void 0);
917
1028
  }
918
1029
  async function startViteDevPreviewServer(root, port) {
@@ -990,9 +1101,9 @@ function createSudaPreviewVitePlugin(root) {
990
1101
  );
991
1102
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
992
1103
  response.end(html);
993
- } catch (error) {
1104
+ } catch (error2) {
994
1105
  response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
995
- response.end(error instanceof Error ? error.stack : String(error));
1106
+ response.end(error2 instanceof Error ? error2.stack : String(error2));
996
1107
  }
997
1108
  })();
998
1109
  });
@@ -1238,7 +1349,7 @@ async function optimizePngIfAvailable(themeRoot, filePath) {
1238
1349
  const sharp = await loadOptionalSharp(themeRoot);
1239
1350
  if (!sharp) {
1240
1351
  if (!warnedMissingSharp) {
1241
- console.warn(formatMissingSharpWarning());
1352
+ console.warn(warning(formatMissingSharpWarning()));
1242
1353
  warnedMissingSharp = true;
1243
1354
  }
1244
1355
  return;
@@ -1249,7 +1360,7 @@ async function optimizePngIfAvailable(themeRoot, filePath) {
1249
1360
  } catch {
1250
1361
  if (!warnedSharpOptimizeFailure) {
1251
1362
  console.warn(
1252
- "warning: PNG optimization failed. The screenshot was saved uncompressed."
1363
+ warning("warning: PNG optimization failed. The screenshot was saved uncompressed.")
1253
1364
  );
1254
1365
  warnedSharpOptimizeFailure = true;
1255
1366
  }
@@ -1275,7 +1386,9 @@ async function captureScreenshot(root, options) {
1275
1386
  await context.close();
1276
1387
  }
1277
1388
  await optimizePngIfAvailable(root, target.outputPath);
1278
- console.log(`captured ${target.device} ${path2.relative(root, target.outputPath)}`);
1389
+ console.log(
1390
+ success(`captured ${target.device} ${style.path(path2.relative(root, target.outputPath))}`)
1391
+ );
1279
1392
  }
1280
1393
  } finally {
1281
1394
  await browser.close();
@@ -1491,8 +1604,8 @@ async function performConfirmedPageOperation(auth, input, fetcher = fetchJson) {
1491
1604
  };
1492
1605
  }
1493
1606
  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.`;
1607
+ const themeRef2 = input.themeVersion ? `${input.themeKey}@${input.themeVersion}` : input.themeKey;
1608
+ const impact = `This will activate theme "${themeRef2}" for project "${input.projectId}". Public pages will render with this theme after activation.`;
1496
1609
  if (input.confirm !== true) {
1497
1610
  return {
1498
1611
  status: "needs_confirmation",
@@ -2059,7 +2172,9 @@ async function publishTheme(root, skipBuild, force) {
2059
2172
  if (forceRes.ok) {
2060
2173
  const { deletedObjects } = await forceRes.json();
2061
2174
  console.log(
2062
- `force-cleared ${key}@${version} (${deletedObjects ?? 0} objects removed). Republishing...`
2175
+ warning(
2176
+ `force-cleared ${themeRef(key, version)} (${deletedObjects ?? 0} objects removed). Republishing...`
2177
+ )
2063
2178
  );
2064
2179
  }
2065
2180
  }
@@ -2098,7 +2213,9 @@ async function publishTheme(root, skipBuild, force) {
2098
2213
  if (!putRes.ok) {
2099
2214
  throw new Error(`Failed to upload ${file.relativePath}: ${putRes.statusText}`);
2100
2215
  }
2101
- console.log(`uploaded ${createThemeObjectKey(key, version, file.relativePath)}`);
2216
+ console.log(
2217
+ success(`uploaded ${style.path(createThemeObjectKey(key, version, file.relativePath))}`)
2218
+ );
2102
2219
  }
2103
2220
  const bundleArtifactRelative = "index.js";
2104
2221
  const previewArtifactRelative = "assets/preview/desktop.png";
@@ -2122,7 +2239,7 @@ async function publishTheme(root, skipBuild, force) {
2122
2239
  if (!completeRes.ok) {
2123
2240
  throw new Error(`Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`);
2124
2241
  }
2125
- console.log(`published ${key}@${version}`);
2242
+ console.log(success(`published ${themeRef(key, version)}`));
2126
2243
  }
2127
2244
  var cachedCliPackageVersions;
2128
2245
  async function readCliPackageVersions() {
@@ -2222,22 +2339,22 @@ function createThemeInitPrompt(nameProvided) {
2222
2339
  async function promptThemeName(rl) {
2223
2340
  while (true) {
2224
2341
  const answer = (await rl.question("Theme name: ")).trim();
2225
- const error = validateThemeName(answer);
2226
- if (error === null) {
2342
+ const error2 = validateThemeName(answer);
2343
+ if (error2 === null) {
2227
2344
  return answer;
2228
2345
  }
2229
- console.error(error);
2346
+ console.error(error(error2));
2230
2347
  }
2231
2348
  }
2232
2349
  async function promptThemeKey(rl, defaultKey) {
2233
2350
  while (true) {
2234
2351
  const answer = (await rl.question(`Theme key (${defaultKey}): `)).trim();
2235
2352
  const key = answer.length > 0 ? answer : defaultKey;
2236
- const error = validateThemeKey(key);
2237
- if (error === null) {
2353
+ const error2 = validateThemeKey(key);
2354
+ if (error2 === null) {
2238
2355
  return key;
2239
2356
  }
2240
- console.error(error);
2357
+ console.error(error(error2));
2241
2358
  }
2242
2359
  }
2243
2360
  async function resolveThemeInitInput(name) {
@@ -2298,7 +2415,7 @@ async function initThemeWithKey(target, key) {
2298
2415
  for (const relativePath of filesToReplace) {
2299
2416
  await replaceTemplateTokens(path2.join(target, relativePath), replacements);
2300
2417
  }
2301
- console.log(`created theme scaffold at ${target}`);
2418
+ console.log(success(`created theme scaffold at ${style.path(target)}`));
2302
2419
  }
2303
2420
  function buildProgram() {
2304
2421
  const program = new Command();
@@ -2310,16 +2427,22 @@ function buildProgram() {
2310
2427
  });
2311
2428
  theme.command("validate").description("Validate a built theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2312
2429
  const result = await validateTheme(resolveThemeRoot(options));
2313
- console.log(`valid ${result.module.manifest.key}@${result.module.manifest.version}`);
2430
+ console.log(
2431
+ success(`valid ${themeRef(result.module.manifest.key, result.module.manifest.version)}`)
2432
+ );
2314
2433
  if (!result.clientEntryPath) {
2315
2434
  console.warn(
2316
- "warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
2435
+ warning(
2436
+ "warning: runtime.client.js is missing; editor runtime will not load until `suda theme build` runs."
2437
+ )
2317
2438
  );
2318
2439
  }
2319
2440
  });
2320
2441
  theme.command("build").description("Build a Vite Suda theme artifact.").option("--theme-root <path>", "Theme source/artifact root.").action(async (options) => {
2321
2442
  const result = await buildTheme(resolveThemeRoot(options));
2322
- console.log(`built ${result.module.manifest.key}@${result.module.manifest.version}`);
2443
+ console.log(
2444
+ success(`built ${themeRef(result.module.manifest.key, result.module.manifest.version)}`)
2445
+ );
2323
2446
  const ok = await runThemeCheck(result);
2324
2447
  if (!ok) {
2325
2448
  throw new Error("AI metadata check failed. Fix the errors above.");
@@ -2327,7 +2450,11 @@ function buildProgram() {
2327
2450
  });
2328
2451
  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
2452
  const result = await finalizeTheme(resolveThemeRoot(options));
2330
- console.log(`finalized ${result.module.manifest.key}@${result.module.manifest.version}`);
2453
+ console.log(
2454
+ success(
2455
+ `finalized ${themeRef(result.module.manifest.key, result.module.manifest.version)}`
2456
+ )
2457
+ );
2331
2458
  });
2332
2459
  theme.command("check").description(
2333
2460
  "Run AI metadata checks on a built theme without rebuilding. Reports missing component-level ai.instructions as errors and weak content as warnings."