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