@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/README.md CHANGED
@@ -94,6 +94,8 @@ dist/schema.json # merged editor schema
94
94
 
95
95
  Authoring: flat `hero.tsx` with `export const meta` + `function App()`. See scaffold docs under `scaffolds/blank/docs/`.
96
96
 
97
+ Marketplace sales: see [18-PAID-TEMPLATE-MARKET](../storefront/templates/docs/18-PAID-TEMPLATE-MARKET.md) — `--price`, releases, wallet buy, forever Owned.
98
+
97
99
  ---
98
100
 
99
101
  ## Preview (`feeef template dev`)
@@ -125,6 +127,14 @@ flowchart LR
125
127
  4. Thin banner: **Previewing draft template — Exit**
126
128
  5. Exit: banner, or `/?preview=0` / `/?preview=exit`, or stop the CLI (clears draft), or wait for token TTL (~24h)
127
129
 
130
+ **API must match the shop.** Hosted `*.feeef.store` loads drafts from production (`https://api.feeef.org/v1`). If the CLI still uses `http://127.0.0.1:3333`, the preview URL opens but you only see the **live** theme:
131
+
132
+ ```bash
133
+ feeef config set apiUrl https://api.feeef.org/v1
134
+ feeef signin
135
+ feeef template dev
136
+ ```
137
+
128
138
  **Hard rule:** `dev` never overwrites live `templateData`.
129
139
 
130
140
  ### Local Next (employees only)
@@ -141,18 +151,25 @@ Not documented for public authors; not required for npm installs.
141
151
  ## Publish (go live)
142
152
 
143
153
  ```bash
144
- feeef template publish --apply
145
- # or: npm run publish -- --apply
154
+ # Free public listing + release
155
+ feeef template publish --public --apply
156
+
157
+ # Paid listing (developers set price; Feeef takes an admin-configurable cut, default 30%)
158
+ feeef template publish --public --price 5000 --version 1.0.0 \
159
+ --changelog "Initial marketplace release" --apply
146
160
  ```
147
161
 
148
162
  | Target | Source |
149
163
  |---|---|
150
- | `store_templates` | `dist/data.json` (i18n stripped) + `dist/schema.json` |
164
+ | `store_templates` | Listing metadata + head `schema`/`data` + **price** |
165
+ | `store_template_releases` | Immutable snapshot + semver + changelog |
151
166
  | `template_components` | `shared/` + `library/` |
152
167
  | `store_template_locales` | `dist/locales/*.json` |
153
- | Install (`--apply`) | Links `store.templateId` + clones data → live `metadata.templateData` |
168
+ | Install (`--apply`) | Pins `store.templateId` + `template_release_id` → live `metadata.templateData` |
169
+
170
+ Merchants who **buy** a paid template own it forever on that store (reinstall / update free). Paid templates cannot be forked into a new catalog row.
154
171
 
155
- Without `--apply`, the marketplace row updates but the storefront customers see does not change until install/editor save.
172
+ Without `--apply`, the marketplace listing/release updates but customers keep the previous live install until they install/update.
156
173
 
157
174
  ---
158
175
 
package/dist/bin.js CHANGED
@@ -800,7 +800,13 @@ function nextComponentOrder(parentDir) {
800
800
  if (fs5.statSync(abs).isFile() && /\.(tsx|jsx)$/.test(name)) {
801
801
  sourcePath = abs;
802
802
  } else if (fs5.statSync(abs).isDirectory()) {
803
- for (const entry of ["component.tsx", "component.jsx"]) {
803
+ const id = name;
804
+ for (const entry of [
805
+ `${id}.tsx`,
806
+ `${id}.jsx`,
807
+ "component.tsx",
808
+ "component.jsx"
809
+ ]) {
804
810
  const p2 = path5.join(abs, entry);
805
811
  if (fs5.existsSync(p2)) {
806
812
  sourcePath = p2;
@@ -1172,10 +1178,54 @@ async function resolveBoundStore(srcDir) {
1172
1178
  }
1173
1179
  return { storeId: store.id, slug: store.slug };
1174
1180
  }
1181
+ function isLoopbackHost(hostname) {
1182
+ const h = hostname.toLowerCase();
1183
+ return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0";
1184
+ }
1185
+ function assertRemotePreviewApiMatch(apiBase, previewUrl) {
1186
+ if (process.env["FEEEF_ALLOW_LOCAL_API_REMOTE_PREVIEW"] === "1") return;
1187
+ let apiHost = "";
1188
+ let previewHost = "";
1189
+ try {
1190
+ apiHost = new URL(apiBase).hostname;
1191
+ } catch {
1192
+ return;
1193
+ }
1194
+ try {
1195
+ previewHost = new URL(previewUrl).hostname;
1196
+ } catch {
1197
+ return;
1198
+ }
1199
+ if (isLoopbackHost(apiHost) && !isLoopbackHost(previewHost)) {
1200
+ throw new Error(
1201
+ [
1202
+ "Draft preview mismatch: CLI API is local, but preview URL is hosted.",
1203
+ ` API ${apiBase}`,
1204
+ ` Preview ${previewUrl}`,
1205
+ "",
1206
+ "Hosted shops load drafts from production API \u2014 your local PUT never reaches them,",
1207
+ "so the page shows the live theme.",
1208
+ "",
1209
+ "Fix:",
1210
+ " feeef config set apiUrl https://api.feeef.org/v1",
1211
+ " feeef signin",
1212
+ " feeef template dev",
1213
+ "",
1214
+ "Or use local Next: feeef template dev --local",
1215
+ "(override: FEEEF_ALLOW_LOCAL_API_REMOTE_PREVIEW=1)"
1216
+ ].join("\n")
1217
+ );
1218
+ }
1219
+ }
1175
1220
  async function runRemoteDraftPreview(srcDir) {
1176
1221
  const outPath = path9.join(srcDir, "dist", "data.json");
1177
1222
  const { storeId, slug } = await resolveBoundStore(srcDir);
1178
- const { ff } = createClient({ requireAuth: true });
1223
+ const { ff, baseURL } = createClient({ requireAuth: true });
1224
+ if (isLoopbackHost(new URL(baseURL).hostname)) {
1225
+ warn(
1226
+ `API is ${baseURL} \u2014 hosted ?preview= will only work if that API is what the shop calls.`
1227
+ );
1228
+ }
1179
1229
  const s = spinner2();
1180
1230
  s.start("Building template\u2026");
1181
1231
  const { buildTemplate } = await loadBuild();
@@ -1208,6 +1258,7 @@ async function runRemoteDraftPreview(srcDir) {
1208
1258
  }
1209
1259
  const data = JSON.parse(fs7.readFileSync(outPath, "utf8"));
1210
1260
  const result = await ff.stores.putTemplatePreview(storeId, { data });
1261
+ assertRemotePreviewApiMatch(baseURL, result.previewUrl);
1211
1262
  lastPreviewUrl = withDevLog(result.previewUrl);
1212
1263
  ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
1213
1264
  } catch (e) {
@@ -1388,7 +1439,25 @@ async function runTemplateDev(dirArg, opts) {
1388
1439
  intro2("feeef template dev");
1389
1440
  try {
1390
1441
  const srcDir = resolveThemeDir(dirArg);
1391
- if (opts.local || opts.storefront) {
1442
+ let useLocal = Boolean(opts.local || opts.storefront);
1443
+ if (!useLocal) {
1444
+ const cfg = loadConfig();
1445
+ let apiHost = "";
1446
+ try {
1447
+ apiHost = new URL(defaultApiUrl(cfg)).hostname;
1448
+ } catch {
1449
+ }
1450
+ const hasStorefront = Boolean(
1451
+ opts.storefront || cfg.storefrontPath || process.env["FEEEF_STOREFRONT_PATH"]
1452
+ );
1453
+ if (isLoopbackHost(apiHost) && hasStorefront) {
1454
+ warn(
1455
+ "Local API + storefrontPath detected \u2192 using --local (hosted ?preview= would show the live theme)."
1456
+ );
1457
+ useLocal = true;
1458
+ }
1459
+ }
1460
+ if (useLocal) {
1392
1461
  await runLocalStorefrontPreview(srcDir, opts);
1393
1462
  return;
1394
1463
  }
@@ -1653,10 +1722,16 @@ function collectPublishableComponents(srcDir) {
1653
1722
  const entries = [...byId.entries()].map(([id, entryPath]) => ({ id, entryPath })).sort((a, b) => a.id.localeCompare(b.id));
1654
1723
  for (const { id: name, entryPath } of entries) {
1655
1724
  const isDir = fs10.statSync(entryPath).isDirectory();
1656
- const metaPath = isDir ? path12.join(entryPath, "component.json") : entryPath.endsWith(".json") ? entryPath : null;
1657
- const tsxPath = isDir ? path12.join(entryPath, "component.tsx") : entryPath.endsWith(".tsx") ? entryPath : null;
1658
- const jsxPath = isDir ? path12.join(entryPath, "component.jsx") : entryPath.endsWith(".jsx") ? entryPath : null;
1659
- const entry = tsxPath && fs10.existsSync(tsxPath) ? tsxPath : jsxPath && fs10.existsSync(jsxPath) ? jsxPath : null;
1725
+ const namedJson = isDir ? path12.join(entryPath, `${name}.json`) : null;
1726
+ const legacyJson = isDir ? path12.join(entryPath, "component.json") : null;
1727
+ const metaPath = isDir ? namedJson && fs10.existsSync(namedJson) ? namedJson : legacyJson && fs10.existsSync(legacyJson) ? legacyJson : null : entryPath.endsWith(".json") ? entryPath : null;
1728
+ const namedTsx = isDir ? path12.join(entryPath, `${name}.tsx`) : null;
1729
+ const namedJsx = isDir ? path12.join(entryPath, `${name}.jsx`) : null;
1730
+ const legacyTsx = isDir ? path12.join(entryPath, "component.tsx") : null;
1731
+ const legacyJsx = isDir ? path12.join(entryPath, "component.jsx") : null;
1732
+ const tsxPath = isDir ? namedTsx && fs10.existsSync(namedTsx) ? namedTsx : legacyTsx && fs10.existsSync(legacyTsx) ? legacyTsx : null : entryPath.endsWith(".tsx") ? entryPath : null;
1733
+ const jsxPath = isDir ? namedJsx && fs10.existsSync(namedJsx) ? namedJsx : legacyJsx && fs10.existsSync(legacyJsx) ? legacyJsx : null : entryPath.endsWith(".jsx") ? entryPath : null;
1734
+ const entry = tsxPath ? tsxPath : jsxPath ? jsxPath : null;
1660
1735
  let meta = {};
1661
1736
  if (metaPath && fs10.existsSync(metaPath)) {
1662
1737
  meta = JSON.parse(fs10.readFileSync(metaPath, "utf8"));
@@ -2123,6 +2198,20 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
2123
2198
  download.start("Downloading template\u2026");
2124
2199
  const full = await ff.storeTemplates.find({ id: templateId });
2125
2200
  download.stop("Template downloaded");
2201
+ const listPrice = Math.max(
2202
+ 0,
2203
+ Number(full.price ?? 0) - Number(full.discount ?? 0)
2204
+ );
2205
+ if (listPrice > 0 && !full.data) {
2206
+ fail(
2207
+ "This is a paid template. Purchase + install it from the merchant app \u2014 CLI unpack of paid source is not available."
2208
+ );
2209
+ }
2210
+ if (listPrice > 0) {
2211
+ fail(
2212
+ `Paid template (${listPrice} DZD). Buy it for your store in the merchant app; do not unpack with feeef template init --from.`
2213
+ );
2214
+ }
2126
2215
  const data = full.data;
2127
2216
  if (!data || typeof data !== "object" || !("pages" in data)) {
2128
2217
  fail("Template has no usable data.pages \u2014 cannot unpack");
@@ -2453,8 +2542,10 @@ async function runTemplatePublish(dirArg, opts) {
2453
2542
  }
2454
2543
  const store = await ff.stores.find({ id: storeId });
2455
2544
  const title = opts.title || rc.title || meta.name || path14.basename(srcDir);
2545
+ const price = Math.max(0, Number(opts.price ?? 0) || 0);
2456
2546
  info(`Template ${srcDir}`);
2457
2547
  info(`Store ${store.slug || storeId} (${policy})`);
2548
+ if (price > 0) info(`Price ${price} DZD (platform cut applies on sale)`);
2458
2549
  const outPath = path14.join(srcDir, "dist", "data.json");
2459
2550
  const s = spinner2();
2460
2551
  s.start("Building\u2026");
@@ -2608,17 +2699,19 @@ async function runTemplatePublish(dirArg, opts) {
2608
2699
  if (templateRow) {
2609
2700
  templateRow = await ff.storeTemplates.update({
2610
2701
  id: templateRow.id,
2611
- data: { title, schema, data, policy }
2702
+ data: { title, schema, data, policy, price }
2612
2703
  });
2613
2704
  s.stop(`Updated ${templateRow.id}`);
2614
2705
  } else {
2615
2706
  templateRow = await ff.storeTemplates.create({
2616
- storeId,
2617
- title,
2618
- schema,
2619
- data,
2620
- policy,
2621
- price: 0
2707
+ data: {
2708
+ storeId,
2709
+ title,
2710
+ schema,
2711
+ data,
2712
+ policy,
2713
+ price
2714
+ }
2622
2715
  });
2623
2716
  s.stop(`Created ${templateRow.id}`);
2624
2717
  }
@@ -2643,6 +2736,24 @@ async function runTemplatePublish(dirArg, opts) {
2643
2736
  }
2644
2737
  s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
2645
2738
  }
2739
+ if (typeof ff.storeTemplates.createRelease === "function") {
2740
+ s.start("Publishing release\u2026");
2741
+ const release = await ff.storeTemplates.createRelease(templateId, {
2742
+ ...opts.version ? { version: opts.version } : {},
2743
+ changelog: opts.changelog ?? null,
2744
+ schema,
2745
+ data
2746
+ });
2747
+ s.stop(`Release ${release.version} (${release.id})`);
2748
+ } else {
2749
+ await http3.post(`/store_templates/${templateId}/releases`, {
2750
+ ...opts.version ? { version: opts.version } : {},
2751
+ changelog: opts.changelog ?? null,
2752
+ schema,
2753
+ data
2754
+ });
2755
+ ok("Release published");
2756
+ }
2646
2757
  const shouldApply = opts.yes && opts.apply !== false ? true : await askApply(store.slug || storeId, opts.apply);
2647
2758
  if (shouldApply) {
2648
2759
  s.start("Installing on store\u2026");
@@ -2825,7 +2936,7 @@ function createProgram() {
2825
2936
  ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
2826
2937
  await runUse(store, opts);
2827
2938
  });
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) => {
2939
+ 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) => {
2829
2940
  await runTemplatePublish(dir, opts);
2830
2941
  });
2831
2942
  const template = program.command("template").description(
@@ -2890,7 +3001,7 @@ function createProgram() {
2890
3001
  ).action(async (dir, opts) => {
2891
3002
  await runTemplateDev(dir, opts);
2892
3003
  });
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) => {
3004
+ 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) => {
2894
3005
  await runTemplatePublish(dir, opts);
2895
3006
  });
2896
3007
  const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");