@feeef.dev/cli 0.2.0 → 0.2.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/dist/bin.js CHANGED
@@ -94,6 +94,13 @@ async function loadBuild() {
94
94
  }
95
95
  return import(pathToFileURL(buildPath).href);
96
96
  }
97
+ async function loadSmoke() {
98
+ const smokePath = path2.join(templateKitRoot(), "lib", "smoke.mjs");
99
+ if (!fs2.existsSync(smokePath)) {
100
+ throw new Error(`template-kit smoke missing at ${smokePath}`);
101
+ }
102
+ return import(pathToFileURL(smokePath).href);
103
+ }
97
104
  async function loadUnpack() {
98
105
  const unpackPath = path2.join(templateKitRoot(), "lib", "unpack.mjs");
99
106
  if (!fs2.existsSync(unpackPath)) {
@@ -145,22 +152,22 @@ function createClient(opts = {}) {
145
152
  if (requireAuth && !token) {
146
153
  throw new Error("Not signed in. Run: feeef signin");
147
154
  }
148
- const http2 = createHttp(baseURL, token);
155
+ const http3 = createHttp(baseURL, token);
149
156
  const ff = new FeeeF({
150
157
  apiKey: token || "",
151
158
  baseURL,
152
- client: http2,
159
+ client: http3,
153
160
  cache: false
154
161
  });
155
- return { ff, http: http2, cfg, baseURL, token };
162
+ return { ff, http: http3, cfg, baseURL, token };
156
163
  }
157
164
  async function signinWithPassword(input) {
158
165
  const baseURL = (input.apiUrl || defaultApiUrl()).replace(/\/$/, "");
159
- const http2 = createHttp(baseURL, void 0);
166
+ const http3 = createHttp(baseURL, void 0);
160
167
  const ff = new FeeeF({
161
168
  apiKey: "",
162
169
  baseURL,
163
- client: http2,
170
+ client: http3,
164
171
  cache: false
165
172
  });
166
173
  const auth = await ff.users.signin({
@@ -660,14 +667,14 @@ function resolveThemeDir(dirArg) {
660
667
  if (dirArg) {
661
668
  const abs = path4.resolve(dirArg);
662
669
  if (!fs4.existsSync(abs)) {
663
- throw new Error(`Theme directory not found: ${abs}`);
670
+ throw new Error(`Template directory not found: ${abs}`);
664
671
  }
665
672
  return findThemeRoot(abs) || abs;
666
673
  }
667
674
  const fromCwd = findThemeRoot(process.cwd());
668
675
  if (fromCwd) return fromCwd;
669
676
  throw new Error(
670
- "No theme found. Run inside a theme folder (feeef.template.json) or: feeef template init"
677
+ "No template found. Run inside a template folder (feeef.template.json) or: feeef template init"
671
678
  );
672
679
  }
673
680
 
@@ -977,7 +984,7 @@ async function runTemplateBuild(dirArg, opts = {}) {
977
984
  const srcDir = resolveThemeDir(dirArg);
978
985
  const outPath = path7.join(srcDir, "dist", "data.json");
979
986
  const s = spinner2();
980
- s.start(opts.checkOnly ? "Checking theme\u2026" : "Building theme\u2026");
987
+ s.start(opts.checkOnly ? "Checking template\u2026" : "Building template\u2026");
981
988
  const { buildTemplate } = await loadBuild();
982
989
  const result = buildTemplate({
983
990
  srcDir,
@@ -1030,7 +1037,7 @@ function assertStorefrontPath() {
1030
1037
  throw new Error(
1031
1038
  [
1032
1039
  "Lithium storefront not found.",
1033
- "Set it once (theme repos do not need storefront source inside them):",
1040
+ "Set it once (template repos do not need storefront source inside them):",
1034
1041
  " feeef config set storefrontPath /path/to/storefront",
1035
1042
  "or:",
1036
1043
  " export FEEEF_STOREFRONT_PATH=/path/to/storefront"
@@ -1040,6 +1047,85 @@ function assertStorefrontPath() {
1040
1047
  return root;
1041
1048
  }
1042
1049
 
1050
+ // src/dev-error-server.ts
1051
+ import http2 from "http";
1052
+
1053
+ // src/dev-error-format.ts
1054
+ function formatFeeefLiveErrorLine(body) {
1055
+ const debug = body.debug || {};
1056
+ const kind = String(body.kind || "error");
1057
+ const message = String(body.message || "unknown");
1058
+ const lines = [
1059
+ `[feeef:custom-live] ${kind}`,
1060
+ debug.sourcePath ? ` path: ${debug.sourcePath}` : null,
1061
+ debug.instanceId ? ` instanceId: ${debug.instanceId}` : null,
1062
+ debug.title ? ` title: ${debug.title}` : null,
1063
+ debug.type ? ` type: ${debug.type}` : null,
1064
+ ` message: ${message}`,
1065
+ body.props && typeof body.props === "object" ? ` props: ${JSON.stringify(body.props)}` : null,
1066
+ body.href ? ` href: ${body.href}` : null
1067
+ ].filter(Boolean);
1068
+ return lines.join("\n");
1069
+ }
1070
+
1071
+ // src/dev-error-server.ts
1072
+ async function startDevErrorServer(opts) {
1073
+ const preferred = opts?.preferredPort ?? (Number(process.env["FEEEF_DEV_ERROR_PORT"]) || 0);
1074
+ const server = http2.createServer((req, res) => {
1075
+ res.setHeader("Access-Control-Allow-Origin", "*");
1076
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1077
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1078
+ if (req.method === "OPTIONS") {
1079
+ res.writeHead(204);
1080
+ res.end();
1081
+ return;
1082
+ }
1083
+ if (req.method === "GET" && (req.url === "/" || req.url === "/health")) {
1084
+ res.writeHead(200, { "Content-Type": "text/plain" });
1085
+ res.end("feeef-dev-error-ok");
1086
+ return;
1087
+ }
1088
+ if (req.method === "POST" && req.url?.startsWith("/feeef-error")) {
1089
+ const chunks = [];
1090
+ req.on("data", (c) => chunks.push(c));
1091
+ req.on("end", () => {
1092
+ try {
1093
+ const raw = Buffer.concat(chunks).toString("utf8");
1094
+ const body = JSON.parse(raw);
1095
+ const line = formatFeeefLiveErrorLine(body);
1096
+ console.error("\n" + line + "\n");
1097
+ res.writeHead(204);
1098
+ res.end();
1099
+ } catch (e) {
1100
+ console.error("[feeef:custom-live] bad error payload", e);
1101
+ res.writeHead(400);
1102
+ res.end("bad json");
1103
+ }
1104
+ });
1105
+ return;
1106
+ }
1107
+ res.writeHead(404);
1108
+ res.end("not found");
1109
+ });
1110
+ await new Promise((resolve, reject) => {
1111
+ server.once("error", reject);
1112
+ server.listen(preferred, "127.0.0.1", () => resolve());
1113
+ });
1114
+ const addr = server.address();
1115
+ if (!addr || typeof addr === "string") {
1116
+ throw new Error("Failed to bind feeef dev error server");
1117
+ }
1118
+ const port = addr.port;
1119
+ const url = `http://127.0.0.1:${port}`;
1120
+ return {
1121
+ port,
1122
+ url,
1123
+ close: () => new Promise((resolve) => {
1124
+ server.close(() => resolve());
1125
+ })
1126
+ };
1127
+ }
1128
+
1043
1129
  // src/commands/template/dev.ts
1044
1130
  function killChildren(children) {
1045
1131
  for (const c of children) {
@@ -1073,7 +1159,7 @@ async function resolveBoundStore(srcDir) {
1073
1159
  });
1074
1160
  if (!storeRef) {
1075
1161
  throw new Error(
1076
- "No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live theme stays unchanged.)"
1162
+ "No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live template stays unchanged.)"
1077
1163
  );
1078
1164
  }
1079
1165
  const { ff } = createClient({ requireAuth: true });
@@ -1090,10 +1176,21 @@ async function runRemoteDraftPreview(srcDir) {
1090
1176
  const { storeId, slug } = await resolveBoundStore(srcDir);
1091
1177
  const { ff } = createClient({ requireAuth: true });
1092
1178
  const s = spinner2();
1093
- s.start("Building theme\u2026");
1179
+ s.start("Building template\u2026");
1094
1180
  const { buildTemplate } = await loadBuild();
1095
1181
  buildTemplate({ srcDir, outPath, write: true });
1096
1182
  s.stop("dist ready");
1183
+ const errorServer = await startDevErrorServer();
1184
+ info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
1185
+ const withDevLog = (previewUrl) => {
1186
+ try {
1187
+ const u = new URL(previewUrl);
1188
+ u.searchParams.set("feeef_dev_log", errorServer.url);
1189
+ return u.toString();
1190
+ } catch {
1191
+ return previewUrl;
1192
+ }
1193
+ };
1097
1194
  let lastPreviewUrl;
1098
1195
  let pushInFlight = false;
1099
1196
  let pushQueued = false;
@@ -1110,8 +1207,8 @@ async function runRemoteDraftPreview(srcDir) {
1110
1207
  }
1111
1208
  const data = JSON.parse(fs7.readFileSync(outPath, "utf8"));
1112
1209
  const result = await ff.stores.putTemplatePreview(storeId, { data });
1113
- lastPreviewUrl = result.previewUrl;
1114
- ok(`Draft preview updated \u2192 ${pc.cyan(result.previewUrl)}`);
1210
+ lastPreviewUrl = withDevLog(result.previewUrl);
1211
+ ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
1115
1212
  } catch (e) {
1116
1213
  fail(e);
1117
1214
  } finally {
@@ -1131,12 +1228,13 @@ async function runRemoteDraftPreview(srcDir) {
1131
1228
  }
1132
1229
  note2(
1133
1230
  [
1134
- `Theme ${srcDir}`,
1135
- `Store ${slug} (${storeId})`,
1136
- `Preview ${lastPreviewUrl ?? "(pending)"}`,
1231
+ `Template ${srcDir}`,
1232
+ `Store ${slug} (${storeId})`,
1233
+ `Preview ${lastPreviewUrl ?? "(pending)"}`,
1137
1234
  "",
1138
1235
  "Live customers still see metadata.templateData.",
1139
1236
  "Exit preview in the browser: banner \u2192 Exit, or /?preview=0",
1237
+ `CustomLive errors print here via ${errorServer.url}`,
1140
1238
  "Stopping the CLI clears the draft."
1141
1239
  ].join("\n"),
1142
1240
  "Remote draft preview"
@@ -1164,6 +1262,10 @@ async function runRemoteDraftPreview(srcDir) {
1164
1262
  watching = false;
1165
1263
  debouncedPush.cancel();
1166
1264
  killChildren([watch]);
1265
+ try {
1266
+ await errorServer.close();
1267
+ } catch {
1268
+ }
1167
1269
  try {
1168
1270
  await ff.stores.clearTemplatePreview(storeId);
1169
1271
  info("Cleared draft preview on server");
@@ -1193,6 +1295,8 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1193
1295
  process.env["FEEEF_STOREFRONT_PATH"] = path9.resolve(opts.storefront);
1194
1296
  }
1195
1297
  const storefrontRoot = assertStorefrontPath();
1298
+ const errorServer = await startDevErrorServer();
1299
+ info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
1196
1300
  const cfg = loadConfig();
1197
1301
  const storeRef = resolveStoreIdFromProject({
1198
1302
  themeRoot: srcDir,
@@ -1219,16 +1323,26 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1219
1323
  }
1220
1324
  }
1221
1325
  const s = spinner2();
1222
- s.start("Building theme\u2026");
1326
+ s.start("Building template\u2026");
1223
1327
  const { buildTemplate } = await loadBuild();
1224
1328
  buildTemplate({ srcDir, outPath, write: true });
1225
1329
  s.stop("dist ready");
1226
1330
  const previewHost = slug ? `http://${slug}.localhost.feeef.org:${port}` : `http://localhost:${port}`;
1331
+ const previewWithLog = (() => {
1332
+ try {
1333
+ const u = new URL(previewHost);
1334
+ u.searchParams.set("feeef_dev_log", errorServer.url);
1335
+ return u.toString();
1336
+ } catch {
1337
+ return previewHost;
1338
+ }
1339
+ })();
1227
1340
  note2(
1228
1341
  [
1229
- `Theme ${srcDir}`,
1342
+ `Template ${srcDir}`,
1230
1343
  `Storefront ${storefrontRoot}`,
1231
- `Preview ${previewHost}`,
1344
+ `Preview ${previewWithLog}`,
1345
+ `Errors ${errorServer.url}`,
1232
1346
  "Internal --local mode (not for public docs)."
1233
1347
  ].join("\n"),
1234
1348
  "Local Next preview"
@@ -1237,6 +1351,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1237
1351
  ...process.env,
1238
1352
  FEEEF_TEMPLATE_DATA_PATH: outPath,
1239
1353
  FEEEF_TEMPLATE_LIBRARY_PATH: libraryPath,
1354
+ FEEEF_DEV_LOG_URL: errorServer.url,
1240
1355
  PORT: String(port)
1241
1356
  };
1242
1357
  if (slug) {
@@ -1255,7 +1370,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1255
1370
  const children = [watch, next];
1256
1371
  const shutdown = (code = 0) => {
1257
1372
  killChildren(children);
1258
- process.exit(code);
1373
+ void errorServer.close().finally(() => process.exit(code));
1259
1374
  };
1260
1375
  process.on("SIGINT", () => shutdown(0));
1261
1376
  process.on("SIGTERM", () => shutdown(0));
@@ -1263,7 +1378,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1263
1378
  watch.on("exit", (code) => {
1264
1379
  if (code && code !== 0) shutdown(code);
1265
1380
  });
1266
- ok(`Open ${pc.cyan(previewHost)}`);
1381
+ ok(`Open ${pc.cyan(previewWithLog)}`);
1267
1382
  outro2("Watching \u2014 Ctrl+C to stop");
1268
1383
  await new Promise(() => {
1269
1384
  });
@@ -1344,15 +1459,16 @@ function packageNameFromFolder(name) {
1344
1459
  return name.toLowerCase();
1345
1460
  }
1346
1461
  var DEFAULT_PACKAGE_JSON = {
1347
- name: "feeef-theme",
1462
+ name: "feeef-template",
1348
1463
  private: true,
1349
1464
  version: "0.1.0",
1350
- description: "Feeef Lithium theme",
1465
+ description: "Feeef Lithium template",
1351
1466
  type: "module",
1352
1467
  engines: { node: ">=18" },
1353
1468
  scripts: {
1354
1469
  build: "feeef template build",
1355
1470
  check: "feeef template check",
1471
+ test: "feeef template smoke",
1356
1472
  dev: "feeef template dev",
1357
1473
  publish: "feeef template publish",
1358
1474
  typecheck: "tsc --noEmit"
@@ -1468,7 +1584,9 @@ function ensureNodePackage(dest, folderName) {
1468
1584
  pkg.scripts = { ...DEFAULT_PACKAGE_JSON.scripts };
1469
1585
  }
1470
1586
  const scripts = pkg.scripts;
1471
- if (!scripts.typecheck) scripts.typecheck = "tsc --noEmit";
1587
+ for (const [key, value] of Object.entries(DEFAULT_PACKAGE_JSON.scripts)) {
1588
+ if (!scripts[key]) scripts[key] = value;
1589
+ }
1472
1590
  const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
1473
1591
  for (const [name, version] of Object.entries(EDITOR_DEV_DEPS)) {
1474
1592
  if (!devDeps[name]) devDeps[name] = version;
@@ -2048,7 +2166,7 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
2048
2166
  );
2049
2167
  const { unpackTemplate } = await loadUnpack();
2050
2168
  const unpackSpin = spinner2();
2051
- unpackSpin.start("Unpacking theme files\u2026");
2169
+ unpackSpin.start("Unpacking template files\u2026");
2052
2170
  const stats = unpackTemplate({
2053
2171
  inputPath: dataPath,
2054
2172
  outDir: dest,
@@ -2121,7 +2239,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2121
2239
  options: [
2122
2240
  {
2123
2241
  value: "blank",
2124
- label: "Blank theme",
2242
+ label: "Blank template",
2125
2243
  hint: resolveBlankKitGitUrl() ? "git clone official / monorepo kit" : "local scaffold"
2126
2244
  },
2127
2245
  {
@@ -2137,8 +2255,8 @@ async function runTemplateInit(nameArg, opts = {}) {
2137
2255
  ]
2138
2256
  });
2139
2257
  const name = nameArg || await text2({
2140
- message: "Theme folder name",
2141
- placeholder: "my-theme",
2258
+ message: "Template folder name",
2259
+ placeholder: "my-template",
2142
2260
  validate: validateFolderName
2143
2261
  });
2144
2262
  const dest = path13.resolve(process.cwd(), name);
@@ -2161,7 +2279,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2161
2279
  info(
2162
2280
  "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build"
2163
2281
  );
2164
- outro2("Theme scaffold ready");
2282
+ outro2("Template scaffold ready");
2165
2283
  return;
2166
2284
  }
2167
2285
  const { ff } = createClient({ requireAuth: false });
@@ -2179,7 +2297,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2179
2297
  info(
2180
2298
  "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build\n npm run dev"
2181
2299
  );
2182
- outro2("Theme cloned and ready");
2300
+ outro2("Template cloned and ready");
2183
2301
  } catch (e) {
2184
2302
  fail(e);
2185
2303
  }
@@ -2262,7 +2380,7 @@ async function runUse(storeArg, opts) {
2262
2380
  const themeRoot = findThemeRoot(process.cwd());
2263
2381
  if (!themeRoot) {
2264
2382
  fail(
2265
- "No feeef.template.json nearby. Run from a theme folder or feeef template init first."
2383
+ "No feeef.template.json nearby. Run from a template folder or feeef template init first."
2266
2384
  );
2267
2385
  }
2268
2386
  const { ff } = createClient();
@@ -2291,7 +2409,7 @@ async function runUse(storeArg, opts) {
2291
2409
  if (opts.apiUrl) patch.apiUrl = opts.apiUrl;
2292
2410
  const { rcPath } = saveProjectRc(themeRoot, patch);
2293
2411
  saveConfig({ defaultStoreId: storeId });
2294
- ok(`Linked theme \u2192 store ${slug || storeId}`);
2412
+ ok(`Linked template \u2192 store ${slug || storeId}`);
2295
2413
  info(rcPath);
2296
2414
  outro2("Next: feeef template publish --apply");
2297
2415
  } catch (e) {
@@ -2312,7 +2430,7 @@ var SOURCE_TAG_PREFIX = "feeef-source:";
2312
2430
  async function runTemplatePublish(dirArg, opts) {
2313
2431
  intro2("feeef template publish");
2314
2432
  try {
2315
- const { ff, http: http2, cfg } = createClient();
2433
+ const { ff, http: http3, cfg } = createClient();
2316
2434
  const srcDir = resolveThemeDir(dirArg);
2317
2435
  const meta = loadTemplateMeta(srcDir);
2318
2436
  const rc = loadProjectRc(srcDir);
@@ -2334,7 +2452,7 @@ async function runTemplatePublish(dirArg, opts) {
2334
2452
  }
2335
2453
  const store = await ff.stores.find({ id: storeId });
2336
2454
  const title = opts.title || rc.title || meta.name || path14.basename(srcDir);
2337
- info(`Theme ${srcDir}`);
2455
+ info(`Template ${srcDir}`);
2338
2456
  info(`Store ${store.slug || storeId} (${policy})`);
2339
2457
  const outPath = path14.join(srcDir, "dist", "data.json");
2340
2458
  const s = spinner2();
@@ -2520,7 +2638,7 @@ async function runTemplatePublish(dirArg, opts) {
2520
2638
  if (typeof ff.storeTemplates.replaceLocales === "function") {
2521
2639
  await ff.storeTemplates.replaceLocales(templateId, locales);
2522
2640
  } else {
2523
- await http2.put(`/store_templates/${templateId}/locales`, { locales });
2641
+ await http3.put(`/store_templates/${templateId}/locales`, { locales });
2524
2642
  }
2525
2643
  s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
2526
2644
  }
@@ -2544,8 +2662,48 @@ async function runTemplatePublish(dirArg, opts) {
2544
2662
  }
2545
2663
  }
2546
2664
 
2547
- // src/commands/template/remove.ts
2665
+ // src/commands/template/smoke.ts
2548
2666
  import path15 from "path";
2667
+ async function runTemplateSmoke(dirArg, opts = {}) {
2668
+ intro2("feeef template smoke");
2669
+ try {
2670
+ const srcDir = resolveThemeDir(dirArg);
2671
+ const s = spinner2();
2672
+ s.start(
2673
+ opts.noBuild ? "Smoking custom components (existing dist)\u2026" : "Building + smoking custom components\u2026"
2674
+ );
2675
+ const { smokeTheme } = await loadSmoke();
2676
+ const result = await smokeTheme({
2677
+ srcDir,
2678
+ build: !opts.noBuild
2679
+ });
2680
+ s.stop(
2681
+ `Smoked ${result.total} custom component${result.total === 1 ? "" : "s"}`
2682
+ );
2683
+ for (const f of result.failed) {
2684
+ const label = f.item.sourcePath || f.item.instanceId || f.item.loc || "?";
2685
+ warn(`\u2717 ${label}`);
2686
+ console.error(pc.red(` ${f.error.message}`));
2687
+ }
2688
+ if (result.failed.length) {
2689
+ fail(
2690
+ new Error(
2691
+ `smoke failed \u2014 ${result.failed.length}/${result.total} components`
2692
+ )
2693
+ );
2694
+ return;
2695
+ }
2696
+ ok(
2697
+ `All ${result.passed} custom component${result.passed === 1 ? "" : "s"} rendered`
2698
+ );
2699
+ outro2(path15.relative(process.cwd(), srcDir) || srcDir);
2700
+ } catch (e) {
2701
+ fail(e);
2702
+ }
2703
+ }
2704
+
2705
+ // src/commands/template/remove.ts
2706
+ import path16 from "path";
2549
2707
  async function runTemplateRemovePage(pageId, opts = {}) {
2550
2708
  intro2("feeef template remove page");
2551
2709
  try {
@@ -2564,7 +2722,7 @@ async function runTemplateRemovePage(pageId, opts = {}) {
2564
2722
  }
2565
2723
  }
2566
2724
  const { pageId: id, removedPath } = removePage(themeRoot, pageId);
2567
- ok(`Removed ${path15.relative(themeRoot, removedPath)}`);
2725
+ ok(`Removed ${path16.relative(themeRoot, removedPath)}`);
2568
2726
  outro2(`Page "${id}" deleted`);
2569
2727
  } catch (err) {
2570
2728
  fail(err instanceof Error ? err.message : String(err));
@@ -2590,7 +2748,7 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
2590
2748
  const { removedPath } = removeComponent(themeRoot, pageId, componentId, {
2591
2749
  ...opts.section ? { section: opts.section } : {}
2592
2750
  });
2593
- ok(`Removed ${path15.relative(themeRoot, removedPath)}`);
2751
+ ok(`Removed ${path16.relative(themeRoot, removedPath)}`);
2594
2752
  outro2("Done");
2595
2753
  } catch (err) {
2596
2754
  fail(err instanceof Error ? err.message : String(err));
@@ -2599,12 +2757,12 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
2599
2757
 
2600
2758
  // src/commands/template/watch.ts
2601
2759
  import { spawn as spawn2 } from "child_process";
2602
- import path16 from "path";
2760
+ import path17 from "path";
2603
2761
  async function runTemplateWatch(dirArg) {
2604
2762
  intro2("feeef template watch");
2605
2763
  try {
2606
2764
  const srcDir = resolveThemeDir(dirArg);
2607
- const outPath = path16.join(srcDir, "dist", "data.json");
2765
+ const outPath = path17.join(srcDir, "dist", "data.json");
2608
2766
  const s = spinner2();
2609
2767
  s.start("Initial build\u2026");
2610
2768
  const { buildTemplate } = await loadBuild();
@@ -2632,7 +2790,7 @@ async function runTemplateWatch(dirArg) {
2632
2790
  // src/index.ts
2633
2791
  function createProgram() {
2634
2792
  const program = new Command();
2635
- program.name("feeef").description("Feeef developer CLI \u2014 themes today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
2793
+ program.name("feeef").description("Feeef developer CLI \u2014 templates today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
2636
2794
  if (thisCommand.args.length === 0) return;
2637
2795
  banner();
2638
2796
  });
@@ -2660,18 +2818,18 @@ function createProgram() {
2660
2818
  runConfigSet(key, value);
2661
2819
  });
2662
2820
  program.command("use").description(
2663
- "Link theme to a store (.feeefrc); omit store to pick from your list"
2664
- ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this theme").action(async (store, opts) => {
2821
+ "Link template to a store (.feeefrc); omit store to pick from your list"
2822
+ ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
2665
2823
  await runUse(store, opts);
2666
2824
  });
2667
- program.command("publish").description("Build + publish current theme (alias of template publish)").argument("[dir]", "Theme directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2825
+ program.command("publish").description("Build + publish current template (alias of template publish)").argument("[dir]", "Template directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2668
2826
  await runTemplatePublish(dir, opts);
2669
2827
  });
2670
2828
  const template = program.command("template").description(
2671
- "Theme kit: init, add/remove page|component, build, check, watch, dev, publish"
2829
+ "Template kit: init, add/remove page|component, build, check, smoke, watch, dev, publish"
2672
2830
  );
2673
2831
  template.command("init").description(
2674
- "Scaffold a theme (blank, git kit, or public marketplace)"
2832
+ "Scaffold a template (blank, git kit, or public marketplace)"
2675
2833
  ).argument("[name]", "Folder name").option("--blank", "Use the blank scaffold (skip picker)").option("--from <templateId>", "Clone a public template by id").option("--git <url>", "Clone a kit repo (depth 1), Adonis/Laravel style").option("-q, --search <query>", "Pre-fill marketplace search").action(async (name, opts) => {
2676
2834
  await runTemplateInit(name, {
2677
2835
  blank: opts.blank,
@@ -2680,8 +2838,8 @@ function createProgram() {
2680
2838
  git: opts.git
2681
2839
  });
2682
2840
  });
2683
- const templateAdd = template.command("add").description("Add a page or component to the current theme");
2684
- templateAdd.command("page").description("Create pages/<pageId>/ with flat components/ + update manifest").argument("<pageId>", "Page id (e.g. home, checkout, thank_you)").option("--dir <path>", "Theme directory").action(async (pageId, opts) => {
2841
+ const templateAdd = template.command("add").description("Add a page or component to the current template");
2842
+ templateAdd.command("page").description("Create pages/<pageId>/ with flat components/ + update manifest").argument("<pageId>", "Page id (e.g. home, checkout, thank_you)").option("--dir <path>", "Template directory").action(async (pageId, opts) => {
2685
2843
  await runTemplateAddPage(pageId, { dir: opts.dir });
2686
2844
  });
2687
2845
  templateAdd.command("component").description(
@@ -2689,42 +2847,47 @@ function createProgram() {
2689
2847
  ).argument("<pageId>", "Page id").argument("<componentId>", "Component file id (e.g. hero)").option("--title <title>", "meta.title (default: Title Case of id)").option(
2690
2848
  "--section <id>",
2691
2849
  "Legacy: write under sections/<id>/components/ instead of flat"
2692
- ).option("--dir <path>", "Theme directory").action(async (pageId, componentId, opts) => {
2850
+ ).option("--dir <path>", "Template directory").action(async (pageId, componentId, opts) => {
2693
2851
  await runTemplateAddComponent(pageId, componentId, {
2694
2852
  dir: opts.dir,
2695
2853
  title: opts.title,
2696
2854
  section: opts.section
2697
2855
  });
2698
2856
  });
2699
- const templateRemove = template.command("remove").description("Remove a page or component from the current theme");
2700
- templateRemove.command("page").description("Delete pages/<pageId>/ and drop it from feeef.template.json").argument("<pageId>", "Page id").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Theme directory").action(async (pageId, opts) => {
2857
+ const templateRemove = template.command("remove").description("Remove a page or component from the current template");
2858
+ templateRemove.command("page").description("Delete pages/<pageId>/ and drop it from feeef.template.json").argument("<pageId>", "Page id").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Template directory").action(async (pageId, opts) => {
2701
2859
  await runTemplateRemovePage(pageId, { dir: opts.dir, yes: opts.yes });
2702
2860
  });
2703
- templateRemove.command("component").description("Delete a component file/folder under a page").argument("<pageId>", "Page id").argument("<componentId>", "Component id").option("--section <id>", "Legacy: only search sections/<id>/components/").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Theme directory").action(async (pageId, componentId, opts) => {
2861
+ templateRemove.command("component").description("Delete a component file/folder under a page").argument("<pageId>", "Page id").argument("<componentId>", "Component id").option("--section <id>", "Legacy: only search sections/<id>/components/").option("-y, --yes", "Skip confirmation").option("--dir <path>", "Template directory").action(async (pageId, componentId, opts) => {
2704
2862
  await runTemplateRemoveComponent(pageId, componentId, {
2705
2863
  dir: opts.dir,
2706
2864
  section: opts.section,
2707
2865
  yes: opts.yes
2708
2866
  });
2709
2867
  });
2710
- template.command("build").description("Compile theme \u2192 dist/data.json").argument("[dir]", "Theme directory").action(async (dir) => {
2868
+ template.command("build").description("Compile template \u2192 dist/data.json").argument("[dir]", "Template directory").action(async (dir) => {
2711
2869
  await runTemplateBuild(dir);
2712
2870
  });
2713
- template.command("check").description("Validate theme without writing dist").argument("[dir]", "Theme directory").action(async (dir) => {
2871
+ template.command("check").description("Validate template without writing dist").argument("[dir]", "Template directory").action(async (dir) => {
2714
2872
  await runTemplateBuild(dir, { checkOnly: true });
2715
2873
  });
2716
- template.command("watch").description("Rebuild on file changes").argument("[dir]", "Theme directory").action(async (dir) => {
2874
+ template.command("smoke").description(
2875
+ "Build + SSR-eval every custom App() (catches broken components)"
2876
+ ).argument("[dir]", "Template directory").option("--no-build", "Use existing dist/data.json only").action(async (dir, opts) => {
2877
+ await runTemplateSmoke(dir, { noBuild: Boolean(opts.noBuild) });
2878
+ });
2879
+ template.command("watch").description("Rebuild on file changes").argument("[dir]", "Template directory").action(async (dir) => {
2717
2880
  await runTemplateWatch(dir);
2718
2881
  });
2719
2882
  template.command("dev").description(
2720
- "Watch + push draft preview to hosted storefront (never overwrites live theme)"
2721
- ).argument("[dir]", "Theme directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
2883
+ "Watch + push draft preview to hosted storefront (never overwrites live template)"
2884
+ ).argument("[dir]", "Template directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
2722
2885
  "--local",
2723
2886
  "Employee escape hatch: spawn local Next (requires storefront)"
2724
2887
  ).action(async (dir, opts) => {
2725
2888
  await runTemplateDev(dir, opts);
2726
2889
  });
2727
- template.command("publish").description("Build, upload components + template + locales, optional install").argument("[dir]", "Theme directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2890
+ template.command("publish").description("Build, upload components + template + locales, optional install").argument("[dir]", "Template directory").option("--public", "Policy public (default private)").option("--unlisted", "Policy unlisted").option("--store <id>", "Owner store id").option("--title <title>", "Marketplace title").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2728
2891
  await runTemplatePublish(dir, opts);
2729
2892
  });
2730
2893
  const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");