@feeef.dev/cli 0.2.1 → 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 +22 -5
- package/dist/bin.js +131 -17
- package/dist/bin.js.map +1 -1
- package/dist/index.js +131 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +9 -7
- package/scaffolds/blank/.cursor/rules/theme-source.mdc +24 -17
- package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +21 -9
- package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +35 -30
- package/scaffolds/blank/AGENTS.md +7 -4
- package/scaffolds/blank/README.md +1 -1
- package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +68 -39
- package/scaffolds/blank/docs/04-WORKFLOW.md +7 -4
- package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +7 -6
- package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +45 -77
- package/scaffolds/blank/docs/08-ORDER-FORM.md +1 -1
- package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +21 -69
- package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +37 -22
- package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +38 -14
- package/scaffolds/blank/package-lock.json +4 -1918
- package/vendor/template-kit/lib/build.mjs +1 -1
- package/vendor/template-kit/lib/component-meta.mjs +92 -8
- package/vendor/template-kit/lib/unpack.mjs +295 -135
- package/vendor/template-kit/lib/validate.mjs +1 -1
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
|
-
|
|
145
|
-
|
|
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` |
|
|
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`) |
|
|
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
|
|
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
|
@@ -127,6 +127,7 @@ function loadEnvFiles() {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// src/index.ts
|
|
130
|
+
import { createRequire } from "module";
|
|
130
131
|
import { Command } from "commander";
|
|
131
132
|
|
|
132
133
|
// src/client.ts
|
|
@@ -799,7 +800,13 @@ function nextComponentOrder(parentDir) {
|
|
|
799
800
|
if (fs5.statSync(abs).isFile() && /\.(tsx|jsx)$/.test(name)) {
|
|
800
801
|
sourcePath = abs;
|
|
801
802
|
} else if (fs5.statSync(abs).isDirectory()) {
|
|
802
|
-
|
|
803
|
+
const id = name;
|
|
804
|
+
for (const entry of [
|
|
805
|
+
`${id}.tsx`,
|
|
806
|
+
`${id}.jsx`,
|
|
807
|
+
"component.tsx",
|
|
808
|
+
"component.jsx"
|
|
809
|
+
]) {
|
|
803
810
|
const p2 = path5.join(abs, entry);
|
|
804
811
|
if (fs5.existsSync(p2)) {
|
|
805
812
|
sourcePath = p2;
|
|
@@ -1171,10 +1178,54 @@ async function resolveBoundStore(srcDir) {
|
|
|
1171
1178
|
}
|
|
1172
1179
|
return { storeId: store.id, slug: store.slug };
|
|
1173
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
|
+
}
|
|
1174
1220
|
async function runRemoteDraftPreview(srcDir) {
|
|
1175
1221
|
const outPath = path9.join(srcDir, "dist", "data.json");
|
|
1176
1222
|
const { storeId, slug } = await resolveBoundStore(srcDir);
|
|
1177
|
-
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
|
+
}
|
|
1178
1229
|
const s = spinner2();
|
|
1179
1230
|
s.start("Building template\u2026");
|
|
1180
1231
|
const { buildTemplate } = await loadBuild();
|
|
@@ -1207,6 +1258,7 @@ async function runRemoteDraftPreview(srcDir) {
|
|
|
1207
1258
|
}
|
|
1208
1259
|
const data = JSON.parse(fs7.readFileSync(outPath, "utf8"));
|
|
1209
1260
|
const result = await ff.stores.putTemplatePreview(storeId, { data });
|
|
1261
|
+
assertRemotePreviewApiMatch(baseURL, result.previewUrl);
|
|
1210
1262
|
lastPreviewUrl = withDevLog(result.previewUrl);
|
|
1211
1263
|
ok(`Draft preview updated \u2192 ${pc.cyan(lastPreviewUrl)}`);
|
|
1212
1264
|
} catch (e) {
|
|
@@ -1387,7 +1439,25 @@ async function runTemplateDev(dirArg, opts) {
|
|
|
1387
1439
|
intro2("feeef template dev");
|
|
1388
1440
|
try {
|
|
1389
1441
|
const srcDir = resolveThemeDir(dirArg);
|
|
1390
|
-
|
|
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) {
|
|
1391
1461
|
await runLocalStorefrontPreview(srcDir, opts);
|
|
1392
1462
|
return;
|
|
1393
1463
|
}
|
|
@@ -1652,10 +1722,16 @@ function collectPublishableComponents(srcDir) {
|
|
|
1652
1722
|
const entries = [...byId.entries()].map(([id, entryPath]) => ({ id, entryPath })).sort((a, b) => a.id.localeCompare(b.id));
|
|
1653
1723
|
for (const { id: name, entryPath } of entries) {
|
|
1654
1724
|
const isDir = fs10.statSync(entryPath).isDirectory();
|
|
1655
|
-
const
|
|
1656
|
-
const
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
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;
|
|
1659
1735
|
let meta = {};
|
|
1660
1736
|
if (metaPath && fs10.existsSync(metaPath)) {
|
|
1661
1737
|
meta = JSON.parse(fs10.readFileSync(metaPath, "utf8"));
|
|
@@ -2122,6 +2198,20 @@ async function initFromPublicTemplate(dest, name, templateId, ff) {
|
|
|
2122
2198
|
download.start("Downloading template\u2026");
|
|
2123
2199
|
const full = await ff.storeTemplates.find({ id: templateId });
|
|
2124
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
|
+
}
|
|
2125
2215
|
const data = full.data;
|
|
2126
2216
|
if (!data || typeof data !== "object" || !("pages" in data)) {
|
|
2127
2217
|
fail("Template has no usable data.pages \u2014 cannot unpack");
|
|
@@ -2452,8 +2542,10 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2452
2542
|
}
|
|
2453
2543
|
const store = await ff.stores.find({ id: storeId });
|
|
2454
2544
|
const title = opts.title || rc.title || meta.name || path14.basename(srcDir);
|
|
2545
|
+
const price = Math.max(0, Number(opts.price ?? 0) || 0);
|
|
2455
2546
|
info(`Template ${srcDir}`);
|
|
2456
2547
|
info(`Store ${store.slug || storeId} (${policy})`);
|
|
2548
|
+
if (price > 0) info(`Price ${price} DZD (platform cut applies on sale)`);
|
|
2457
2549
|
const outPath = path14.join(srcDir, "dist", "data.json");
|
|
2458
2550
|
const s = spinner2();
|
|
2459
2551
|
s.start("Building\u2026");
|
|
@@ -2607,17 +2699,19 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2607
2699
|
if (templateRow) {
|
|
2608
2700
|
templateRow = await ff.storeTemplates.update({
|
|
2609
2701
|
id: templateRow.id,
|
|
2610
|
-
data: { title, schema, data, policy }
|
|
2702
|
+
data: { title, schema, data, policy, price }
|
|
2611
2703
|
});
|
|
2612
2704
|
s.stop(`Updated ${templateRow.id}`);
|
|
2613
2705
|
} else {
|
|
2614
2706
|
templateRow = await ff.storeTemplates.create({
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2707
|
+
data: {
|
|
2708
|
+
storeId,
|
|
2709
|
+
title,
|
|
2710
|
+
schema,
|
|
2711
|
+
data,
|
|
2712
|
+
policy,
|
|
2713
|
+
price
|
|
2714
|
+
}
|
|
2621
2715
|
});
|
|
2622
2716
|
s.stop(`Created ${templateRow.id}`);
|
|
2623
2717
|
}
|
|
@@ -2642,6 +2736,24 @@ async function runTemplatePublish(dirArg, opts) {
|
|
|
2642
2736
|
}
|
|
2643
2737
|
s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
|
|
2644
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
|
+
}
|
|
2645
2757
|
const shouldApply = opts.yes && opts.apply !== false ? true : await askApply(store.slug || storeId, opts.apply);
|
|
2646
2758
|
if (shouldApply) {
|
|
2647
2759
|
s.start("Installing on store\u2026");
|
|
@@ -2788,9 +2900,11 @@ async function runTemplateWatch(dirArg) {
|
|
|
2788
2900
|
}
|
|
2789
2901
|
|
|
2790
2902
|
// src/index.ts
|
|
2903
|
+
var require2 = createRequire(import.meta.url);
|
|
2904
|
+
var { version: CLI_VERSION } = require2("../package.json");
|
|
2791
2905
|
function createProgram() {
|
|
2792
2906
|
const program = new Command();
|
|
2793
|
-
program.name("feeef").description("Feeef developer CLI \u2014 templates today, cloud tools tomorrow").version(
|
|
2907
|
+
program.name("feeef").description("Feeef developer CLI \u2014 templates today, cloud tools tomorrow").version(CLI_VERSION).hook("preAction", (thisCommand) => {
|
|
2794
2908
|
if (thisCommand.args.length === 0) return;
|
|
2795
2909
|
banner();
|
|
2796
2910
|
});
|
|
@@ -2822,7 +2936,7 @@ function createProgram() {
|
|
|
2822
2936
|
).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this template").action(async (store, opts) => {
|
|
2823
2937
|
await runUse(store, opts);
|
|
2824
2938
|
});
|
|
2825
|
-
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) => {
|
|
2826
2940
|
await runTemplatePublish(dir, opts);
|
|
2827
2941
|
});
|
|
2828
2942
|
const template = program.command("template").description(
|
|
@@ -2887,7 +3001,7 @@ function createProgram() {
|
|
|
2887
3001
|
).action(async (dir, opts) => {
|
|
2888
3002
|
await runTemplateDev(dir, opts);
|
|
2889
3003
|
});
|
|
2890
|
-
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) => {
|
|
2891
3005
|
await runTemplatePublish(dir, opts);
|
|
2892
3006
|
});
|
|
2893
3007
|
const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");
|