@feeef.dev/cli 0.2.2 → 0.2.3

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
@@ -737,7 +737,13 @@ function nextComponentOrder(parentDir) {
737
737
  if (fs3.statSync(abs).isFile() && /\.(tsx|jsx)$/.test(name)) {
738
738
  sourcePath = abs;
739
739
  } else if (fs3.statSync(abs).isDirectory()) {
740
- for (const entry of ["component.tsx", "component.jsx"]) {
740
+ const id = name;
741
+ for (const entry of [
742
+ `${id}.tsx`,
743
+ `${id}.jsx`,
744
+ "component.tsx",
745
+ "component.jsx"
746
+ ]) {
741
747
  const p2 = path3.join(abs, entry);
742
748
  if (fs3.existsSync(p2)) {
743
749
  sourcePath = p2;
@@ -1152,10 +1158,54 @@ async function resolveBoundStore(srcDir) {
1152
1158
  }
1153
1159
  return { storeId: store.id, slug: store.slug };
1154
1160
  }
1161
+ function isLoopbackHost(hostname) {
1162
+ const h = hostname.toLowerCase();
1163
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
1164
+ }
1165
+ function assertRemotePreviewApiMatch(apiBase, previewUrl) {
1166
+ if (process.env["FEEEF_ALLOW_LOCAL_API_REMOTE_PREVIEW"] === "1") return;
1167
+ let apiHost = "";
1168
+ let previewHost = "";
1169
+ try {
1170
+ apiHost = new URL(apiBase).hostname;
1171
+ } catch {
1172
+ return;
1173
+ }
1174
+ try {
1175
+ previewHost = new URL(previewUrl).hostname;
1176
+ } catch {
1177
+ return;
1178
+ }
1179
+ if (isLoopbackHost(apiHost) && !isLoopbackHost(previewHost)) {
1180
+ throw new Error(
1181
+ [
1182
+ "Draft preview mismatch: CLI API is local, but preview URL is hosted.",
1183
+ ` API ${apiBase}`,
1184
+ ` Preview ${previewUrl}`,
1185
+ "",
1186
+ "Hosted shops load drafts from production API \u2014 your local PUT never reaches them,",
1187
+ "so the page shows the live theme.",
1188
+ "",
1189
+ "Fix:",
1190
+ " feeef config set apiUrl https://api.feeef.org/v1",
1191
+ " feeef signin",
1192
+ " feeef template dev",
1193
+ "",
1194
+ "Or use local Next: feeef template dev --local",
1195
+ "(override: FEEEF_ALLOW_LOCAL_API_REMOTE_PREVIEW=1)"
1196
+ ].join("\n")
1197
+ );
1198
+ }
1199
+ }
1155
1200
  async function runRemoteDraftPreview(srcDir) {
1156
1201
  const outPath = path8.join(srcDir, "dist", "data.json");
1157
1202
  const { storeId, slug } = await resolveBoundStore(srcDir);
1158
- const { ff } = createClient({ requireAuth: true });
1203
+ const { ff, baseURL } = createClient({ requireAuth: true });
1204
+ if (isLoopbackHost(new URL(baseURL).hostname)) {
1205
+ warn(
1206
+ `API is ${baseURL} \u2014 hosted ?preview= will only work if that API is what the shop calls.`
1207
+ );
1208
+ }
1159
1209
  const s = spinner2();
1160
1210
  s.start("Building template\u2026");
1161
1211
  const { buildTemplate } = await loadBuild();
@@ -1188,6 +1238,7 @@ async function runRemoteDraftPreview(srcDir) {
1188
1238
  }
1189
1239
  const data = JSON.parse(fs6.readFileSync(outPath, "utf8"));
1190
1240
  const result = await ff.stores.putTemplatePreview(storeId, { data });
1241
+ assertRemotePreviewApiMatch(baseURL, result.previewUrl);
1191
1242
  lastPreviewUrl = withDevLog(result.previewUrl);
1192
1243
  ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
1193
1244
  } catch (e) {
@@ -1368,7 +1419,25 @@ async function runTemplateDev(dirArg, opts) {
1368
1419
  intro2("feeef template dev");
1369
1420
  try {
1370
1421
  const srcDir = resolveThemeDir(dirArg);
1371
- if (opts.local || opts.storefront) {
1422
+ let useLocal = Boolean(opts.local || opts.storefront);
1423
+ if (!useLocal) {
1424
+ const cfg = loadConfig();
1425
+ let apiHost = "";
1426
+ try {
1427
+ apiHost = new URL(defaultApiUrl(cfg)).hostname;
1428
+ } catch {
1429
+ }
1430
+ const hasStorefront = Boolean(
1431
+ opts.storefront || cfg.storefrontPath || process.env["FEEEF_STOREFRONT_PATH"]
1432
+ );
1433
+ if (isLoopbackHost(apiHost) && hasStorefront) {
1434
+ warn(
1435
+ "Local API + storefrontPath detected \u2192 using --local (hosted ?preview= would show the live theme)."
1436
+ );
1437
+ useLocal = true;
1438
+ }
1439
+ }
1440
+ if (useLocal) {
1372
1441
  await runLocalStorefrontPreview(srcDir, opts);
1373
1442
  return;
1374
1443
  }
@@ -1633,10 +1702,16 @@ function collectPublishableComponents(srcDir) {
1633
1702
  const entries = [...byId.entries()].map(([id, entryPath]) => ({ id, entryPath })).sort((a, b) => a.id.localeCompare(b.id));
1634
1703
  for (const { id: name, entryPath } of entries) {
1635
1704
  const isDir = fs9.statSync(entryPath).isDirectory();
1636
- const metaPath = isDir ? path11.join(entryPath, "component.json") : entryPath.endsWith(".json") ? entryPath : null;
1637
- const tsxPath = isDir ? path11.join(entryPath, "component.tsx") : entryPath.endsWith(".tsx") ? entryPath : null;
1638
- const jsxPath = isDir ? path11.join(entryPath, "component.jsx") : entryPath.endsWith(".jsx") ? entryPath : null;
1639
- const entry = tsxPath && fs9.existsSync(tsxPath) ? tsxPath : jsxPath && fs9.existsSync(jsxPath) ? jsxPath : null;
1705
+ const namedJson = isDir ? path11.join(entryPath, `${name}.json`) : null;
1706
+ const legacyJson = isDir ? path11.join(entryPath, "component.json") : null;
1707
+ const metaPath = isDir ? namedJson && fs9.existsSync(namedJson) ? namedJson : legacyJson && fs9.existsSync(legacyJson) ? legacyJson : null : entryPath.endsWith(".json") ? entryPath : null;
1708
+ const namedTsx = isDir ? path11.join(entryPath, `${name}.tsx`) : null;
1709
+ const namedJsx = isDir ? path11.join(entryPath, `${name}.jsx`) : null;
1710
+ const legacyTsx = isDir ? path11.join(entryPath, "component.tsx") : null;
1711
+ const legacyJsx = isDir ? path11.join(entryPath, "component.jsx") : null;
1712
+ const tsxPath = isDir ? namedTsx && fs9.existsSync(namedTsx) ? namedTsx : legacyTsx && fs9.existsSync(legacyTsx) ? legacyTsx : null : entryPath.endsWith(".tsx") ? entryPath : null;
1713
+ const jsxPath = isDir ? namedJsx && fs9.existsSync(namedJsx) ? namedJsx : legacyJsx && fs9.existsSync(legacyJsx) ? legacyJsx : null : entryPath.endsWith(".jsx") ? entryPath : null;
1714
+ const entry = tsxPath ? tsxPath : jsxPath ? jsxPath : null;
1640
1715
  let meta = {};
1641
1716
  if (metaPath && fs9.existsSync(metaPath)) {
1642
1717
  meta = JSON.parse(fs9.readFileSync(metaPath, "utf8"));
@@ -2103,6 +2178,20 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
2103
2178
  download.start("Downloading template\u2026");
2104
2179
  const full = await ff.storeTemplates.find({ id: templateId });
2105
2180
  download.stop("Template downloaded");
2181
+ const listPrice = Math.max(
2182
+ 0,
2183
+ Number(full.price ?? 0) - Number(full.discount ?? 0)
2184
+ );
2185
+ if (listPrice > 0 && !full.data) {
2186
+ fail(
2187
+ "This is a paid template. Purchase + install it from the merchant app \u2014 CLI unpack of paid source is not available."
2188
+ );
2189
+ }
2190
+ if (listPrice > 0) {
2191
+ fail(
2192
+ `Paid template (${listPrice} DZD). Buy it for your store in the merchant app; do not unpack with feeef template init --from.`
2193
+ );
2194
+ }
2106
2195
  const data = full.data;
2107
2196
  if (!data || typeof data !== "object" || !("pages" in data)) {
2108
2197
  fail("Template has no usable data.pages \u2014 cannot unpack");
@@ -2433,8 +2522,10 @@ async function runTemplatePublish(dirArg, opts) {
2433
2522
  }
2434
2523
  const store = await ff.stores.find({ id: storeId });
2435
2524
  const title = opts.title || rc.title || meta.name || path13.basename(srcDir);
2525
+ const price = Math.max(0, Number(opts.price ?? 0) || 0);
2436
2526
  info(`Template ${srcDir}`);
2437
2527
  info(`Store ${store.slug || storeId} (${policy})`);
2528
+ if (price > 0) info(`Price ${price} DZD (platform cut applies on sale)`);
2438
2529
  const outPath = path13.join(srcDir, "dist", "data.json");
2439
2530
  const s = spinner2();
2440
2531
  s.start("Building\u2026");
@@ -2588,17 +2679,19 @@ async function runTemplatePublish(dirArg, opts) {
2588
2679
  if (templateRow) {
2589
2680
  templateRow = await ff.storeTemplates.update({
2590
2681
  id: templateRow.id,
2591
- data: { title, schema, data, policy }
2682
+ data: { title, schema, data, policy, price }
2592
2683
  });
2593
2684
  s.stop(`Updated ${templateRow.id}`);
2594
2685
  } else {
2595
2686
  templateRow = await ff.storeTemplates.create({
2596
- storeId,
2597
- title,
2598
- schema,
2599
- data,
2600
- policy,
2601
- price: 0
2687
+ data: {
2688
+ storeId,
2689
+ title,
2690
+ schema,
2691
+ data,
2692
+ policy,
2693
+ price
2694
+ }
2602
2695
  });
2603
2696
  s.stop(`Created ${templateRow.id}`);
2604
2697
  }
@@ -2623,6 +2716,24 @@ async function runTemplatePublish(dirArg, opts) {
2623
2716
  }
2624
2717
  s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
2625
2718
  }
2719
+ if (typeof ff.storeTemplates.createRelease === "function") {
2720
+ s.start("Publishing release\u2026");
2721
+ const release = await ff.storeTemplates.createRelease(templateId, {
2722
+ ...opts.version ? { version: opts.version } : {},
2723
+ changelog: opts.changelog ?? null,
2724
+ schema,
2725
+ data
2726
+ });
2727
+ s.stop(`Release ${release.version} (${release.id})`);
2728
+ } else {
2729
+ await http3.post(`/store_templates/${templateId}/releases`, {
2730
+ ...opts.version ? { version: opts.version } : {},
2731
+ changelog: opts.changelog ?? null,
2732
+ schema,
2733
+ data
2734
+ });
2735
+ ok("Release published");
2736
+ }
2626
2737
  const shouldApply = opts.yes && opts.apply !== false ? true : await askApply(store.slug || storeId, opts.apply);
2627
2738
  if (shouldApply) {
2628
2739
  s.start("Installing on store\u2026");
@@ -2805,7 +2916,7 @@ function createProgram() {
2805
2916
  ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
2806
2917
  await runUse(store, opts);
2807
2918
  });
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) => {
2919
+ 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("--price <n>", "List price in DZD (0 = free)").option("--version <semver>", "Release version (default: auto bump)").option("--changelog <text>", "Release changelog for merchants").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2809
2920
  await runTemplatePublish(dir, opts);
2810
2921
  });
2811
2922
  const template = program.command("template").description(
@@ -2870,7 +2981,7 @@ function createProgram() {
2870
2981
  ).action(async (dir, opts) => {
2871
2982
  await runTemplateDev(dir, opts);
2872
2983
  });
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) => {
2984
+ 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("--price <n>", "List price in DZD (0 = free)").option("--version <semver>", "Release version (default: auto bump)").option("--changelog <text>", "Release changelog for merchants").option("--yes", "Skip prompts").option("--apply", "Install onto the store").option("--no-apply", "Do not install").action(async (dir, opts) => {
2874
2985
  await runTemplatePublish(dir, opts);
2875
2986
  });
2876
2987
  const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");