@feeef.dev/cli 0.2.0 → 0.2.2

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