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