@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/index.js CHANGED
@@ -89,22 +89,22 @@ function createClient(opts = {}) {
89
89
  if (requireAuth && !token) {
90
90
  throw new Error("Not signed in. Run: feeef signin");
91
91
  }
92
- const http2 = createHttp(baseURL, token);
92
+ const http3 = createHttp(baseURL, token);
93
93
  const ff = new FeeeF({
94
94
  apiKey: token || "",
95
95
  baseURL,
96
- client: http2,
96
+ client: http3,
97
97
  cache: false
98
98
  });
99
- return { ff, http: http2, cfg, baseURL, token };
99
+ return { ff, http: http3, cfg, baseURL, token };
100
100
  }
101
101
  async function signinWithPassword(input) {
102
102
  const baseURL = (input.apiUrl || defaultApiUrl()).replace(/\/$/, "");
103
- const http2 = createHttp(baseURL, void 0);
103
+ const http3 = createHttp(baseURL, void 0);
104
104
  const ff = new FeeeF({
105
105
  apiKey: "",
106
106
  baseURL,
107
- client: http2,
107
+ client: http3,
108
108
  cache: false
109
109
  });
110
110
  const auth = await ff.users.signin({
@@ -604,14 +604,14 @@ function resolveThemeDir(dirArg) {
604
604
  if (dirArg) {
605
605
  const abs = path2.resolve(dirArg);
606
606
  if (!fs2.existsSync(abs)) {
607
- throw new Error(`Theme directory not found: ${abs}`);
607
+ throw new Error(`Template directory not found: ${abs}`);
608
608
  }
609
609
  return findThemeRoot(abs) || abs;
610
610
  }
611
611
  const fromCwd = findThemeRoot(process.cwd());
612
612
  if (fromCwd) return fromCwd;
613
613
  throw new Error(
614
- "No theme found. Run inside a theme folder (feeef.template.json) or: feeef template init"
614
+ "No template found. Run inside a template folder (feeef.template.json) or: feeef template init"
615
615
  );
616
616
  }
617
617
 
@@ -938,6 +938,13 @@ async function loadBuild() {
938
938
  }
939
939
  return import(pathToFileURL(buildPath).href);
940
940
  }
941
+ async function loadSmoke() {
942
+ const smokePath = path5.join(templateKitRoot(), "lib", "smoke.mjs");
943
+ if (!fs4.existsSync(smokePath)) {
944
+ throw new Error(`template-kit smoke missing at ${smokePath}`);
945
+ }
946
+ return import(pathToFileURL(smokePath).href);
947
+ }
941
948
  async function loadUnpack() {
942
949
  const unpackPath = path5.join(templateKitRoot(), "lib", "unpack.mjs");
943
950
  if (!fs4.existsSync(unpackPath)) {
@@ -957,7 +964,7 @@ async function runTemplateBuild(dirArg, opts = {}) {
957
964
  const srcDir = resolveThemeDir(dirArg);
958
965
  const outPath = path6.join(srcDir, "dist", "data.json");
959
966
  const s = spinner2();
960
- s.start(opts.checkOnly ? "Checking theme\u2026" : "Building theme\u2026");
967
+ s.start(opts.checkOnly ? "Checking template\u2026" : "Building template\u2026");
961
968
  const { buildTemplate } = await loadBuild();
962
969
  const result = buildTemplate({
963
970
  srcDir,
@@ -1010,7 +1017,7 @@ function assertStorefrontPath() {
1010
1017
  throw new Error(
1011
1018
  [
1012
1019
  "Lithium storefront not found.",
1013
- "Set it once (theme repos do not need storefront source inside them):",
1020
+ "Set it once (template repos do not need storefront source inside them):",
1014
1021
  " feeef config set storefrontPath /path/to/storefront",
1015
1022
  "or:",
1016
1023
  " export FEEEF_STOREFRONT_PATH=/path/to/storefront"
@@ -1020,6 +1027,85 @@ function assertStorefrontPath() {
1020
1027
  return root;
1021
1028
  }
1022
1029
 
1030
+ // src/dev-error-server.ts
1031
+ import http2 from "http";
1032
+
1033
+ // src/dev-error-format.ts
1034
+ function formatFeeefLiveErrorLine(body) {
1035
+ const debug = body.debug || {};
1036
+ const kind = String(body.kind || "error");
1037
+ const message = String(body.message || "unknown");
1038
+ const lines = [
1039
+ `[feeef:custom-live] ${kind}`,
1040
+ debug.sourcePath ? ` path: ${debug.sourcePath}` : null,
1041
+ debug.instanceId ? ` instanceId: ${debug.instanceId}` : null,
1042
+ debug.title ? ` title: ${debug.title}` : null,
1043
+ debug.type ? ` type: ${debug.type}` : null,
1044
+ ` message: ${message}`,
1045
+ body.props && typeof body.props === "object" ? ` props: ${JSON.stringify(body.props)}` : null,
1046
+ body.href ? ` href: ${body.href}` : null
1047
+ ].filter(Boolean);
1048
+ return lines.join("\n");
1049
+ }
1050
+
1051
+ // src/dev-error-server.ts
1052
+ async function startDevErrorServer(opts) {
1053
+ const preferred = opts?.preferredPort ?? (Number(process.env["FEEEF_DEV_ERROR_PORT"]) || 0);
1054
+ const server = http2.createServer((req, res) => {
1055
+ res.setHeader("Access-Control-Allow-Origin", "*");
1056
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1057
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1058
+ if (req.method === "OPTIONS") {
1059
+ res.writeHead(204);
1060
+ res.end();
1061
+ return;
1062
+ }
1063
+ if (req.method === "GET" && (req.url === "/" || req.url === "/health")) {
1064
+ res.writeHead(200, { "Content-Type": "text/plain" });
1065
+ res.end("feeef-dev-error-ok");
1066
+ return;
1067
+ }
1068
+ if (req.method === "POST" && req.url?.startsWith("/feeef-error")) {
1069
+ const chunks = [];
1070
+ req.on("data", (c) => chunks.push(c));
1071
+ req.on("end", () => {
1072
+ try {
1073
+ const raw = Buffer.concat(chunks).toString("utf8");
1074
+ const body = JSON.parse(raw);
1075
+ const line = formatFeeefLiveErrorLine(body);
1076
+ console.error("\n" + line + "\n");
1077
+ res.writeHead(204);
1078
+ res.end();
1079
+ } catch (e) {
1080
+ console.error("[feeef:custom-live] bad error payload", e);
1081
+ res.writeHead(400);
1082
+ res.end("bad json");
1083
+ }
1084
+ });
1085
+ return;
1086
+ }
1087
+ res.writeHead(404);
1088
+ res.end("not found");
1089
+ });
1090
+ await new Promise((resolve, reject) => {
1091
+ server.once("error", reject);
1092
+ server.listen(preferred, "127.0.0.1", () => resolve());
1093
+ });
1094
+ const addr = server.address();
1095
+ if (!addr || typeof addr === "string") {
1096
+ throw new Error("Failed to bind feeef dev error server");
1097
+ }
1098
+ const port = addr.port;
1099
+ const url = `http://127.0.0.1:${port}`;
1100
+ return {
1101
+ port,
1102
+ url,
1103
+ close: () => new Promise((resolve) => {
1104
+ server.close(() => resolve());
1105
+ })
1106
+ };
1107
+ }
1108
+
1023
1109
  // src/commands/template/dev.ts
1024
1110
  function killChildren(children) {
1025
1111
  for (const c of children) {
@@ -1053,7 +1139,7 @@ async function resolveBoundStore(srcDir) {
1053
1139
  });
1054
1140
  if (!storeRef) {
1055
1141
  throw new Error(
1056
- "No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live theme stays unchanged.)"
1142
+ "No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live template stays unchanged.)"
1057
1143
  );
1058
1144
  }
1059
1145
  const { ff } = createClient({ requireAuth: true });
@@ -1070,10 +1156,21 @@ async function runRemoteDraftPreview(srcDir) {
1070
1156
  const { storeId, slug } = await resolveBoundStore(srcDir);
1071
1157
  const { ff } = createClient({ requireAuth: true });
1072
1158
  const s = spinner2();
1073
- s.start("Building theme\u2026");
1159
+ s.start("Building template\u2026");
1074
1160
  const { buildTemplate } = await loadBuild();
1075
1161
  buildTemplate({ srcDir, outPath, write: true });
1076
1162
  s.stop("dist ready");
1163
+ const errorServer = await startDevErrorServer();
1164
+ info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
1165
+ const withDevLog = (previewUrl) => {
1166
+ try {
1167
+ const u = new URL(previewUrl);
1168
+ u.searchParams.set("feeef_dev_log", errorServer.url);
1169
+ return u.toString();
1170
+ } catch {
1171
+ return previewUrl;
1172
+ }
1173
+ };
1077
1174
  let lastPreviewUrl;
1078
1175
  let pushInFlight = false;
1079
1176
  let pushQueued = false;
@@ -1090,8 +1187,8 @@ async function runRemoteDraftPreview(srcDir) {
1090
1187
  }
1091
1188
  const data = JSON.parse(fs6.readFileSync(outPath, "utf8"));
1092
1189
  const result = await ff.stores.putTemplatePreview(storeId, { data });
1093
- lastPreviewUrl = result.previewUrl;
1094
- ok(`Draft preview updated \u2192 ${pc.cyan(result.previewUrl)}`);
1190
+ lastPreviewUrl = withDevLog(result.previewUrl);
1191
+ ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
1095
1192
  } catch (e) {
1096
1193
  fail(e);
1097
1194
  } finally {
@@ -1111,12 +1208,13 @@ async function runRemoteDraftPreview(srcDir) {
1111
1208
  }
1112
1209
  note2(
1113
1210
  [
1114
- `Theme ${srcDir}`,
1115
- `Store ${slug} (${storeId})`,
1116
- `Preview ${lastPreviewUrl ?? "(pending)"}`,
1211
+ `Template ${srcDir}`,
1212
+ `Store ${slug} (${storeId})`,
1213
+ `Preview ${lastPreviewUrl ?? "(pending)"}`,
1117
1214
  "",
1118
1215
  "Live customers still see metadata.templateData.",
1119
1216
  "Exit preview in the browser: banner \u2192 Exit, or /?preview=0",
1217
+ `CustomLive errors print here via ${errorServer.url}`,
1120
1218
  "Stopping the CLI clears the draft."
1121
1219
  ].join("\n"),
1122
1220
  "Remote draft preview"
@@ -1144,6 +1242,10 @@ async function runRemoteDraftPreview(srcDir) {
1144
1242
  watching = false;
1145
1243
  debouncedPush.cancel();
1146
1244
  killChildren([watch]);
1245
+ try {
1246
+ await errorServer.close();
1247
+ } catch {
1248
+ }
1147
1249
  try {
1148
1250
  await ff.stores.clearTemplatePreview(storeId);
1149
1251
  info("Cleared draft preview on server");
@@ -1173,6 +1275,8 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1173
1275
  process.env["FEEEF_STOREFRONT_PATH"] = path8.resolve(opts.storefront);
1174
1276
  }
1175
1277
  const storefrontRoot = assertStorefrontPath();
1278
+ const errorServer = await startDevErrorServer();
1279
+ info(`Component errors \u2192 ${pc.cyan(errorServer.url)} (terminal)`);
1176
1280
  const cfg = loadConfig();
1177
1281
  const storeRef = resolveStoreIdFromProject({
1178
1282
  themeRoot: srcDir,
@@ -1199,16 +1303,26 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1199
1303
  }
1200
1304
  }
1201
1305
  const s = spinner2();
1202
- s.start("Building theme\u2026");
1306
+ s.start("Building template\u2026");
1203
1307
  const { buildTemplate } = await loadBuild();
1204
1308
  buildTemplate({ srcDir, outPath, write: true });
1205
1309
  s.stop("dist ready");
1206
1310
  const previewHost = slug ? `http://${slug}.localhost.feeef.org:${port}` : `http://localhost:${port}`;
1311
+ const previewWithLog = (() => {
1312
+ try {
1313
+ const u = new URL(previewHost);
1314
+ u.searchParams.set("feeef_dev_log", errorServer.url);
1315
+ return u.toString();
1316
+ } catch {
1317
+ return previewHost;
1318
+ }
1319
+ })();
1207
1320
  note2(
1208
1321
  [
1209
- `Theme ${srcDir}`,
1322
+ `Template ${srcDir}`,
1210
1323
  `Storefront ${storefrontRoot}`,
1211
- `Preview ${previewHost}`,
1324
+ `Preview ${previewWithLog}`,
1325
+ `Errors ${errorServer.url}`,
1212
1326
  "Internal --local mode (not for public docs)."
1213
1327
  ].join("\n"),
1214
1328
  "Local Next preview"
@@ -1217,6 +1331,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1217
1331
  ...process.env,
1218
1332
  FEEEF_TEMPLATE_DATA_PATH: outPath,
1219
1333
  FEEEF_TEMPLATE_LIBRARY_PATH: libraryPath,
1334
+ FEEEF_DEV_LOG_URL: errorServer.url,
1220
1335
  PORT: String(port)
1221
1336
  };
1222
1337
  if (slug) {
@@ -1235,7 +1350,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1235
1350
  const children = [watch, next];
1236
1351
  const shutdown = (code = 0) => {
1237
1352
  killChildren(children);
1238
- process.exit(code);
1353
+ void errorServer.close().finally(() => process.exit(code));
1239
1354
  };
1240
1355
  process.on("SIGINT", () => shutdown(0));
1241
1356
  process.on("SIGTERM", () => shutdown(0));
@@ -1243,7 +1358,7 @@ async function runLocalStorefrontPreview(srcDir, opts) {
1243
1358
  watch.on("exit", (code) => {
1244
1359
  if (code && code !== 0) shutdown(code);
1245
1360
  });
1246
- ok(`Open ${pc.cyan(previewHost)}`);
1361
+ ok(`Open ${pc.cyan(previewWithLog)}`);
1247
1362
  outro2("Watching \u2014 Ctrl+C to stop");
1248
1363
  await new Promise(() => {
1249
1364
  });
@@ -1324,15 +1439,16 @@ function packageNameFromFolder(name) {
1324
1439
  return name.toLowerCase();
1325
1440
  }
1326
1441
  var DEFAULT_PACKAGE_JSON = {
1327
- name: "feeef-theme",
1442
+ name: "feeef-template",
1328
1443
  private: true,
1329
1444
  version: "0.1.0",
1330
- description: "Feeef Lithium theme",
1445
+ description: "Feeef Lithium template",
1331
1446
  type: "module",
1332
1447
  engines: { node: ">=18" },
1333
1448
  scripts: {
1334
1449
  build: "feeef template build",
1335
1450
  check: "feeef template check",
1451
+ test: "feeef template smoke",
1336
1452
  dev: "feeef template dev",
1337
1453
  publish: "feeef template publish",
1338
1454
  typecheck: "tsc --noEmit"
@@ -1448,7 +1564,9 @@ function ensureNodePackage(dest, folderName) {
1448
1564
  pkg.scripts = { ...DEFAULT_PACKAGE_JSON.scripts };
1449
1565
  }
1450
1566
  const scripts = pkg.scripts;
1451
- if (!scripts.typecheck) scripts.typecheck = "tsc --noEmit";
1567
+ for (const [key, value] of Object.entries(DEFAULT_PACKAGE_JSON.scripts)) {
1568
+ if (!scripts[key]) scripts[key] = value;
1569
+ }
1452
1570
  const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
1453
1571
  for (const [name, version] of Object.entries(EDITOR_DEV_DEPS)) {
1454
1572
  if (!devDeps[name]) devDeps[name] = version;
@@ -2028,7 +2146,7 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
2028
2146
  );
2029
2147
  const { unpackTemplate } = await loadUnpack();
2030
2148
  const unpackSpin = spinner2();
2031
- unpackSpin.start("Unpacking theme files\u2026");
2149
+ unpackSpin.start("Unpacking template files\u2026");
2032
2150
  const stats = unpackTemplate({
2033
2151
  inputPath: dataPath,
2034
2152
  outDir: dest,
@@ -2101,7 +2219,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2101
2219
  options: [
2102
2220
  {
2103
2221
  value: "blank",
2104
- label: "Blank theme",
2222
+ label: "Blank template",
2105
2223
  hint: resolveBlankKitGitUrl() ? "git clone official / monorepo kit" : "local scaffold"
2106
2224
  },
2107
2225
  {
@@ -2117,8 +2235,8 @@ async function runTemplateInit(nameArg, opts = {}) {
2117
2235
  ]
2118
2236
  });
2119
2237
  const name = nameArg || await text2({
2120
- message: "Theme folder name",
2121
- placeholder: "my-theme",
2238
+ message: "Template folder name",
2239
+ placeholder: "my-template",
2122
2240
  validate: validateFolderName
2123
2241
  });
2124
2242
  const dest = path12.resolve(process.cwd(), name);
@@ -2141,7 +2259,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2141
2259
  info(
2142
2260
  "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build"
2143
2261
  );
2144
- outro2("Theme scaffold ready");
2262
+ outro2("Template scaffold ready");
2145
2263
  return;
2146
2264
  }
2147
2265
  const { ff } = createClient({ requireAuth: false });
@@ -2159,7 +2277,7 @@ async function runTemplateInit(nameArg, opts = {}) {
2159
2277
  info(
2160
2278
  "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build\n npm run dev"
2161
2279
  );
2162
- outro2("Theme cloned and ready");
2280
+ outro2("Template cloned and ready");
2163
2281
  } catch (e) {
2164
2282
  fail(e);
2165
2283
  }
@@ -2242,7 +2360,7 @@ async function runUse(storeArg, opts) {
2242
2360
  const themeRoot = findThemeRoot(process.cwd());
2243
2361
  if (!themeRoot) {
2244
2362
  fail(
2245
- "No feeef.template.json nearby. Run from a theme folder or feeef template init first."
2363
+ "No feeef.template.json nearby. Run from a template folder or feeef template init first."
2246
2364
  );
2247
2365
  }
2248
2366
  const { ff } = createClient();
@@ -2271,7 +2389,7 @@ async function runUse(storeArg, opts) {
2271
2389
  if (opts.apiUrl) patch.apiUrl = opts.apiUrl;
2272
2390
  const { rcPath } = saveProjectRc(themeRoot, patch);
2273
2391
  saveConfig({ defaultStoreId: storeId });
2274
- ok(`Linked theme \u2192 store ${slug || storeId}`);
2392
+ ok(`Linked template \u2192 store ${slug || storeId}`);
2275
2393
  info(rcPath);
2276
2394
  outro2("Next: feeef template publish --apply");
2277
2395
  } catch (e) {
@@ -2292,7 +2410,7 @@ var SOURCE_TAG_PREFIX = "feeef-source:";
2292
2410
  async function runTemplatePublish(dirArg, opts) {
2293
2411
  intro2("feeef template publish");
2294
2412
  try {
2295
- const { ff, http: http2, cfg } = createClient();
2413
+ const { ff, http: http3, cfg } = createClient();
2296
2414
  const srcDir = resolveThemeDir(dirArg);
2297
2415
  const meta = loadTemplateMeta(srcDir);
2298
2416
  const rc = loadProjectRc(srcDir);
@@ -2314,7 +2432,7 @@ async function runTemplatePublish(dirArg, opts) {
2314
2432
  }
2315
2433
  const store = await ff.stores.find({ id: storeId });
2316
2434
  const title = opts.title || rc.title || meta.name || path13.basename(srcDir);
2317
- info(`Theme ${srcDir}`);
2435
+ info(`Template ${srcDir}`);
2318
2436
  info(`Store ${store.slug || storeId} (${policy})`);
2319
2437
  const outPath = path13.join(srcDir, "dist", "data.json");
2320
2438
  const s = spinner2();
@@ -2500,7 +2618,7 @@ async function runTemplatePublish(dirArg, opts) {
2500
2618
  if (typeof ff.storeTemplates.replaceLocales === "function") {
2501
2619
  await ff.storeTemplates.replaceLocales(templateId, locales);
2502
2620
  } else {
2503
- await http2.put(`/store_templates/${templateId}/locales`, { locales });
2621
+ await http3.put(`/store_templates/${templateId}/locales`, { locales });
2504
2622
  }
2505
2623
  s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
2506
2624
  }
@@ -2524,8 +2642,48 @@ async function runTemplatePublish(dirArg, opts) {
2524
2642
  }
2525
2643
  }
2526
2644
 
2527
- // src/commands/template/remove.ts
2645
+ // src/commands/template/smoke.ts
2528
2646
  import path14 from "path";
2647
+ async function runTemplateSmoke(dirArg, opts = {}) {
2648
+ intro2("feeef template smoke");
2649
+ try {
2650
+ const srcDir = resolveThemeDir(dirArg);
2651
+ const s = spinner2();
2652
+ s.start(
2653
+ opts.noBuild ? "Smoking custom components (existing dist)\u2026" : "Building + smoking custom components\u2026"
2654
+ );
2655
+ const { smokeTheme } = await loadSmoke();
2656
+ const result = await smokeTheme({
2657
+ srcDir,
2658
+ build: !opts.noBuild
2659
+ });
2660
+ s.stop(
2661
+ `Smoked ${result.total} custom component${result.total === 1 ? "" : "s"}`
2662
+ );
2663
+ for (const f of result.failed) {
2664
+ const label = f.item.sourcePath || f.item.instanceId || f.item.loc || "?";
2665
+ warn(`\u2717 ${label}`);
2666
+ console.error(pc.red(` ${f.error.message}`));
2667
+ }
2668
+ if (result.failed.length) {
2669
+ fail(
2670
+ new Error(
2671
+ `smoke failed \u2014 ${result.failed.length}/${result.total} components`
2672
+ )
2673
+ );
2674
+ return;
2675
+ }
2676
+ ok(
2677
+ `All ${result.passed} custom component${result.passed === 1 ? "" : "s"} rendered`
2678
+ );
2679
+ outro2(path14.relative(process.cwd(), srcDir) || srcDir);
2680
+ } catch (e) {
2681
+ fail(e);
2682
+ }
2683
+ }
2684
+
2685
+ // src/commands/template/remove.ts
2686
+ import path15 from "path";
2529
2687
  async function runTemplateRemovePage(pageId, opts = {}) {
2530
2688
  intro2("feeef template remove page");
2531
2689
  try {
@@ -2544,7 +2702,7 @@ async function runTemplateRemovePage(pageId, opts = {}) {
2544
2702
  }
2545
2703
  }
2546
2704
  const { pageId: id, removedPath } = removePage(themeRoot, pageId);
2547
- ok(`Removed ${path14.relative(themeRoot, removedPath)}`);
2705
+ ok(`Removed ${path15.relative(themeRoot, removedPath)}`);
2548
2706
  outro2(`Page "${id}" deleted`);
2549
2707
  } catch (err) {
2550
2708
  fail(err instanceof Error ? err.message : String(err));
@@ -2570,7 +2728,7 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
2570
2728
  const { removedPath } = removeComponent(themeRoot, pageId, componentId, {
2571
2729
  ...opts.section ? { section: opts.section } : {}
2572
2730
  });
2573
- ok(`Removed ${path14.relative(themeRoot, removedPath)}`);
2731
+ ok(`Removed ${path15.relative(themeRoot, removedPath)}`);
2574
2732
  outro2("Done");
2575
2733
  } catch (err) {
2576
2734
  fail(err instanceof Error ? err.message : String(err));
@@ -2579,12 +2737,12 @@ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
2579
2737
 
2580
2738
  // src/commands/template/watch.ts
2581
2739
  import { spawn as spawn2 } from "child_process";
2582
- import path15 from "path";
2740
+ import path16 from "path";
2583
2741
  async function runTemplateWatch(dirArg) {
2584
2742
  intro2("feeef template watch");
2585
2743
  try {
2586
2744
  const srcDir = resolveThemeDir(dirArg);
2587
- const outPath = path15.join(srcDir, "dist", "data.json");
2745
+ const outPath = path16.join(srcDir, "dist", "data.json");
2588
2746
  const s = spinner2();
2589
2747
  s.start("Initial build\u2026");
2590
2748
  const { buildTemplate } = await loadBuild();
@@ -2612,7 +2770,7 @@ async function runTemplateWatch(dirArg) {
2612
2770
  // src/index.ts
2613
2771
  function createProgram() {
2614
2772
  const program = new Command();
2615
- program.name("feeef").description("Feeef developer CLI \u2014 themes today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
2773
+ program.name("feeef").description("Feeef developer CLI \u2014 templates today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
2616
2774
  if (thisCommand.args.length === 0) return;
2617
2775
  banner();
2618
2776
  });
@@ -2640,18 +2798,18 @@ function createProgram() {
2640
2798
  runConfigSet(key, value);
2641
2799
  });
2642
2800
  program.command("use").description(
2643
- "Link theme to a store (.feeefrc); omit store to pick from your list"
2644
- ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this theme").action(async (store, opts) => {
2801
+ "Link template to a store (.feeefrc); omit store to pick from your list"
2802
+ ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
2645
2803
  await runUse(store, opts);
2646
2804
  });
2647
- 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) => {
2805
+ 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) => {
2648
2806
  await runTemplatePublish(dir, opts);
2649
2807
  });
2650
2808
  const template = program.command("template").description(
2651
- "Theme kit: init, add/remove page|component, build, check, watch, dev, publish"
2809
+ "Template kit: init, add/remove page|component, build, check, smoke, watch, dev, publish"
2652
2810
  );
2653
2811
  template.command("init").description(
2654
- "Scaffold a theme (blank, git kit, or public marketplace)"
2812
+ "Scaffold a template (blank, git kit, or public marketplace)"
2655
2813
  ).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) => {
2656
2814
  await runTemplateInit(name, {
2657
2815
  blank: opts.blank,
@@ -2660,8 +2818,8 @@ function createProgram() {
2660
2818
  git: opts.git
2661
2819
  });
2662
2820
  });
2663
- const templateAdd = template.command("add").description("Add a page or component to the current theme");
2664
- 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) => {
2821
+ const templateAdd = template.command("add").description("Add a page or component to the current template");
2822
+ 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) => {
2665
2823
  await runTemplateAddPage(pageId, { dir: opts.dir });
2666
2824
  });
2667
2825
  templateAdd.command("component").description(
@@ -2669,42 +2827,47 @@ function createProgram() {
2669
2827
  ).argument("<pageId>", "Page id").argument("<componentId>", "Component file id (e.g. hero)").option("--title <title>", "meta.title (default: Title Case of id)").option(
2670
2828
  "--section <id>",
2671
2829
  "Legacy: write under sections/<id>/components/ instead of flat"
2672
- ).option("--dir <path>", "Theme directory").action(async (pageId, componentId, opts) => {
2830
+ ).option("--dir <path>", "Template directory").action(async (pageId, componentId, opts) => {
2673
2831
  await runTemplateAddComponent(pageId, componentId, {
2674
2832
  dir: opts.dir,
2675
2833
  title: opts.title,
2676
2834
  section: opts.section
2677
2835
  });
2678
2836
  });
2679
- const templateRemove = template.command("remove").description("Remove a page or component from the current theme");
2680
- 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) => {
2837
+ const templateRemove = template.command("remove").description("Remove a page or component from the current template");
2838
+ 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) => {
2681
2839
  await runTemplateRemovePage(pageId, { dir: opts.dir, yes: opts.yes });
2682
2840
  });
2683
- 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) => {
2841
+ 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) => {
2684
2842
  await runTemplateRemoveComponent(pageId, componentId, {
2685
2843
  dir: opts.dir,
2686
2844
  section: opts.section,
2687
2845
  yes: opts.yes
2688
2846
  });
2689
2847
  });
2690
- template.command("build").description("Compile theme \u2192 dist/data.json").argument("[dir]", "Theme directory").action(async (dir) => {
2848
+ template.command("build").description("Compile template \u2192 dist/data.json").argument("[dir]", "Template directory").action(async (dir) => {
2691
2849
  await runTemplateBuild(dir);
2692
2850
  });
2693
- template.command("check").description("Validate theme without writing dist").argument("[dir]", "Theme directory").action(async (dir) => {
2851
+ template.command("check").description("Validate template without writing dist").argument("[dir]", "Template directory").action(async (dir) => {
2694
2852
  await runTemplateBuild(dir, { checkOnly: true });
2695
2853
  });
2696
- template.command("watch").description("Rebuild on file changes").argument("[dir]", "Theme directory").action(async (dir) => {
2854
+ template.command("smoke").description(
2855
+ "Build + SSR-eval every custom App() (catches broken components)"
2856
+ ).argument("[dir]", "Template directory").option("--no-build", "Use existing dist/data.json only").action(async (dir, opts) => {
2857
+ await runTemplateSmoke(dir, { noBuild: Boolean(opts.noBuild) });
2858
+ });
2859
+ template.command("watch").description("Rebuild on file changes").argument("[dir]", "Template directory").action(async (dir) => {
2697
2860
  await runTemplateWatch(dir);
2698
2861
  });
2699
2862
  template.command("dev").description(
2700
- "Watch + push draft preview to hosted storefront (never overwrites live theme)"
2701
- ).argument("[dir]", "Theme directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
2863
+ "Watch + push draft preview to hosted storefront (never overwrites live template)"
2864
+ ).argument("[dir]", "Template directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
2702
2865
  "--local",
2703
2866
  "Employee escape hatch: spawn local Next (requires storefront)"
2704
2867
  ).action(async (dir, opts) => {
2705
2868
  await runTemplateDev(dir, opts);
2706
2869
  });
2707
- 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) => {
2870
+ 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) => {
2708
2871
  await runTemplatePublish(dir, opts);
2709
2872
  });
2710
2873
  const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");