@feeef.dev/cli 0.2.0

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.
Files changed (61) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +94 -0
  3. package/dist/bin.js +2752 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +2730 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +67 -0
  9. package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
  10. package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
  11. package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
  12. package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
  13. package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
  14. package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
  15. package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
  16. package/scaffolds/blank/.vscode/extensions.json +3 -0
  17. package/scaffolds/blank/.vscode/settings.json +13 -0
  18. package/scaffolds/blank/AGENTS.md +104 -0
  19. package/scaffolds/blank/README.md +21 -0
  20. package/scaffolds/blank/docs/00-INDEX.md +33 -0
  21. package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
  22. package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
  23. package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
  24. package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
  25. package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
  26. package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
  27. package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
  28. package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
  29. package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
  30. package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
  31. package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
  32. package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
  33. package/scaffolds/blank/feeef.template.json +7 -0
  34. package/scaffolds/blank/locales/README.md +4 -0
  35. package/scaffolds/blank/locales/ar.json +10 -0
  36. package/scaffolds/blank/locales/en.json +10 -0
  37. package/scaffolds/blank/package-lock.json +1975 -0
  38. package/scaffolds/blank/package.json +23 -0
  39. package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
  40. package/scaffolds/blank/pages/home/page.json +3 -0
  41. package/scaffolds/blank/props.json +6 -0
  42. package/scaffolds/blank/schema.ts +49 -0
  43. package/scaffolds/blank/tsconfig.json +24 -0
  44. package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
  45. package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
  46. package/vendor/template-kit/README.md +64 -0
  47. package/vendor/template-kit/bin/dev.mjs +61 -0
  48. package/vendor/template-kit/bin/feeef-template.mjs +212 -0
  49. package/vendor/template-kit/data/lithium-schema.json +4733 -0
  50. package/vendor/template-kit/lib/build.mjs +377 -0
  51. package/vendor/template-kit/lib/compile-component.mjs +188 -0
  52. package/vendor/template-kit/lib/component-meta.mjs +481 -0
  53. package/vendor/template-kit/lib/library.mjs +168 -0
  54. package/vendor/template-kit/lib/locales.mjs +68 -0
  55. package/vendor/template-kit/lib/paths.mjs +84 -0
  56. package/vendor/template-kit/lib/shared.mjs +268 -0
  57. package/vendor/template-kit/lib/theme-schema.mjs +300 -0
  58. package/vendor/template-kit/lib/unpack.mjs +324 -0
  59. package/vendor/template-kit/lib/validate.mjs +207 -0
  60. package/vendor/template-kit/schemas/component.schema.json +55 -0
  61. package/vendor/template-kit/schemas/template.schema.json +18 -0
package/dist/index.js ADDED
@@ -0,0 +1,2730 @@
1
+ // src/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/client.ts
5
+ import axios from "axios";
6
+ import { FeeeF } from "feeef";
7
+
8
+ // src/config.ts
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "feeef");
13
+ var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
14
+ var DIR_MODE = 448;
15
+ var FILE_MODE = 384;
16
+ function ensureConfigDir() {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: DIR_MODE });
18
+ try {
19
+ fs.chmodSync(CONFIG_DIR, DIR_MODE);
20
+ } catch {
21
+ }
22
+ }
23
+ function writeConfigFile(data) {
24
+ ensureConfigDir();
25
+ const body = `${JSON.stringify(data, null, 2)}
26
+ `;
27
+ fs.writeFileSync(CONFIG_PATH, body, { encoding: "utf8", mode: FILE_MODE });
28
+ try {
29
+ fs.chmodSync(CONFIG_PATH, FILE_MODE);
30
+ } catch {
31
+ }
32
+ }
33
+ function configPath() {
34
+ return CONFIG_PATH;
35
+ }
36
+ function loadConfig() {
37
+ try {
38
+ if (!fs.existsSync(CONFIG_PATH)) return {};
39
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+ function saveConfig(patch) {
45
+ const next = { ...loadConfig(), ...patch };
46
+ for (const key of Object.keys(next)) {
47
+ const value = next[key];
48
+ if (value === void 0 || value === null || value === "") {
49
+ delete next[key];
50
+ }
51
+ }
52
+ writeConfigFile(next);
53
+ return next;
54
+ }
55
+ function clearAuth() {
56
+ const cfg = loadConfig();
57
+ delete cfg.token;
58
+ delete cfg.email;
59
+ writeConfigFile(cfg);
60
+ }
61
+ function defaultApiUrl(cfg = loadConfig()) {
62
+ return (process.env["FEEEF_API_BASE_URL"] || cfg.apiUrl || "https://api.feeef.org/v1").replace(/\/$/, "");
63
+ }
64
+ function defaultDevPort(cfg = loadConfig()) {
65
+ const fromEnv = Number(process.env["FEEEF_DEV_PORT"]);
66
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
67
+ if (typeof cfg.devPort === "number" && cfg.devPort > 0) return cfg.devPort;
68
+ return 3456;
69
+ }
70
+
71
+ // src/client.ts
72
+ function createHttp(baseURL, token) {
73
+ return axios.create({
74
+ baseURL,
75
+ maxBodyLength: Infinity,
76
+ maxContentLength: Infinity,
77
+ headers: {
78
+ Accept: "application/json",
79
+ "Content-Type": "application/json",
80
+ ...token ? { Authorization: `Bearer ${token}` } : {}
81
+ }
82
+ });
83
+ }
84
+ function createClient(opts = {}) {
85
+ const requireAuth = opts.requireAuth !== false;
86
+ const cfg = loadConfig();
87
+ const baseURL = defaultApiUrl(cfg);
88
+ const token = cfg.token;
89
+ if (requireAuth && !token) {
90
+ throw new Error("Not signed in. Run: feeef signin");
91
+ }
92
+ const http2 = createHttp(baseURL, token);
93
+ const ff = new FeeeF({
94
+ apiKey: token || "",
95
+ baseURL,
96
+ client: http2,
97
+ cache: false
98
+ });
99
+ return { ff, http: http2, cfg, baseURL, token };
100
+ }
101
+ async function signinWithPassword(input) {
102
+ const baseURL = (input.apiUrl || defaultApiUrl()).replace(/\/$/, "");
103
+ const http2 = createHttp(baseURL, void 0);
104
+ const ff = new FeeeF({
105
+ apiKey: "",
106
+ baseURL,
107
+ client: http2,
108
+ cache: false
109
+ });
110
+ const auth = await ff.users.signin({
111
+ email: input.email,
112
+ password: input.password
113
+ });
114
+ const token = auth?.token?.token;
115
+ if (!token) {
116
+ throw new Error("Signin succeeded but no token was returned");
117
+ }
118
+ ff.setHeader("Authorization", `Bearer ${token}`);
119
+ saveConfig({ token, email: input.email, apiUrl: baseURL });
120
+ return auth;
121
+ }
122
+
123
+ // src/oauth.ts
124
+ import crypto from "crypto";
125
+ import http from "http";
126
+ import { exec } from "child_process";
127
+ import axios2 from "axios";
128
+ import { FeeeF as FeeeF2, OAuthRepository } from "feeef";
129
+ var DEFAULT_OAUTH_CLIENT_ID = "gan5yqm11fk4eh1ayheelkcs";
130
+ var DEFAULT_OAUTH_REDIRECT_URI = "http://localhost.feeef.org:7777/oauth/callback";
131
+ var DEFAULT_OAUTH_CALLBACK_PORT = 7777;
132
+ var DEFAULT_CLI_OAUTH_SCOPES = [
133
+ "auth",
134
+ "store",
135
+ "store.settings",
136
+ "store_templates",
137
+ "store_templates.read"
138
+ ];
139
+ function originOnly(raw) {
140
+ try {
141
+ const u = new URL(raw);
142
+ return `${u.protocol}//${u.host}`;
143
+ } catch {
144
+ return raw.replace(/\/$/, "");
145
+ }
146
+ }
147
+ function resolveAuthorizeBaseUrl(apiUrl) {
148
+ const fromEnv = process.env["FEEEF_ACCOUNTS_URL"]?.trim() || process.env["FEEEF_OAUTH_AUTHORIZE_BASE_URL"]?.trim();
149
+ if (fromEnv) return originOnly(fromEnv);
150
+ try {
151
+ const u = new URL(apiUrl);
152
+ if (u.hostname === "api.feeef.org") {
153
+ return "https://accounts.feeef.org";
154
+ }
155
+ if (u.hostname.startsWith("api.")) {
156
+ const host = u.hostname.replace(/^api\./, "accounts.");
157
+ const port = u.port ? `:${u.port}` : "";
158
+ return `${u.protocol}//${host}${port}`;
159
+ }
160
+ if (u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "localhost.feeef.org") {
161
+ const port = u.port ? `:${u.port}` : "";
162
+ return `http://accounts.localhost.feeef.org${port}`;
163
+ }
164
+ if (u.hostname.startsWith("accounts.")) {
165
+ return `${u.protocol}//${u.host}`;
166
+ }
167
+ return `${u.protocol}//${u.host}`;
168
+ } catch {
169
+ return apiUrl.replace(/\/$/, "");
170
+ }
171
+ }
172
+ function resolveOAuthSettings(apiUrlOverride) {
173
+ const clientId = process.env["FEEEF_OAUTH_CLIENT_ID"]?.trim() || DEFAULT_OAUTH_CLIENT_ID;
174
+ const clientSecret = process.env["FEEEF_OAUTH_CLIENT_SECRET"]?.trim() || void 0;
175
+ const redirectUri = process.env["FEEEF_OAUTH_REDIRECT_URI"]?.trim() || DEFAULT_OAUTH_REDIRECT_URI;
176
+ const callbackPort = Number(
177
+ process.env["FEEEF_OAUTH_CALLBACK_PORT"] || DEFAULT_OAUTH_CALLBACK_PORT
178
+ );
179
+ const apiUrl = (apiUrlOverride || defaultApiUrl()).replace(/\/$/, "");
180
+ const scopesEnv = process.env["FEEEF_OAUTH_SCOPES"]?.trim();
181
+ const scopes = scopesEnv ? scopesEnv.split(/[\s,]+/).filter(Boolean) : [...DEFAULT_CLI_OAUTH_SCOPES];
182
+ return {
183
+ clientId,
184
+ clientSecret,
185
+ redirectUri,
186
+ callbackPort: Number.isFinite(callbackPort) ? callbackPort : DEFAULT_OAUTH_CALLBACK_PORT,
187
+ apiUrl,
188
+ authorizeBaseUrl: resolveAuthorizeBaseUrl(apiUrl),
189
+ scopes
190
+ };
191
+ }
192
+ function createPkcePair() {
193
+ const verifier = crypto.randomBytes(32).toString("base64url");
194
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
195
+ return { verifier, challenge };
196
+ }
197
+ function openBrowser(url) {
198
+ const platform = process.platform;
199
+ const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
200
+ exec(cmd, () => {
201
+ });
202
+ }
203
+ function htmlPage(title, body) {
204
+ return `<!doctype html>
205
+ <html lang="en">
206
+ <head>
207
+ <meta charset="utf-8" />
208
+ <title>${title}</title>
209
+ <style>
210
+ body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; }
211
+ h1 { font-size: 1.25rem; }
212
+ p { color: #444; }
213
+ </style>
214
+ </head>
215
+ <body>
216
+ <h1>${title}</h1>
217
+ <p>${body}</p>
218
+ <p>You can close this tab and return to the terminal.</p>
219
+ </body>
220
+ </html>`;
221
+ }
222
+ function listenForOAuthCallback(opts) {
223
+ const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1e3;
224
+ return new Promise((resolve, reject) => {
225
+ let settled = false;
226
+ const settle = (fn) => {
227
+ if (settled) return;
228
+ settled = true;
229
+ fn();
230
+ };
231
+ const server = http.createServer((req, res) => {
232
+ try {
233
+ const url = new URL(req.url || "/", `http://127.0.0.1:${opts.port}`);
234
+ if (url.pathname !== "/oauth/callback") {
235
+ res.writeHead(404, { "Content-Type": "text/plain" });
236
+ res.end("Not found");
237
+ return;
238
+ }
239
+ const err = url.searchParams.get("error");
240
+ if (err) {
241
+ const desc = url.searchParams.get("error_description") || err;
242
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
243
+ res.end(htmlPage("Authorization failed", desc));
244
+ settle(() => {
245
+ server.close();
246
+ reject(new Error(desc));
247
+ });
248
+ return;
249
+ }
250
+ const returnedState = url.searchParams.get("state");
251
+ if (returnedState !== opts.expectedState) {
252
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
253
+ res.end(htmlPage("Invalid state", "CSRF check failed. Try again."));
254
+ settle(() => {
255
+ server.close();
256
+ reject(new Error("OAuth state mismatch"));
257
+ });
258
+ return;
259
+ }
260
+ const authCode = url.searchParams.get("code");
261
+ if (!authCode) {
262
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
263
+ res.end(htmlPage("Missing code", "No authorization code in callback."));
264
+ settle(() => {
265
+ server.close();
266
+ reject(new Error("Missing authorization code"));
267
+ });
268
+ return;
269
+ }
270
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
271
+ res.end(htmlPage("Signed in", "Feeef CLI authorization complete."));
272
+ settle(() => {
273
+ server.close();
274
+ resolve(authCode);
275
+ });
276
+ } catch (e) {
277
+ settle(() => {
278
+ server.close();
279
+ reject(e);
280
+ });
281
+ }
282
+ });
283
+ server.on("error", (e) => {
284
+ settle(() => {
285
+ reject(
286
+ e instanceof Error && e.code === "EADDRINUSE" ? new Error(
287
+ `Port ${opts.port} is in use. Free it or set FEEEF_OAUTH_CALLBACK_PORT.`
288
+ ) : e
289
+ );
290
+ });
291
+ });
292
+ server.listen(opts.port, "127.0.0.1", () => {
293
+ opts.onListening?.();
294
+ });
295
+ setTimeout(() => {
296
+ settle(() => {
297
+ server.close();
298
+ reject(new Error("OAuth timed out waiting for browser callback"));
299
+ });
300
+ }, timeoutMs).unref();
301
+ });
302
+ }
303
+ async function signinWithOAuth(opts) {
304
+ const settings = resolveOAuthSettings(opts.apiUrl);
305
+ const state = crypto.randomUUID();
306
+ const { verifier, challenge } = createPkcePair();
307
+ const authorizeUrl = OAuthRepository.buildAuthorizeUrl({
308
+ baseUrl: settings.authorizeBaseUrl,
309
+ clientId: settings.clientId,
310
+ redirectUri: settings.redirectUri,
311
+ scope: settings.scopes,
312
+ state,
313
+ codeChallenge: challenge,
314
+ codeChallengeMethod: "S256"
315
+ });
316
+ opts.onAuthorizeUrl?.(authorizeUrl);
317
+ const code = await listenForOAuthCallback({
318
+ port: settings.callbackPort,
319
+ expectedState: state,
320
+ onListening: () => {
321
+ if (opts.openBrowser !== false) {
322
+ openBrowser(authorizeUrl);
323
+ }
324
+ }
325
+ });
326
+ const httpClient = axios2.create({
327
+ baseURL: settings.apiUrl,
328
+ headers: {
329
+ Accept: "application/json",
330
+ "Content-Type": "application/json"
331
+ }
332
+ });
333
+ const ff = new FeeeF2({
334
+ apiKey: "",
335
+ baseURL: settings.apiUrl,
336
+ client: httpClient,
337
+ cache: false
338
+ });
339
+ const tokenResponse = await ff.oauth.exchangeAuthorizationCode({
340
+ code,
341
+ redirectUri: settings.redirectUri,
342
+ clientId: settings.clientId,
343
+ ...settings.clientSecret ? { clientSecret: settings.clientSecret } : {},
344
+ codeVerifier: verifier
345
+ });
346
+ const accessToken = tokenResponse.access_token;
347
+ if (!accessToken) {
348
+ throw new Error("Token exchange returned no access_token");
349
+ }
350
+ saveConfig({
351
+ token: accessToken,
352
+ apiUrl: settings.apiUrl
353
+ });
354
+ ff.setHeader("Authorization", `Bearer ${accessToken}`);
355
+ try {
356
+ const me = await ff.users.me();
357
+ const payload = me;
358
+ const email = payload.user?.email || payload.email;
359
+ if (email) {
360
+ saveConfig({ email });
361
+ return { email };
362
+ }
363
+ return {};
364
+ } catch {
365
+ return {};
366
+ }
367
+ }
368
+
369
+ // src/ui.ts
370
+ import * as p from "@clack/prompts";
371
+ import pc from "picocolors";
372
+ function banner() {
373
+ console.log();
374
+ console.log(pc.cyan(" feeef") + pc.dim(" developer cli"));
375
+ console.log();
376
+ }
377
+ function intro2(title) {
378
+ p.intro(pc.bgCyan(pc.black(` ${title} `)));
379
+ }
380
+ function outro2(message) {
381
+ p.outro(pc.green(message));
382
+ }
383
+ function note2(message, title) {
384
+ p.note(message, title);
385
+ }
386
+ function cancel2(message = "Cancelled.") {
387
+ p.cancel(message);
388
+ process.exit(0);
389
+ }
390
+ async function text2(opts) {
391
+ const value = await p.text(opts);
392
+ if (p.isCancel(value)) cancel2();
393
+ return String(value);
394
+ }
395
+ async function password2(opts) {
396
+ const value = await p.password(opts);
397
+ if (p.isCancel(value)) cancel2();
398
+ return String(value);
399
+ }
400
+ async function confirm2(opts) {
401
+ const value = await p.confirm(opts);
402
+ if (p.isCancel(value)) cancel2();
403
+ return Boolean(value);
404
+ }
405
+ async function select2(opts) {
406
+ const value = await p.select(opts);
407
+ if (p.isCancel(value)) cancel2();
408
+ return value;
409
+ }
410
+ function fail(err) {
411
+ const anyErr = err;
412
+ let message;
413
+ if (anyErr?.response?.data != null) {
414
+ message = typeof anyErr.response.data === "string" ? anyErr.response.data : JSON.stringify(anyErr.response.data).slice(0, 1500);
415
+ } else if (err instanceof Error) {
416
+ message = err.message;
417
+ } else {
418
+ message = String(err);
419
+ }
420
+ p.log.error(pc.red(message));
421
+ process.exit(1);
422
+ }
423
+ function ok(message) {
424
+ p.log.success(message);
425
+ }
426
+ function info(message) {
427
+ p.log.info(message);
428
+ }
429
+ function warn(message) {
430
+ p.log.warn(message);
431
+ }
432
+ function spinner2() {
433
+ return p.spinner();
434
+ }
435
+
436
+ // src/commands/auth.ts
437
+ async function runSignin(opts) {
438
+ intro2("feeef signin");
439
+ try {
440
+ const usePassword = Boolean(opts.passwordAuth) || Boolean(opts.email);
441
+ if (!usePassword) {
442
+ const s2 = spinner2();
443
+ s2.start("Waiting for browser authorization\u2026");
444
+ const result = await signinWithOAuth({
445
+ ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {},
446
+ onAuthorizeUrl: (url) => {
447
+ s2.stop("Open this URL if the browser did not launch:");
448
+ note2(url, "Authorize");
449
+ s2.start("Waiting for callback on localhost.feeef.org:7777\u2026");
450
+ }
451
+ });
452
+ s2.stop(
453
+ result.email ? `Signed in as ${result.email}` : "Signed in (token saved)"
454
+ );
455
+ info(`Config: ${configPath()}`);
456
+ outro2("Ready. Try: feeef whoami");
457
+ return;
458
+ }
459
+ const email = opts.email || await text2({
460
+ message: "Email",
461
+ placeholder: "you@example.com",
462
+ validate: (v) => !v?.includes("@") ? "Enter a valid email" : void 0
463
+ });
464
+ const pass = opts.password || await password2({
465
+ message: "Password",
466
+ validate: (v) => !v || v.length < 8 ? "Min 8 characters" : void 0
467
+ });
468
+ const s = spinner2();
469
+ s.start("Signing in\u2026");
470
+ const auth = await signinWithPassword({
471
+ email,
472
+ password: pass,
473
+ ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
474
+ });
475
+ s.stop(`Signed in as ${auth.user?.email || email}`);
476
+ info(`Config: ${configPath()}`);
477
+ outro2("Ready. Try: feeef whoami");
478
+ } catch (e) {
479
+ fail(e);
480
+ }
481
+ }
482
+ async function runWhoami() {
483
+ intro2("feeef whoami");
484
+ try {
485
+ const { ff, cfg } = createClient();
486
+ const me = await ff.users.me();
487
+ const payload = me;
488
+ const user = payload.user ?? payload;
489
+ ok(String(user.email || user.id || JSON.stringify(me)));
490
+ info(`API ${defaultApiUrl(cfg)}`);
491
+ outro2("Authenticated");
492
+ } catch (e) {
493
+ fail(e);
494
+ }
495
+ }
496
+ function runSignout() {
497
+ intro2("feeef signout");
498
+ clearAuth();
499
+ ok(`Cleared token (${configPath()})`);
500
+ outro2("Signed out");
501
+ }
502
+ function runConfigShow() {
503
+ intro2("feeef config");
504
+ const cfg = loadConfig();
505
+ console.log(
506
+ JSON.stringify(
507
+ {
508
+ path: configPath(),
509
+ email: cfg.email,
510
+ apiUrl: defaultApiUrl(cfg),
511
+ defaultStoreId: cfg.defaultStoreId,
512
+ lastPublishedTemplateId: cfg.lastPublishedTemplateId,
513
+ storefrontPath: cfg.storefrontPath,
514
+ devPort: cfg.devPort,
515
+ hasToken: Boolean(cfg.token)
516
+ },
517
+ null,
518
+ 2
519
+ )
520
+ );
521
+ outro2("Done");
522
+ }
523
+ var CONFIG_KEYS = [
524
+ "apiUrl",
525
+ "defaultStoreId",
526
+ "storefrontPath",
527
+ "devPort",
528
+ "lastPublishedTemplateId"
529
+ ];
530
+ function runConfigSet(key, value) {
531
+ intro2("feeef config set");
532
+ if (!CONFIG_KEYS.includes(key)) {
533
+ fail(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}`);
534
+ }
535
+ const typedKey = key;
536
+ if (typedKey === "devPort") {
537
+ const n = Number(value);
538
+ if (!Number.isFinite(n) || n <= 0) {
539
+ fail("devPort must be a positive number");
540
+ }
541
+ saveConfig({ devPort: n });
542
+ } else {
543
+ const patch = { [typedKey]: value };
544
+ saveConfig(patch);
545
+ }
546
+ ok(`Set ${typedKey}`);
547
+ outro2(configPath());
548
+ }
549
+
550
+ // src/commands/template/add.ts
551
+ import path4 from "path";
552
+
553
+ // src/project.ts
554
+ import fs2 from "fs";
555
+ import path2 from "path";
556
+ var FEEF_RC = ".feeefrc";
557
+ var TEMPLATE_META = "feeef.template.json";
558
+ function findThemeRoot(startDir = process.cwd()) {
559
+ let dir = path2.resolve(startDir);
560
+ for (; ; ) {
561
+ if (fs2.existsSync(path2.join(dir, TEMPLATE_META))) return dir;
562
+ const parent = path2.dirname(dir);
563
+ if (parent === dir) return null;
564
+ dir = parent;
565
+ }
566
+ }
567
+ function loadProjectRc(themeRoot) {
568
+ const rcPath = path2.join(themeRoot, FEEF_RC);
569
+ try {
570
+ if (!fs2.existsSync(rcPath)) return {};
571
+ return JSON.parse(fs2.readFileSync(rcPath, "utf8"));
572
+ } catch {
573
+ return {};
574
+ }
575
+ }
576
+ function saveProjectRc(themeRoot, patch) {
577
+ const rcPath = path2.join(themeRoot, FEEF_RC);
578
+ const next = { ...loadProjectRc(themeRoot), ...patch };
579
+ for (const key of Object.keys(next)) {
580
+ const value = next[key];
581
+ if (value === void 0 || value === null || value === "") {
582
+ delete next[key];
583
+ }
584
+ }
585
+ fs2.writeFileSync(rcPath, `${JSON.stringify(next, null, 2)}
586
+ `, "utf8");
587
+ return { rcPath, rc: next };
588
+ }
589
+ function loadTemplateMeta(themeRoot) {
590
+ const metaPath = path2.join(themeRoot, TEMPLATE_META);
591
+ if (!fs2.existsSync(metaPath)) return {};
592
+ try {
593
+ return JSON.parse(fs2.readFileSync(metaPath, "utf8"));
594
+ } catch {
595
+ return {};
596
+ }
597
+ }
598
+ function resolveStoreIdFromProject(input) {
599
+ const rc = loadProjectRc(input.themeRoot);
600
+ const meta = loadTemplateMeta(input.themeRoot);
601
+ return input.storeFlag || rc.store || meta.store || meta.storeId || input.globalCfg.defaultStoreId || null;
602
+ }
603
+ function resolveThemeDir(dirArg) {
604
+ if (dirArg) {
605
+ const abs = path2.resolve(dirArg);
606
+ if (!fs2.existsSync(abs)) {
607
+ throw new Error(`Theme directory not found: ${abs}`);
608
+ }
609
+ return findThemeRoot(abs) || abs;
610
+ }
611
+ const fromCwd = findThemeRoot(process.cwd());
612
+ if (fromCwd) return fromCwd;
613
+ throw new Error(
614
+ "No theme found. Run inside a theme folder (feeef.template.json) or: feeef template init"
615
+ );
616
+ }
617
+
618
+ // src/theme-scaffold.ts
619
+ import fs3 from "fs";
620
+ import path3 from "path";
621
+ var PAGE_ID_RE = /^[a-z][a-z0-9_]*$/;
622
+ var COMPONENT_ID_RE = /^[a-z][a-z0-9_-]*$/;
623
+ function assertPageId(id) {
624
+ const trimmed = id.trim();
625
+ if (!PAGE_ID_RE.test(trimmed)) {
626
+ throw new Error(
627
+ `Invalid page id "${id}" \u2014 use lowercase letters, digits, underscore (e.g. home, thank_you)`
628
+ );
629
+ }
630
+ return trimmed;
631
+ }
632
+ function assertComponentId(id) {
633
+ const trimmed = id.trim();
634
+ if (!COMPONENT_ID_RE.test(trimmed)) {
635
+ throw new Error(
636
+ `Invalid component id "${id}" \u2014 use lowercase letters, digits, hyphen/underscore (e.g. hero, product-grid)`
637
+ );
638
+ }
639
+ return trimmed;
640
+ }
641
+ function titleCaseFromId(id) {
642
+ return id.split(/[-_]/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
643
+ }
644
+ function readMeta(themeRoot) {
645
+ const metaPath = path3.join(themeRoot, TEMPLATE_META);
646
+ if (!fs3.existsSync(metaPath)) {
647
+ throw new Error(`Missing ${TEMPLATE_META} in ${themeRoot}`);
648
+ }
649
+ return JSON.parse(fs3.readFileSync(metaPath, "utf8"));
650
+ }
651
+ function writeMeta(themeRoot, meta) {
652
+ const metaPath = path3.join(themeRoot, TEMPLATE_META);
653
+ fs3.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}
654
+ `, "utf8");
655
+ }
656
+ function pageUsesLegacySections(themeRoot, pageId) {
657
+ const flatDir = path3.join(themeRoot, "pages", pageId, "components");
658
+ if (fs3.existsSync(flatDir)) return false;
659
+ const sectionsDir = path3.join(themeRoot, "pages", pageId, "sections");
660
+ if (!fs3.existsSync(sectionsDir)) return false;
661
+ for (const section of fs3.readdirSync(sectionsDir, { withFileTypes: true })) {
662
+ if (!section.isDirectory() || section.name.startsWith(".")) continue;
663
+ const comps = path3.join(sectionsDir, section.name, "components");
664
+ if (!fs3.existsSync(comps)) continue;
665
+ const entries = fs3.readdirSync(comps).filter((n) => !n.startsWith("."));
666
+ if (entries.length > 0) return true;
667
+ }
668
+ return false;
669
+ }
670
+ function resolveComponentParentDir(themeRoot, pageId, section) {
671
+ if (section) {
672
+ const parentDir = path3.join(
673
+ themeRoot,
674
+ "pages",
675
+ pageId,
676
+ "sections",
677
+ section,
678
+ "components"
679
+ );
680
+ return { parentDir, kind: "legacy" };
681
+ }
682
+ if (pageUsesLegacySections(themeRoot, pageId)) {
683
+ return {
684
+ parentDir: path3.join(
685
+ themeRoot,
686
+ "pages",
687
+ pageId,
688
+ "sections",
689
+ "main",
690
+ "components"
691
+ ),
692
+ kind: "legacy"
693
+ };
694
+ }
695
+ return {
696
+ parentDir: path3.join(themeRoot, "pages", pageId, "components"),
697
+ kind: "flat"
698
+ };
699
+ }
700
+ function formatStarterComponent(opts) {
701
+ const order = typeof opts.order === "number" ? opts.order : 0;
702
+ const titleJson = JSON.stringify(opts.title);
703
+ return `const propsSchema = {
704
+ heading: { type: "string", name: "Heading" },
705
+ } as const;
706
+
707
+ export const meta = {
708
+ type: "custom",
709
+ title: ${titleJson},
710
+ order: ${order},
711
+ propsSchema,
712
+ props: {
713
+ heading: "",
714
+ },
715
+ } as const satisfies FeeefComponentMeta<typeof propsSchema>;
716
+
717
+ type Props = FeeefLivePropsOf<typeof propsSchema>;
718
+
719
+ function App() {
720
+ const p = props as Props;
721
+ return (
722
+ <section style={{ padding: "2rem 1.5rem" }}>
723
+ <h2>{p.heading || ${titleJson}}</h2>
724
+ </section>
725
+ );
726
+ }
727
+ `;
728
+ }
729
+ function nextComponentOrder(parentDir) {
730
+ if (!fs3.existsSync(parentDir)) return 0;
731
+ let max = -1;
732
+ for (const name of fs3.readdirSync(parentDir)) {
733
+ if (name.startsWith(".")) continue;
734
+ const abs = path3.join(parentDir, name);
735
+ let sourcePath = null;
736
+ if (fs3.statSync(abs).isFile() && /\.(tsx|jsx)$/.test(name)) {
737
+ sourcePath = abs;
738
+ } else if (fs3.statSync(abs).isDirectory()) {
739
+ for (const entry of ["component.tsx", "component.jsx"]) {
740
+ const p2 = path3.join(abs, entry);
741
+ if (fs3.existsSync(p2)) {
742
+ sourcePath = p2;
743
+ break;
744
+ }
745
+ }
746
+ }
747
+ if (!sourcePath) continue;
748
+ const src = fs3.readFileSync(sourcePath, "utf8");
749
+ const m = /\border\s*:\s*(\d+)/.exec(src);
750
+ if (m) max = Math.max(max, Number(m[1]));
751
+ }
752
+ return max + 1;
753
+ }
754
+ function addPage(themeRoot, pageIdRaw) {
755
+ const pageId = assertPageId(pageIdRaw);
756
+ const pageDir = path3.join(themeRoot, "pages", pageId);
757
+ if (fs3.existsSync(pageDir)) {
758
+ throw new Error(`Page already exists: pages/${pageId}/`);
759
+ }
760
+ fs3.mkdirSync(path3.join(pageDir, "components"), { recursive: true });
761
+ fs3.writeFileSync(
762
+ path3.join(pageDir, "page.json"),
763
+ `${JSON.stringify({ props: {} }, null, 2)}
764
+ `,
765
+ "utf8"
766
+ );
767
+ const meta = readMeta(themeRoot);
768
+ const pages = Array.isArray(meta.pages) ? [...meta.pages] : [];
769
+ if (!pages.includes(pageId)) pages.push(pageId);
770
+ meta.pages = pages;
771
+ writeMeta(themeRoot, meta);
772
+ return { pageDir, pageId };
773
+ }
774
+ function removePage(themeRoot, pageIdRaw) {
775
+ const pageId = assertPageId(pageIdRaw);
776
+ const pageDir = path3.join(themeRoot, "pages", pageId);
777
+ if (!fs3.existsSync(pageDir)) {
778
+ throw new Error(`Page not found: pages/${pageId}/`);
779
+ }
780
+ fs3.rmSync(pageDir, { recursive: true, force: true });
781
+ const meta = readMeta(themeRoot);
782
+ if (Array.isArray(meta.pages)) {
783
+ meta.pages = meta.pages.filter((id) => id !== pageId);
784
+ writeMeta(themeRoot, meta);
785
+ }
786
+ return { pageId, removedPath: pageDir };
787
+ }
788
+ function addComponent(themeRoot, pageIdRaw, componentIdRaw, opts = {}) {
789
+ const pageId = assertPageId(pageIdRaw);
790
+ const componentId = assertComponentId(componentIdRaw);
791
+ const pageDir = path3.join(themeRoot, "pages", pageId);
792
+ if (!fs3.existsSync(pageDir)) {
793
+ throw new Error(
794
+ `Page not found: pages/${pageId}/ \u2014 run: feeef template add page ${pageId}`
795
+ );
796
+ }
797
+ const { parentDir, kind } = resolveComponentParentDir(
798
+ themeRoot,
799
+ pageId,
800
+ opts.section
801
+ );
802
+ fs3.mkdirSync(parentDir, { recursive: true });
803
+ const filePath = path3.join(parentDir, `${componentId}.tsx`);
804
+ const folderPath = path3.join(parentDir, componentId);
805
+ if (fs3.existsSync(filePath) || fs3.existsSync(folderPath)) {
806
+ throw new Error(
807
+ `Component already exists under ${path3.relative(themeRoot, parentDir)}/${componentId}`
808
+ );
809
+ }
810
+ const order = nextComponentOrder(parentDir);
811
+ const title = opts.title?.trim() || titleCaseFromId(componentId);
812
+ fs3.writeFileSync(filePath, formatStarterComponent({ title, order }), "utf8");
813
+ return { filePath, pageId, componentId, kind };
814
+ }
815
+ function findComponentEntry(themeRoot, pageId, componentId, section) {
816
+ const candidates = [];
817
+ if (section) {
818
+ const base = path3.join(
819
+ themeRoot,
820
+ "pages",
821
+ pageId,
822
+ "sections",
823
+ section,
824
+ "components"
825
+ );
826
+ candidates.push(
827
+ path3.join(base, `${componentId}.tsx`),
828
+ path3.join(base, `${componentId}.jsx`),
829
+ path3.join(base, `${componentId}.json`),
830
+ path3.join(base, componentId)
831
+ );
832
+ } else {
833
+ const flat = path3.join(themeRoot, "pages", pageId, "components");
834
+ candidates.push(
835
+ path3.join(flat, `${componentId}.tsx`),
836
+ path3.join(flat, `${componentId}.jsx`),
837
+ path3.join(flat, `${componentId}.json`),
838
+ path3.join(flat, componentId)
839
+ );
840
+ const sectionsRoot = path3.join(themeRoot, "pages", pageId, "sections");
841
+ if (fs3.existsSync(sectionsRoot)) {
842
+ for (const sec of fs3.readdirSync(sectionsRoot, { withFileTypes: true })) {
843
+ if (!sec.isDirectory() || sec.name.startsWith(".")) continue;
844
+ const base = path3.join(sectionsRoot, sec.name, "components");
845
+ candidates.push(
846
+ path3.join(base, `${componentId}.tsx`),
847
+ path3.join(base, `${componentId}.jsx`),
848
+ path3.join(base, `${componentId}.json`),
849
+ path3.join(base, componentId)
850
+ );
851
+ }
852
+ }
853
+ }
854
+ for (const c of candidates) {
855
+ if (fs3.existsSync(c)) return c;
856
+ }
857
+ return null;
858
+ }
859
+ function removeComponent(themeRoot, pageIdRaw, componentIdRaw, opts = {}) {
860
+ const pageId = assertPageId(pageIdRaw);
861
+ const componentId = assertComponentId(componentIdRaw);
862
+ const entry = findComponentEntry(
863
+ themeRoot,
864
+ pageId,
865
+ componentId,
866
+ opts.section
867
+ );
868
+ if (!entry) {
869
+ throw new Error(
870
+ `Component not found: ${pageId}/${componentId}` + (opts.section ? ` (section ${opts.section})` : "")
871
+ );
872
+ }
873
+ fs3.rmSync(entry, { recursive: true, force: true });
874
+ return { pageId, componentId, removedPath: entry };
875
+ }
876
+
877
+ // src/commands/template/add.ts
878
+ async function runTemplateAddPage(pageId, opts = {}) {
879
+ intro2("feeef template add page");
880
+ try {
881
+ if (!pageId?.trim()) {
882
+ fail("Usage: feeef template add page <pageId>");
883
+ }
884
+ const themeRoot = resolveThemeDir(opts.dir);
885
+ const { pageId: id } = addPage(themeRoot, pageId);
886
+ ok(`Created pages/${id}/ (flat components/)`);
887
+ outro2(`Next: feeef template add component ${id} <name>`);
888
+ } catch (err) {
889
+ fail(err instanceof Error ? err.message : String(err));
890
+ }
891
+ }
892
+ async function runTemplateAddComponent(pageId, componentId, opts = {}) {
893
+ intro2("feeef template add component");
894
+ try {
895
+ if (!pageId?.trim() || !componentId?.trim()) {
896
+ fail("Usage: feeef template add component <pageId> <componentId>");
897
+ }
898
+ const themeRoot = resolveThemeDir(opts.dir);
899
+ const result = addComponent(themeRoot, pageId, componentId, {
900
+ ...opts.title ? { title: opts.title } : {},
901
+ ...opts.section ? { section: opts.section } : {}
902
+ });
903
+ const rel = path4.relative(themeRoot, result.filePath);
904
+ ok(`Created ${rel}`);
905
+ if (result.kind === "legacy") {
906
+ outro2("Added under legacy sections/main (page already uses sections)");
907
+ } else {
908
+ outro2("Edit the file, then npm run build / npm run dev");
909
+ }
910
+ } catch (err) {
911
+ fail(err instanceof Error ? err.message : String(err));
912
+ }
913
+ }
914
+
915
+ // src/commands/template/build.ts
916
+ import path6 from "path";
917
+
918
+ // src/kit.ts
919
+ import fs4 from "fs";
920
+ import path5 from "path";
921
+ import { fileURLToPath, pathToFileURL } from "url";
922
+ var __dirname = path5.dirname(fileURLToPath(import.meta.url));
923
+ function cliRoot() {
924
+ const candidate = path5.resolve(__dirname, "..");
925
+ if (fs4.existsSync(path5.join(candidate, "package.json"))) return candidate;
926
+ return path5.resolve(__dirname, "../..");
927
+ }
928
+ function templateKitRoot() {
929
+ return path5.join(cliRoot(), "vendor", "template-kit");
930
+ }
931
+ function scaffoldsRoot() {
932
+ return path5.join(cliRoot(), "scaffolds");
933
+ }
934
+ async function loadBuild() {
935
+ const buildPath = path5.join(templateKitRoot(), "lib", "build.mjs");
936
+ if (!fs4.existsSync(buildPath)) {
937
+ throw new Error(`template-kit missing at ${buildPath}`);
938
+ }
939
+ return import(pathToFileURL(buildPath).href);
940
+ }
941
+ async function loadUnpack() {
942
+ const unpackPath = path5.join(templateKitRoot(), "lib", "unpack.mjs");
943
+ if (!fs4.existsSync(unpackPath)) {
944
+ throw new Error(`template-kit missing at ${unpackPath}`);
945
+ }
946
+ return import(pathToFileURL(unpackPath).href);
947
+ }
948
+ function kitBin() {
949
+ return path5.join(templateKitRoot(), "bin", "feeef-template.mjs");
950
+ }
951
+
952
+ // src/commands/template/build.ts
953
+ async function runTemplateBuild(dirArg, opts = {}) {
954
+ const label = opts.checkOnly ? "feeef template check" : "feeef template build";
955
+ intro2(label);
956
+ try {
957
+ const srcDir = resolveThemeDir(dirArg);
958
+ const outPath = path6.join(srcDir, "dist", "data.json");
959
+ const s = spinner2();
960
+ s.start(opts.checkOnly ? "Checking theme\u2026" : "Building theme\u2026");
961
+ const { buildTemplate } = await loadBuild();
962
+ const result = buildTemplate({
963
+ srcDir,
964
+ outPath,
965
+ write: !opts.checkOnly
966
+ });
967
+ for (const w of result.warnings || []) {
968
+ warn(`[${w.path}] ${w.message}`);
969
+ }
970
+ s.stop(
971
+ opts.checkOnly ? `OK \u2014 ${result.pageCount ?? "?"} pages / ${result.componentCount ?? "?"} components` : `Built \u2192 ${result.outPath}`
972
+ );
973
+ if (!opts.checkOnly) {
974
+ if (result.localeCount) info(`${result.localeCount} locales`);
975
+ if (result.libraryCount) info(`${result.libraryCount} library entries`);
976
+ }
977
+ ok(opts.checkOnly ? "Check passed" : "Build complete");
978
+ outro2(opts.checkOnly ? "Ready to publish" : path6.relative(process.cwd(), outPath) || outPath);
979
+ } catch (e) {
980
+ fail(e);
981
+ }
982
+ }
983
+
984
+ // src/commands/template/dev.ts
985
+ import { spawn } from "child_process";
986
+ import fs6 from "fs";
987
+ import path8 from "path";
988
+
989
+ // src/storefront.ts
990
+ import fs5 from "fs";
991
+ import path7 from "path";
992
+ function resolveStorefrontPath() {
993
+ const fromEnv = process.env["FEEEF_STOREFRONT_PATH"]?.trim();
994
+ if (fromEnv && fs5.existsSync(path7.join(fromEnv, "package.json"))) {
995
+ return path7.resolve(fromEnv);
996
+ }
997
+ const cfg = loadConfig();
998
+ if (cfg.storefrontPath && fs5.existsSync(path7.join(cfg.storefrontPath, "package.json"))) {
999
+ return path7.resolve(cfg.storefrontPath);
1000
+ }
1001
+ const sibling = path7.resolve(cliRoot(), "../storefront");
1002
+ if (fs5.existsSync(path7.join(sibling, "package.json"))) {
1003
+ return sibling;
1004
+ }
1005
+ return null;
1006
+ }
1007
+ function assertStorefrontPath() {
1008
+ const root = resolveStorefrontPath();
1009
+ if (!root) {
1010
+ throw new Error(
1011
+ [
1012
+ "Lithium storefront not found.",
1013
+ "Set it once (theme repos do not need storefront source inside them):",
1014
+ " feeef config set storefrontPath /path/to/storefront",
1015
+ "or:",
1016
+ " export FEEEF_STOREFRONT_PATH=/path/to/storefront"
1017
+ ].join("\n")
1018
+ );
1019
+ }
1020
+ return root;
1021
+ }
1022
+
1023
+ // src/commands/template/dev.ts
1024
+ function killChildren(children) {
1025
+ for (const c of children) {
1026
+ try {
1027
+ c.kill("SIGTERM");
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ }
1032
+ function debounce(fn, ms) {
1033
+ let timer;
1034
+ const wrapped = ((...args) => {
1035
+ if (timer) clearTimeout(timer);
1036
+ timer = setTimeout(() => fn(...args), ms);
1037
+ });
1038
+ wrapped.cancel = () => {
1039
+ if (timer) clearTimeout(timer);
1040
+ };
1041
+ return wrapped;
1042
+ }
1043
+ async function resolveBoundStore(srcDir) {
1044
+ const cfg = loadConfig();
1045
+ if (!cfg.token) {
1046
+ throw new Error(
1047
+ "Not signed in. Run: feeef signin\nThen bind a store: feeef use <slug>"
1048
+ );
1049
+ }
1050
+ const storeRef = resolveStoreIdFromProject({
1051
+ themeRoot: srcDir,
1052
+ globalCfg: cfg
1053
+ });
1054
+ if (!storeRef) {
1055
+ throw new Error(
1056
+ "No store linked. Run: feeef use <slug>\n(Remote preview pushes a draft to your store \u2014 live theme stays unchanged.)"
1057
+ );
1058
+ }
1059
+ const { ff } = createClient({ requireAuth: true });
1060
+ let store;
1061
+ try {
1062
+ store = await ff.stores.find({ id: storeRef });
1063
+ } catch {
1064
+ store = await ff.stores.find({ id: storeRef, by: "slug" });
1065
+ }
1066
+ return { storeId: store.id, slug: store.slug };
1067
+ }
1068
+ async function runRemoteDraftPreview(srcDir) {
1069
+ const outPath = path8.join(srcDir, "dist", "data.json");
1070
+ const { storeId, slug } = await resolveBoundStore(srcDir);
1071
+ const { ff } = createClient({ requireAuth: true });
1072
+ const s = spinner2();
1073
+ s.start("Building theme\u2026");
1074
+ const { buildTemplate } = await loadBuild();
1075
+ buildTemplate({ srcDir, outPath, write: true });
1076
+ s.stop("dist ready");
1077
+ let lastPreviewUrl;
1078
+ let pushInFlight = false;
1079
+ let pushQueued = false;
1080
+ const pushDraft = async () => {
1081
+ if (pushInFlight) {
1082
+ pushQueued = true;
1083
+ return;
1084
+ }
1085
+ pushInFlight = true;
1086
+ try {
1087
+ if (!fs6.existsSync(outPath)) {
1088
+ info("Waiting for dist/data.json\u2026");
1089
+ return;
1090
+ }
1091
+ const data = JSON.parse(fs6.readFileSync(outPath, "utf8"));
1092
+ const result = await ff.stores.putTemplatePreview(storeId, { data });
1093
+ lastPreviewUrl = result.previewUrl;
1094
+ ok(`Draft preview updated \u2192 ${pc.cyan(result.previewUrl)}`);
1095
+ } catch (e) {
1096
+ fail(e);
1097
+ } finally {
1098
+ pushInFlight = false;
1099
+ if (pushQueued) {
1100
+ pushQueued = false;
1101
+ await pushDraft();
1102
+ }
1103
+ }
1104
+ };
1105
+ const debouncedPush = debounce(() => {
1106
+ void pushDraft();
1107
+ }, 400);
1108
+ await pushDraft();
1109
+ if (lastPreviewUrl) {
1110
+ openBrowser(lastPreviewUrl);
1111
+ }
1112
+ note2(
1113
+ [
1114
+ `Theme ${srcDir}`,
1115
+ `Store ${slug} (${storeId})`,
1116
+ `Preview ${lastPreviewUrl ?? "(pending)"}`,
1117
+ "",
1118
+ "Live customers still see metadata.templateData.",
1119
+ "Exit preview in the browser: banner \u2192 Exit, or /?preview=0",
1120
+ "Stopping the CLI clears the draft."
1121
+ ].join("\n"),
1122
+ "Remote draft preview"
1123
+ );
1124
+ const watch = spawn(
1125
+ process.execPath,
1126
+ [kitBin(), "watch", srcDir, "-o", outPath],
1127
+ { cwd: srcDir, env: process.env, stdio: "inherit" }
1128
+ );
1129
+ let watching = true;
1130
+ const onDistChange = () => {
1131
+ if (!watching) return;
1132
+ debouncedPush();
1133
+ };
1134
+ try {
1135
+ fs6.watch(path8.dirname(outPath), { persistent: true }, (_event, filename) => {
1136
+ if (!filename || filename === "data.json" || String(filename).endsWith("data.json")) {
1137
+ onDistChange();
1138
+ }
1139
+ });
1140
+ } catch {
1141
+ setInterval(() => onDistChange(), 2e3).unref();
1142
+ }
1143
+ const shutdown = async (code = 0) => {
1144
+ watching = false;
1145
+ debouncedPush.cancel();
1146
+ killChildren([watch]);
1147
+ try {
1148
+ await ff.stores.clearTemplatePreview(storeId);
1149
+ info("Cleared draft preview on server");
1150
+ } catch {
1151
+ }
1152
+ process.exit(code);
1153
+ };
1154
+ process.on("SIGINT", () => {
1155
+ void shutdown(0);
1156
+ });
1157
+ process.on("SIGTERM", () => {
1158
+ void shutdown(0);
1159
+ });
1160
+ watch.on("exit", (code) => {
1161
+ if (code && code !== 0) void shutdown(code);
1162
+ });
1163
+ ok(`Open ${pc.cyan(lastPreviewUrl ?? `https://${slug}.feeef.org`)}`);
1164
+ outro2("Watching \u2014 Ctrl+C to stop (clears draft)");
1165
+ await new Promise(() => {
1166
+ });
1167
+ }
1168
+ async function runLocalStorefrontPreview(srcDir, opts) {
1169
+ const outPath = path8.join(srcDir, "dist", "data.json");
1170
+ const libraryPath = path8.join(srcDir, "dist", "schema.library.json");
1171
+ const port = Number(opts.port) || defaultDevPort();
1172
+ if (opts.storefront) {
1173
+ process.env["FEEEF_STOREFRONT_PATH"] = path8.resolve(opts.storefront);
1174
+ }
1175
+ const storefrontRoot = assertStorefrontPath();
1176
+ const cfg = loadConfig();
1177
+ const storeRef = resolveStoreIdFromProject({
1178
+ themeRoot: srcDir,
1179
+ globalCfg: cfg
1180
+ });
1181
+ const rc = loadProjectRc(srcDir);
1182
+ let slug = rc.slug;
1183
+ if (!slug && storeRef) {
1184
+ try {
1185
+ const { ff } = createClient({ requireAuth: Boolean(cfg.token) });
1186
+ if (cfg.token) {
1187
+ try {
1188
+ const store = await ff.stores.find({ id: storeRef });
1189
+ slug = store.slug;
1190
+ } catch {
1191
+ try {
1192
+ const store = await ff.stores.find({ id: storeRef, by: "slug" });
1193
+ slug = store.slug;
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ const s = spinner2();
1202
+ s.start("Building theme\u2026");
1203
+ const { buildTemplate } = await loadBuild();
1204
+ buildTemplate({ srcDir, outPath, write: true });
1205
+ s.stop("dist ready");
1206
+ const previewHost = slug ? `http://${slug}.localhost.feeef.org:${port}` : `http://localhost:${port}`;
1207
+ note2(
1208
+ [
1209
+ `Theme ${srcDir}`,
1210
+ `Storefront ${storefrontRoot}`,
1211
+ `Preview ${previewHost}`,
1212
+ "Internal --local mode (not for public docs)."
1213
+ ].join("\n"),
1214
+ "Local Next preview"
1215
+ );
1216
+ const env = {
1217
+ ...process.env,
1218
+ FEEEF_TEMPLATE_DATA_PATH: outPath,
1219
+ FEEEF_TEMPLATE_LIBRARY_PATH: libraryPath,
1220
+ PORT: String(port)
1221
+ };
1222
+ if (slug) {
1223
+ env["NEXT_PUBLIC_DEV_STORE_SLUG"] = slug;
1224
+ }
1225
+ const watch = spawn(
1226
+ process.execPath,
1227
+ [kitBin(), "watch", srcDir, "-o", outPath],
1228
+ { cwd: srcDir, env, stdio: "inherit" }
1229
+ );
1230
+ const next = spawn(
1231
+ "npx",
1232
+ ["next", "dev", "--turbopack", "-H", "0.0.0.0", "-p", String(port)],
1233
+ { cwd: storefrontRoot, env, stdio: "inherit", shell: true }
1234
+ );
1235
+ const children = [watch, next];
1236
+ const shutdown = (code = 0) => {
1237
+ killChildren(children);
1238
+ process.exit(code);
1239
+ };
1240
+ process.on("SIGINT", () => shutdown(0));
1241
+ process.on("SIGTERM", () => shutdown(0));
1242
+ next.on("exit", (code) => shutdown(code ?? 0));
1243
+ watch.on("exit", (code) => {
1244
+ if (code && code !== 0) shutdown(code);
1245
+ });
1246
+ ok(`Open ${pc.cyan(previewHost)}`);
1247
+ outro2("Watching \u2014 Ctrl+C to stop");
1248
+ await new Promise(() => {
1249
+ });
1250
+ }
1251
+ async function runTemplateDev(dirArg, opts) {
1252
+ intro2("feeef template dev");
1253
+ try {
1254
+ const srcDir = resolveThemeDir(dirArg);
1255
+ if (opts.local || opts.storefront) {
1256
+ await runLocalStorefrontPreview(srcDir, opts);
1257
+ return;
1258
+ }
1259
+ await runRemoteDraftPreview(srcDir);
1260
+ } catch (e) {
1261
+ fail(e);
1262
+ }
1263
+ }
1264
+
1265
+ // src/commands/template/init.ts
1266
+ import fs10 from "fs";
1267
+ import os2 from "os";
1268
+ import path12 from "path";
1269
+ import { TemplateComponentPolicy } from "feeef";
1270
+
1271
+ // src/kits.ts
1272
+ import { execFileSync } from "child_process";
1273
+ import fs7 from "fs";
1274
+ import path9 from "path";
1275
+ var DEFAULT_BLANK_KIT_GIT = "https://github.com/feeef/theme-blank.git";
1276
+ function resolveBlankKitGitUrl(root = cliRoot()) {
1277
+ const raw = process.env["FEEEF_THEME_BLANK_GIT"];
1278
+ if (raw !== void 0) {
1279
+ const trimmed = raw.trim();
1280
+ if (trimmed === "" || trimmed === "0" || trimmed === "local") {
1281
+ return void 0;
1282
+ }
1283
+ return trimmed;
1284
+ }
1285
+ const monorepoKit = path9.resolve(root, "..", "theme-kits", "blank");
1286
+ if (fs7.existsSync(path9.join(monorepoKit, ".git"))) {
1287
+ return monorepoKit;
1288
+ }
1289
+ return DEFAULT_BLANK_KIT_GIT;
1290
+ }
1291
+ function cloneKitFromGit(opts) {
1292
+ const dest = path9.resolve(opts.dest);
1293
+ if (fs7.existsSync(dest)) {
1294
+ const entries = fs7.readdirSync(dest);
1295
+ if (entries.length > 0) {
1296
+ throw new Error(`Destination not empty: ${dest}`);
1297
+ }
1298
+ fs7.rmSync(dest, { recursive: true, force: true });
1299
+ }
1300
+ const depth = opts.depth ?? 1;
1301
+ execFileSync(
1302
+ "git",
1303
+ ["clone", `--depth=${depth}`, opts.url, dest],
1304
+ { stdio: ["ignore", "pipe", "pipe"] }
1305
+ );
1306
+ const gitDir = path9.join(dest, ".git");
1307
+ if (fs7.existsSync(gitDir)) {
1308
+ fs7.rmSync(gitDir, { recursive: true, force: true });
1309
+ }
1310
+ return { method: "git", source: opts.url };
1311
+ }
1312
+ function copyKitFromLocal(opts) {
1313
+ if (!fs7.existsSync(opts.src)) {
1314
+ throw new Error(`Local kit missing: ${opts.src}`);
1315
+ }
1316
+ opts.copyDir(opts.src, opts.dest);
1317
+ return { method: "local", source: opts.src };
1318
+ }
1319
+
1320
+ // src/theme-package.ts
1321
+ import fs8 from "fs";
1322
+ import path10 from "path";
1323
+ function packageNameFromFolder(name) {
1324
+ return name.toLowerCase();
1325
+ }
1326
+ var DEFAULT_PACKAGE_JSON = {
1327
+ name: "feeef-theme",
1328
+ private: true,
1329
+ version: "0.1.0",
1330
+ description: "Feeef Lithium theme",
1331
+ type: "module",
1332
+ engines: { node: ">=18" },
1333
+ scripts: {
1334
+ build: "feeef template build",
1335
+ check: "feeef template check",
1336
+ dev: "feeef template dev",
1337
+ publish: "feeef template publish",
1338
+ typecheck: "tsc --noEmit"
1339
+ },
1340
+ dependencies: {},
1341
+ devDependencies: {
1342
+ "@types/react": "^19.0.0",
1343
+ "@types/react-dom": "^19.0.0",
1344
+ typescript: "^5.4.5"
1345
+ }
1346
+ };
1347
+ var DEFAULT_GITIGNORE = `node_modules/
1348
+ dist/
1349
+ .env
1350
+ .env.local
1351
+ .DS_Store
1352
+ `;
1353
+ var AGENT_PACK_ENTRIES = ["AGENTS.md", "docs", ".cursor"];
1354
+ var EDITOR_PACK_ENTRIES = [
1355
+ "tsconfig.json",
1356
+ "types",
1357
+ ".vscode"
1358
+ ];
1359
+ var EDITOR_DEV_DEPS = {
1360
+ "@types/react": "^19.0.0",
1361
+ "@types/react-dom": "^19.0.0",
1362
+ typescript: "^5.4.5"
1363
+ };
1364
+ function copyDir(src, dest) {
1365
+ fs8.mkdirSync(dest, { recursive: true });
1366
+ for (const entry of fs8.readdirSync(src, { withFileTypes: true })) {
1367
+ if (entry.name === "." || entry.name === "..") continue;
1368
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".env" || entry.name.startsWith(".env.")) {
1369
+ continue;
1370
+ }
1371
+ const from = path10.join(src, entry.name);
1372
+ const to = path10.join(dest, entry.name);
1373
+ if (entry.isDirectory()) copyDir(from, to);
1374
+ else fs8.copyFileSync(from, to);
1375
+ }
1376
+ }
1377
+ function copyEntryIfMissing(from, to) {
1378
+ if (!fs8.existsSync(from) || fs8.existsSync(to)) return false;
1379
+ const st = fs8.statSync(from);
1380
+ if (st.isDirectory()) copyDir(from, to);
1381
+ else {
1382
+ fs8.mkdirSync(path10.dirname(to), { recursive: true });
1383
+ fs8.copyFileSync(from, to);
1384
+ }
1385
+ return true;
1386
+ }
1387
+ function ensureAgentPack(dest) {
1388
+ const agentsPath = path10.join(dest, "AGENTS.md");
1389
+ if (fs8.existsSync(agentsPath)) return false;
1390
+ const blank = path10.join(scaffoldsRoot(), "blank");
1391
+ let copied = false;
1392
+ for (const name of AGENT_PACK_ENTRIES) {
1393
+ if (copyEntryIfMissing(path10.join(blank, name), path10.join(dest, name))) {
1394
+ copied = true;
1395
+ }
1396
+ }
1397
+ return copied;
1398
+ }
1399
+ function ensureEditorSupport(dest) {
1400
+ const blank = path10.join(scaffoldsRoot(), "blank");
1401
+ let changed = false;
1402
+ for (const name of EDITOR_PACK_ENTRIES) {
1403
+ if (copyEntryIfMissing(path10.join(blank, name), path10.join(dest, name))) {
1404
+ changed = true;
1405
+ }
1406
+ }
1407
+ const scopeFrom = path10.join(blank, "types", "feeef-live-scope.d.ts");
1408
+ const scopeTo = path10.join(dest, "types", "feeef-live-scope.d.ts");
1409
+ if (copyEntryIfMissing(scopeFrom, scopeTo)) changed = true;
1410
+ const themeSchemaFrom = path10.join(blank, "types", "feeef-template-schema.d.ts");
1411
+ const themeSchemaTo = path10.join(dest, "types", "feeef-template-schema.d.ts");
1412
+ if (copyEntryIfMissing(themeSchemaFrom, themeSchemaTo)) changed = true;
1413
+ if (copyEntryIfMissing(path10.join(blank, "schema.ts"), path10.join(dest, "schema.ts"))) {
1414
+ changed = true;
1415
+ }
1416
+ if (copyEntryIfMissing(path10.join(blank, "props.json"), path10.join(dest, "props.json"))) {
1417
+ changed = true;
1418
+ }
1419
+ const pkgPath = path10.join(dest, "package.json");
1420
+ if (fs8.existsSync(pkgPath)) {
1421
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf8"));
1422
+ const scripts = pkg.scripts && typeof pkg.scripts === "object" ? { ...pkg.scripts } : {};
1423
+ if (!scripts.typecheck) {
1424
+ scripts.typecheck = "tsc --noEmit";
1425
+ pkg.scripts = scripts;
1426
+ changed = true;
1427
+ }
1428
+ const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? { ...pkg.devDependencies } : {};
1429
+ for (const [name, version] of Object.entries(EDITOR_DEV_DEPS)) {
1430
+ if (!devDeps[name]) {
1431
+ devDeps[name] = version;
1432
+ changed = true;
1433
+ }
1434
+ }
1435
+ pkg.devDependencies = devDeps;
1436
+ fs8.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
1437
+ `, "utf8");
1438
+ }
1439
+ return changed;
1440
+ }
1441
+ function ensureNodePackage(dest, folderName) {
1442
+ const pkgName = packageNameFromFolder(folderName);
1443
+ const pkgPath = path10.join(dest, "package.json");
1444
+ if (fs8.existsSync(pkgPath)) {
1445
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf8"));
1446
+ pkg.name = pkgName;
1447
+ if (!pkg.scripts || typeof pkg.scripts !== "object") {
1448
+ pkg.scripts = { ...DEFAULT_PACKAGE_JSON.scripts };
1449
+ }
1450
+ const scripts = pkg.scripts;
1451
+ if (!scripts.typecheck) scripts.typecheck = "tsc --noEmit";
1452
+ const devDeps = pkg.devDependencies && typeof pkg.devDependencies === "object" ? pkg.devDependencies : {};
1453
+ for (const [name, version] of Object.entries(EDITOR_DEV_DEPS)) {
1454
+ if (!devDeps[name]) devDeps[name] = version;
1455
+ }
1456
+ pkg.devDependencies = devDeps;
1457
+ fs8.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
1458
+ `, "utf8");
1459
+ } else {
1460
+ fs8.writeFileSync(
1461
+ pkgPath,
1462
+ `${JSON.stringify({ ...DEFAULT_PACKAGE_JSON, name: pkgName }, null, 2)}
1463
+ `,
1464
+ "utf8"
1465
+ );
1466
+ }
1467
+ const gitignorePath = path10.join(dest, ".gitignore");
1468
+ if (!fs8.existsSync(gitignorePath)) {
1469
+ fs8.writeFileSync(gitignorePath, DEFAULT_GITIGNORE, "utf8");
1470
+ } else {
1471
+ const existing = fs8.readFileSync(gitignorePath, "utf8");
1472
+ const extras = [];
1473
+ if (!/(^|\/)node_modules\/?$/m.test(existing) && !existing.includes("node_modules")) {
1474
+ extras.push("node_modules/");
1475
+ }
1476
+ if (!/(^|\/)dist\/?$/m.test(existing) && !existing.includes("dist/")) {
1477
+ extras.push("dist/");
1478
+ }
1479
+ if (extras.length > 0) {
1480
+ const sep = existing.endsWith("\n") ? "" : "\n";
1481
+ fs8.writeFileSync(
1482
+ gitignorePath,
1483
+ `${existing}${sep}${extras.join("\n")}
1484
+ `,
1485
+ "utf8"
1486
+ );
1487
+ }
1488
+ }
1489
+ }
1490
+
1491
+ // src/theme.ts
1492
+ import fs9 from "fs";
1493
+ import path11 from "path";
1494
+ function collectPublishableComponents(srcDir) {
1495
+ const out = [];
1496
+ for (const kind of ["shared", "library"]) {
1497
+ const root = path11.join(srcDir, kind, "components");
1498
+ if (!fs9.existsSync(root)) continue;
1499
+ const byId = /* @__PURE__ */ new Map();
1500
+ for (const ent of fs9.readdirSync(root, { withFileTypes: true })) {
1501
+ if (ent.name.startsWith(".")) continue;
1502
+ if (ent.isDirectory()) {
1503
+ byId.set(ent.name, path11.join(root, ent.name));
1504
+ }
1505
+ }
1506
+ for (const ent of fs9.readdirSync(root, { withFileTypes: true })) {
1507
+ if (ent.name.startsWith(".") || !ent.isFile()) continue;
1508
+ const m = /^(.*)\.(tsx|jsx|json)$/.exec(ent.name);
1509
+ if (!m) continue;
1510
+ const id = m[1];
1511
+ if (byId.has(id)) continue;
1512
+ byId.set(id, path11.join(root, ent.name));
1513
+ }
1514
+ const entries = [...byId.entries()].map(([id, entryPath]) => ({ id, entryPath })).sort((a, b) => a.id.localeCompare(b.id));
1515
+ for (const { id: name, entryPath } of entries) {
1516
+ const isDir = fs9.statSync(entryPath).isDirectory();
1517
+ const metaPath = isDir ? path11.join(entryPath, "component.json") : entryPath.endsWith(".json") ? entryPath : null;
1518
+ const tsxPath = isDir ? path11.join(entryPath, "component.tsx") : entryPath.endsWith(".tsx") ? entryPath : null;
1519
+ const jsxPath = isDir ? path11.join(entryPath, "component.jsx") : entryPath.endsWith(".jsx") ? entryPath : null;
1520
+ const entry = tsxPath && fs9.existsSync(tsxPath) ? tsxPath : jsxPath && fs9.existsSync(jsxPath) ? jsxPath : null;
1521
+ let meta = {};
1522
+ if (metaPath && fs9.existsSync(metaPath)) {
1523
+ meta = JSON.parse(fs9.readFileSync(metaPath, "utf8"));
1524
+ }
1525
+ let code = typeof meta["code"] === "string" ? meta["code"] : "";
1526
+ if (entry) {
1527
+ const source = fs9.readFileSync(entry, "utf8");
1528
+ code = source;
1529
+ const metaMatch = source.match(
1530
+ /export\s+const\s+meta(?:\s+satisfies\s+[^=]+)?\s*=\s*(\{[\s\S]*?\})\s*(?:as\s+const)?\s*;?/
1531
+ );
1532
+ if (metaMatch?.[1]) {
1533
+ try {
1534
+ meta = { ...meta, ...new Function(`return (${metaMatch[1]})`)() };
1535
+ } catch {
1536
+ }
1537
+ }
1538
+ }
1539
+ if (!code.trim() && !(metaPath && fs9.existsSync(metaPath))) continue;
1540
+ if (!code.trim()) continue;
1541
+ const sourceKey = typeof meta["libraryId"] === "string" && meta["libraryId"] || typeof meta["id"] === "string" && meta["id"] || name;
1542
+ out.push({ sourceKey, kind, dir: entryPath, meta, code });
1543
+ }
1544
+ }
1545
+ const seen = /* @__PURE__ */ new Set();
1546
+ return out.filter((c) => {
1547
+ if (seen.has(c.sourceKey)) return false;
1548
+ seen.add(c.sourceKey);
1549
+ return true;
1550
+ });
1551
+ }
1552
+ function rewriteDataToReferences(data, codeToRefId) {
1553
+ if (!data || typeof data !== "object") return data;
1554
+ const walk = (node) => {
1555
+ if (!node || typeof node !== "object") return node;
1556
+ if (Array.isArray(node)) return node.map(walk);
1557
+ const obj = node;
1558
+ if ((obj["type"] === "custom" || !obj["type"]) && typeof obj["code"] === "string" && codeToRefId.has(obj["code"].trim())) {
1559
+ const refId = codeToRefId.get(obj["code"].trim());
1560
+ const {
1561
+ code: _c,
1562
+ propsSchema: _ps,
1563
+ slotsSchema: _ss,
1564
+ ...rest
1565
+ } = obj;
1566
+ return walk({ ...rest, type: "reference", refId });
1567
+ }
1568
+ const out = {};
1569
+ for (const [k, v] of Object.entries(obj)) {
1570
+ out[k] = walk(v);
1571
+ }
1572
+ return out;
1573
+ };
1574
+ return walk(data);
1575
+ }
1576
+ function stripDataI18n(data) {
1577
+ const clone = structuredClone(data);
1578
+ delete clone["i18n"];
1579
+ return clone;
1580
+ }
1581
+ function loadDistLocales(distDir, defaultLocale = "en") {
1582
+ const localesDir = path11.join(distDir, "locales");
1583
+ if (!fs9.existsSync(localesDir)) return [];
1584
+ return fs9.readdirSync(localesDir).filter((f) => f.endsWith(".json")).map((f) => {
1585
+ const locale = path11.basename(f, ".json");
1586
+ const messages = JSON.parse(
1587
+ fs9.readFileSync(path11.join(localesDir, f), "utf8")
1588
+ );
1589
+ return {
1590
+ locale,
1591
+ messages,
1592
+ isDefault: locale === defaultLocale
1593
+ };
1594
+ });
1595
+ }
1596
+ var MAX_REF_DEPTH = 6;
1597
+ var MAX_REF_ROUNDS = 4;
1598
+ function collectTemplateRefIds(data) {
1599
+ const out = /* @__PURE__ */ new Set();
1600
+ const walkComp = (node, depth) => {
1601
+ if (!node || typeof node !== "object" || depth > MAX_REF_DEPTH) return;
1602
+ if (Array.isArray(node)) {
1603
+ for (const item of node) walkComp(item, depth);
1604
+ return;
1605
+ }
1606
+ const obj = node;
1607
+ if (obj["type"] === "reference") {
1608
+ const raw = obj["refId"] ?? obj["ref_id"];
1609
+ if (typeof raw === "string" && raw.trim()) out.add(raw.trim());
1610
+ }
1611
+ if (Array.isArray(obj["children"])) {
1612
+ for (const ch of obj["children"]) walkComp(ch, depth + 1);
1613
+ }
1614
+ if (obj["slots"] && typeof obj["slots"] === "object") {
1615
+ for (const arr of Object.values(obj["slots"])) {
1616
+ if (!Array.isArray(arr)) continue;
1617
+ for (const ch of arr) walkComp(ch, depth + 1);
1618
+ }
1619
+ }
1620
+ };
1621
+ const pages = data?.pages;
1622
+ if (!pages || typeof pages !== "object") return out;
1623
+ for (const page of Object.values(pages)) {
1624
+ const sections = page?.sections;
1625
+ if (!sections) continue;
1626
+ for (const section of Object.values(sections)) {
1627
+ const comps = section?.components;
1628
+ if (!Array.isArray(comps)) continue;
1629
+ for (const c of comps) walkComp(c, 0);
1630
+ }
1631
+ }
1632
+ return out;
1633
+ }
1634
+ function mergeDeep(base, override) {
1635
+ if (!override || typeof override !== "object") return base ? { ...base } : {};
1636
+ if (!base || typeof base !== "object") return { ...override };
1637
+ const out = { ...base };
1638
+ for (const key of Object.keys(override)) {
1639
+ const ov = override[key];
1640
+ const bv = base[key];
1641
+ if (ov !== null && typeof ov === "object" && !Array.isArray(ov) && bv !== null && typeof bv === "object" && !Array.isArray(bv)) {
1642
+ out[key] = mergeDeep(
1643
+ bv,
1644
+ ov
1645
+ );
1646
+ } else {
1647
+ out[key] = ov;
1648
+ }
1649
+ }
1650
+ return out;
1651
+ }
1652
+ function mergeSlots(base, override) {
1653
+ const hasBase = !!(base && Object.keys(base).length > 0);
1654
+ const hasOverride = !!(override && Object.keys(override).length > 0);
1655
+ if (!hasBase && !hasOverride) return null;
1656
+ const out = {};
1657
+ if (hasBase) {
1658
+ for (const [k, v] of Object.entries(base)) {
1659
+ out[k] = Array.isArray(v) ? [...v] : [];
1660
+ }
1661
+ }
1662
+ if (hasOverride) {
1663
+ for (const [k, v] of Object.entries(override)) {
1664
+ if (Array.isArray(v)) out[k] = v;
1665
+ }
1666
+ }
1667
+ return out;
1668
+ }
1669
+ function materializeNode(comp, resolved, depth) {
1670
+ if (!comp || depth > MAX_REF_DEPTH) return comp ?? null;
1671
+ if (Array.isArray(comp["children"])) {
1672
+ const next = [];
1673
+ for (const child of comp["children"]) {
1674
+ const rewritten = materializeNode(
1675
+ child,
1676
+ resolved,
1677
+ depth + 1
1678
+ );
1679
+ if (rewritten) next.push(rewritten);
1680
+ }
1681
+ comp["children"] = next;
1682
+ }
1683
+ if (comp["slots"] && typeof comp["slots"] === "object") {
1684
+ const slots = comp["slots"];
1685
+ for (const [slotKey, arr] of Object.entries(slots)) {
1686
+ if (!Array.isArray(arr)) continue;
1687
+ const next = [];
1688
+ for (const child of arr) {
1689
+ const rewritten = materializeNode(
1690
+ child,
1691
+ resolved,
1692
+ depth + 1
1693
+ );
1694
+ if (rewritten) next.push(rewritten);
1695
+ }
1696
+ slots[slotKey] = next;
1697
+ }
1698
+ }
1699
+ if (comp["type"] !== "reference") return comp;
1700
+ const refId = comp["refId"] ?? comp["ref_id"];
1701
+ if (typeof refId !== "string" || !refId.trim()) return null;
1702
+ const entry = resolved[refId.trim()];
1703
+ if (!entry) return null;
1704
+ const mergedProps = mergeDeep(
1705
+ entry.propsDefault ?? {},
1706
+ comp["props"] ?? {}
1707
+ );
1708
+ const mergedSlots = mergeSlots(
1709
+ entry.slotsDefault,
1710
+ comp["slots"]
1711
+ );
1712
+ const materialized = {
1713
+ ...comp,
1714
+ type: "custom",
1715
+ code: entry.code,
1716
+ propsSchema: entry.propsSchema ?? {},
1717
+ slotsSchema: entry.slotsSchema ?? {},
1718
+ props: mergedProps,
1719
+ slots: mergedSlots
1720
+ };
1721
+ if (entry.slotsLayout != null && materialized["slotsLayout"] == null) {
1722
+ materialized["slotsLayout"] = entry.slotsLayout;
1723
+ }
1724
+ delete materialized["refId"];
1725
+ delete materialized["ref_id"];
1726
+ delete materialized["refVersion"];
1727
+ return materialized;
1728
+ }
1729
+ function rewriteReferencesInTemplateData(data, resolved) {
1730
+ const pages = data["pages"];
1731
+ if (!pages || typeof pages !== "object") return;
1732
+ for (const page of Object.values(pages)) {
1733
+ if (!page || typeof page !== "object") continue;
1734
+ const sections = page["sections"];
1735
+ if (!sections || typeof sections !== "object") continue;
1736
+ for (const section of Object.values(sections)) {
1737
+ if (!section || typeof section !== "object") continue;
1738
+ const comps = section["components"];
1739
+ if (!Array.isArray(comps)) continue;
1740
+ const next = [];
1741
+ for (const c of comps) {
1742
+ const rewritten = materializeNode(
1743
+ c,
1744
+ resolved,
1745
+ 0
1746
+ );
1747
+ if (rewritten) next.push(rewritten);
1748
+ }
1749
+ section["components"] = next;
1750
+ }
1751
+ }
1752
+ }
1753
+ async function materializeTemplateReferences(opts) {
1754
+ const catalog = {};
1755
+ const allMissing = /* @__PURE__ */ new Set();
1756
+ let resolvedCount = 0;
1757
+ for (let round = 0; round < MAX_REF_ROUNDS; round++) {
1758
+ const ids = [...collectTemplateRefIds(opts.data)].filter(
1759
+ (id) => !(id in catalog)
1760
+ );
1761
+ if (ids.length === 0) break;
1762
+ const res = await opts.resolve({
1763
+ storeId: opts.sourceStoreId,
1764
+ ids
1765
+ });
1766
+ const batch = res.resolved ?? {};
1767
+ for (const [id, entry] of Object.entries(batch)) {
1768
+ catalog[id] = entry;
1769
+ resolvedCount += 1;
1770
+ }
1771
+ for (const id of res.missing ?? []) allMissing.add(id);
1772
+ rewriteReferencesInTemplateData(opts.data, catalog);
1773
+ if (Object.keys(batch).length === 0) break;
1774
+ }
1775
+ const leftover = [...collectTemplateRefIds(opts.data)];
1776
+ for (const id of leftover) allMissing.add(id);
1777
+ return {
1778
+ resolved: resolvedCount,
1779
+ missing: [...allMissing]
1780
+ };
1781
+ }
1782
+
1783
+ // src/commands/template/init.ts
1784
+ var PAGE_SIZE = 15;
1785
+ var LOAD_MORE = "__load_more__";
1786
+ var SEARCH = "__search__";
1787
+ var CLEAR_SEARCH = "__clear_search__";
1788
+ function copyDir2(src, dest) {
1789
+ fs10.mkdirSync(dest, { recursive: true });
1790
+ for (const entry of fs10.readdirSync(src, { withFileTypes: true })) {
1791
+ if (entry.name === "." || entry.name === "..") continue;
1792
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".env" || entry.name.startsWith(".env.")) {
1793
+ continue;
1794
+ }
1795
+ const from = path12.join(src, entry.name);
1796
+ const to = path12.join(dest, entry.name);
1797
+ if (entry.isDirectory()) copyDir2(from, to);
1798
+ else fs10.copyFileSync(from, to);
1799
+ }
1800
+ }
1801
+ function validateFolderName(v) {
1802
+ if (!v || !/^[a-z0-9-_]+$/i.test(v)) {
1803
+ return "Use letters, numbers, - or _";
1804
+ }
1805
+ return void 0;
1806
+ }
1807
+ function formatTemplateLabel(row) {
1808
+ const title = row.title || row.id;
1809
+ if (row.category) return `${title} \xB7 ${row.category}`;
1810
+ return title;
1811
+ }
1812
+ function writeLocaleFiles(dest, messages) {
1813
+ const localesDir = path12.join(dest, "locales");
1814
+ fs10.mkdirSync(localesDir, { recursive: true });
1815
+ let count = 0;
1816
+ for (const [locale, tree] of Object.entries(messages)) {
1817
+ fs10.writeFileSync(
1818
+ path12.join(localesDir, `${locale}.json`),
1819
+ `${JSON.stringify(tree, null, 2)}
1820
+ `,
1821
+ "utf8"
1822
+ );
1823
+ count += 1;
1824
+ }
1825
+ return count;
1826
+ }
1827
+ function patchMeta(dest, patch) {
1828
+ const metaPath = path12.join(dest, "feeef.template.json");
1829
+ if (!fs10.existsSync(metaPath)) return;
1830
+ const meta = JSON.parse(fs10.readFileSync(metaPath, "utf8"));
1831
+ Object.assign(meta, patch);
1832
+ fs10.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}
1833
+ `, "utf8");
1834
+ }
1835
+ async function pickPublicTemplateInteractive(ff, initialQuery = "") {
1836
+ let page = 1;
1837
+ let query = initialQuery.trim();
1838
+ const accumulated = [];
1839
+ let total;
1840
+ let lastBatchSize = 0;
1841
+ const resetList = () => {
1842
+ page = 1;
1843
+ accumulated.length = 0;
1844
+ total = void 0;
1845
+ lastBatchSize = 0;
1846
+ };
1847
+ const fetchPage = async () => {
1848
+ const s = spinner2();
1849
+ const label = page === 1 ? query ? `Searching public templates for \u201C${query}\u201D\u2026` : "Loading public templates\u2026" : `Loading more (page ${page})\u2026`;
1850
+ s.start(label);
1851
+ const res = await ff.storeTemplates.list({
1852
+ page,
1853
+ limit: PAGE_SIZE,
1854
+ policy: TemplateComponentPolicy.public,
1855
+ ...query ? { q: query } : {}
1856
+ });
1857
+ s.stop(page === 1 ? "Templates loaded" : "More templates loaded");
1858
+ if (typeof res.total === "number") total = res.total;
1859
+ const batch = res.data || [];
1860
+ lastBatchSize = batch.length;
1861
+ accumulated.push(...batch);
1862
+ };
1863
+ for (; ; ) {
1864
+ if (accumulated.length === 0) {
1865
+ await fetchPage();
1866
+ }
1867
+ if (accumulated.length === 0) {
1868
+ const again = await select2({
1869
+ message: query ? `No public templates matched \u201C${query}\u201D` : "No public templates found",
1870
+ options: [
1871
+ { value: SEARCH, label: "Search again\u2026" },
1872
+ ...query ? [{ value: CLEAR_SEARCH, label: "Clear search (show all)" }] : []
1873
+ ]
1874
+ });
1875
+ if (again === CLEAR_SEARCH) {
1876
+ query = "";
1877
+ resetList();
1878
+ continue;
1879
+ }
1880
+ query = (await text2({
1881
+ message: "Search templates",
1882
+ placeholder: "dawn, fashion, \u2026"
1883
+ })).trim();
1884
+ resetList();
1885
+ continue;
1886
+ }
1887
+ const hasMore = lastBatchSize >= PAGE_SIZE && (total == null || accumulated.length < total);
1888
+ const options = [
1889
+ ...accumulated.map((row) => ({
1890
+ value: row.id,
1891
+ label: formatTemplateLabel(row),
1892
+ hint: row.id
1893
+ })),
1894
+ {
1895
+ value: SEARCH,
1896
+ label: `${pc.dim("\u2315")} Search\u2026`,
1897
+ hint: query ? `current: ${query}` : "filter by title"
1898
+ }
1899
+ ];
1900
+ if (query) {
1901
+ options.push({
1902
+ value: CLEAR_SEARCH,
1903
+ label: `${pc.dim("\xD7")} Clear search`,
1904
+ hint: query
1905
+ });
1906
+ }
1907
+ if (hasMore) {
1908
+ const hint = total != null ? `${accumulated.length} of ${total}` : `loaded ${accumulated.length}`;
1909
+ options.push({
1910
+ value: LOAD_MORE,
1911
+ label: `${pc.dim("\u2192")} Load more\u2026`,
1912
+ hint
1913
+ });
1914
+ }
1915
+ const chosen = await select2({
1916
+ message: query ? `Pick a public template (search: ${query})` : "Pick a public template",
1917
+ options
1918
+ });
1919
+ if (chosen === SEARCH) {
1920
+ query = (await text2({
1921
+ message: "Search templates",
1922
+ placeholder: "dawn, fashion, \u2026",
1923
+ initialValue: query
1924
+ })).trim();
1925
+ resetList();
1926
+ continue;
1927
+ }
1928
+ if (chosen === CLEAR_SEARCH) {
1929
+ query = "";
1930
+ resetList();
1931
+ continue;
1932
+ }
1933
+ if (chosen === LOAD_MORE) {
1934
+ page += 1;
1935
+ await fetchPage();
1936
+ if (lastBatchSize === 0) {
1937
+ info("No more templates");
1938
+ total = accumulated.length;
1939
+ }
1940
+ continue;
1941
+ }
1942
+ const picked = accumulated.find((row) => row.id === chosen);
1943
+ if (!picked) fail("Selected template not found");
1944
+ return picked;
1945
+ }
1946
+ }
1947
+ async function initFromBlank(dest, name, opts = {}) {
1948
+ const blank = path12.join(scaffoldsRoot(), "blank");
1949
+ const gitUrl = opts.gitUrl || resolveBlankKitGitUrl();
1950
+ let result;
1951
+ if (gitUrl) {
1952
+ const s = spinner2();
1953
+ s.start(`Cloning kit from ${gitUrl}\u2026`);
1954
+ try {
1955
+ result = cloneKitFromGit({ url: gitUrl, dest });
1956
+ s.stop("Kit cloned");
1957
+ } catch (e) {
1958
+ s.stop("Git clone failed \u2014 falling back to local blank scaffold");
1959
+ warn(
1960
+ e instanceof Error ? e.message : String(e)
1961
+ );
1962
+ if (fs10.existsSync(dest)) {
1963
+ fs10.rmSync(dest, { recursive: true, force: true });
1964
+ }
1965
+ if (!fs10.existsSync(blank)) {
1966
+ fail(`Scaffold missing at ${blank}`);
1967
+ }
1968
+ result = copyKitFromLocal({ src: blank, dest, copyDir: copyDir2 });
1969
+ }
1970
+ } else {
1971
+ if (!fs10.existsSync(blank)) {
1972
+ fail(`Scaffold missing at ${blank}`);
1973
+ }
1974
+ result = copyKitFromLocal({ src: blank, dest, copyDir: copyDir2 });
1975
+ }
1976
+ patchMeta(dest, { name });
1977
+ ensureNodePackage(dest, name);
1978
+ ensureAgentPack(dest);
1979
+ ensureEditorSupport(dest);
1980
+ return result;
1981
+ }
1982
+ async function initFromPublicTemplate(dest, name, templateId, ff) {
1983
+ const download = spinner2();
1984
+ download.start("Downloading template\u2026");
1985
+ const full = await ff.storeTemplates.find({ id: templateId });
1986
+ download.stop("Template downloaded");
1987
+ const data = full.data;
1988
+ if (!data || typeof data !== "object" || !("pages" in data)) {
1989
+ fail("Template has no usable data.pages \u2014 cannot unpack");
1990
+ }
1991
+ const dataForUnpack = structuredClone(data);
1992
+ if (full.storeId) {
1993
+ const matSpin = spinner2();
1994
+ matSpin.start("Inlining library references\u2026");
1995
+ try {
1996
+ const mat = await materializeTemplateReferences({
1997
+ data: dataForUnpack,
1998
+ sourceStoreId: full.storeId,
1999
+ resolve: (args) => ff.actions.resolveComponents(args)
2000
+ });
2001
+ if (mat.missing.length > 0) {
2002
+ matSpin.stop(
2003
+ `Inlined ${mat.resolved} library component(s); ${mat.missing.length} missing`
2004
+ );
2005
+ warn(
2006
+ `Unresolved refId(s): ${mat.missing.slice(0, 8).join(", ")}${mat.missing.length > 8 ? "\u2026" : ""}`
2007
+ );
2008
+ } else {
2009
+ matSpin.stop(
2010
+ mat.resolved > 0 ? `Inlined ${mat.resolved} library component(s)` : "No library references to inline"
2011
+ );
2012
+ }
2013
+ } catch (e) {
2014
+ matSpin.stop("Could not inline library references");
2015
+ warn(e instanceof Error ? e.message : String(e));
2016
+ }
2017
+ } else {
2018
+ warn("Template has no storeId \u2014 skipping library reference inline");
2019
+ }
2020
+ const tmpDir = fs10.mkdtempSync(path12.join(os2.tmpdir(), "feeef-init-"));
2021
+ const dataPath = path12.join(tmpDir, "data.json");
2022
+ try {
2023
+ fs10.writeFileSync(
2024
+ dataPath,
2025
+ `${JSON.stringify(dataForUnpack, null, 2)}
2026
+ `,
2027
+ "utf8"
2028
+ );
2029
+ const { unpackTemplate } = await loadUnpack();
2030
+ const unpackSpin = spinner2();
2031
+ unpackSpin.start("Unpacking theme files\u2026");
2032
+ const stats = unpackTemplate({
2033
+ inputPath: dataPath,
2034
+ outDir: dest,
2035
+ force: true
2036
+ });
2037
+ unpackSpin.stop(
2038
+ `Unpacked ${stats.pages} pages / ${stats.components} components (${stats.files} files)`
2039
+ );
2040
+ } finally {
2041
+ fs10.rmSync(tmpDir, { recursive: true, force: true });
2042
+ }
2043
+ let localeCount = 0;
2044
+ let defaultLocale;
2045
+ try {
2046
+ const localeSpin = spinner2();
2047
+ localeSpin.start("Fetching locales\u2026");
2048
+ const locales = await ff.storeTemplates.listLocales(templateId);
2049
+ localeSpin.stop("Locales loaded");
2050
+ defaultLocale = locales.defaultLocale;
2051
+ if (locales.messages && Object.keys(locales.messages).length > 0) {
2052
+ localeCount = writeLocaleFiles(dest, locales.messages);
2053
+ }
2054
+ } catch {
2055
+ const embedded = data.i18n;
2056
+ if (embedded?.messages) {
2057
+ localeCount = writeLocaleFiles(dest, embedded.messages);
2058
+ defaultLocale = embedded.defaultLocale;
2059
+ }
2060
+ }
2061
+ patchMeta(dest, {
2062
+ name,
2063
+ ...defaultLocale ? { defaultLocale } : {},
2064
+ sourceTemplateId: templateId,
2065
+ sourceTitle: full.title
2066
+ });
2067
+ ensureNodePackage(dest, name);
2068
+ ensureAgentPack(dest);
2069
+ ensureEditorSupport(dest);
2070
+ saveProjectRc(dest, { templateId });
2071
+ const readmePath = path12.join(dest, "README.md");
2072
+ if (!fs10.existsSync(readmePath)) {
2073
+ fs10.writeFileSync(
2074
+ readmePath,
2075
+ [
2076
+ `# ${name}`,
2077
+ "",
2078
+ `Cloned from public template **${full.title}** (\`${templateId}\`).`,
2079
+ "",
2080
+ "This folder is a Node.js package \u2014 scripts wrap the global `feeef` CLI.",
2081
+ "",
2082
+ "```bash",
2083
+ "feeef signin",
2084
+ "feeef use",
2085
+ "npm run build",
2086
+ "npm run dev",
2087
+ "npm run publish -- --apply",
2088
+ "```",
2089
+ ""
2090
+ ].join("\n"),
2091
+ "utf8"
2092
+ );
2093
+ }
2094
+ return { title: full.title, localeCount };
2095
+ }
2096
+ async function runTemplateInit(nameArg, opts = {}) {
2097
+ intro2("feeef template init");
2098
+ try {
2099
+ let source = opts.blank ? "blank" : opts.from ? "public" : opts.git ? "git" : await select2({
2100
+ message: "Start from",
2101
+ options: [
2102
+ {
2103
+ value: "blank",
2104
+ label: "Blank theme",
2105
+ hint: resolveBlankKitGitUrl() ? "git clone official / monorepo kit" : "local scaffold"
2106
+ },
2107
+ {
2108
+ value: "git",
2109
+ label: "Git kit URL",
2110
+ hint: "clone --depth 1 (Adonis/Laravel style)"
2111
+ },
2112
+ {
2113
+ value: "public",
2114
+ label: "Public marketplace template",
2115
+ hint: "search + unpack"
2116
+ }
2117
+ ]
2118
+ });
2119
+ const name = nameArg || await text2({
2120
+ message: "Theme folder name",
2121
+ placeholder: "my-theme",
2122
+ validate: validateFolderName
2123
+ });
2124
+ const dest = path12.resolve(process.cwd(), name);
2125
+ if (fs10.existsSync(dest) && fs10.readdirSync(dest).length > 0) {
2126
+ fail(`Directory not empty: ${dest}`);
2127
+ }
2128
+ if (source === "blank" || source === "git") {
2129
+ let gitUrl = opts.git;
2130
+ if (source === "git" && !gitUrl) {
2131
+ gitUrl = (await text2({
2132
+ message: "Git kit URL",
2133
+ placeholder: "https://github.com/feeef/theme-blank.git",
2134
+ validate: (v) => !v?.trim() ? "URL required" : void 0
2135
+ })).trim();
2136
+ }
2137
+ const kit = await initFromBlank(dest, name, {
2138
+ ...gitUrl ? { gitUrl } : {}
2139
+ });
2140
+ ok(`Created ${dest} (${kit.method}: ${kit.source})`);
2141
+ info(
2142
+ "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build"
2143
+ );
2144
+ outro2("Theme scaffold ready");
2145
+ return;
2146
+ }
2147
+ const { ff } = createClient({ requireAuth: false });
2148
+ let templateId = opts.from;
2149
+ if (!templateId) {
2150
+ const picked = await pickPublicTemplateInteractive(ff, opts.search || "");
2151
+ templateId = picked.id;
2152
+ info(`Selected ${formatTemplateLabel(picked)}`);
2153
+ }
2154
+ const result = await initFromPublicTemplate(dest, name, templateId, ff);
2155
+ ok(`Created ${dest} from \u201C${result.title}\u201D`);
2156
+ if (result.localeCount > 0) {
2157
+ info(`Wrote ${result.localeCount} locale file(s)`);
2158
+ }
2159
+ info(
2160
+ "Next:\n cd " + name + "\n feeef signin\n feeef use\n npm run build\n npm run dev"
2161
+ );
2162
+ outro2("Theme cloned and ready");
2163
+ } catch (e) {
2164
+ fail(e);
2165
+ }
2166
+ }
2167
+
2168
+ // src/commands/template/publish.ts
2169
+ import fs11 from "fs";
2170
+ import path13 from "path";
2171
+ import { pathToFileURL as pathToFileURL2 } from "url";
2172
+
2173
+ // src/commands/use.ts
2174
+ var PAGE_SIZE2 = 20;
2175
+ var LOAD_MORE2 = "__load_more__";
2176
+ function formatStoreLabel(store) {
2177
+ const title = store.name || store.slug || store.id;
2178
+ if (store.slug && store.name && store.slug !== store.name) {
2179
+ return `${store.name} (${store.slug})`;
2180
+ }
2181
+ return title;
2182
+ }
2183
+ async function pickStoreInteractive(ff) {
2184
+ const me = await ff.users.me();
2185
+ const userId = me.user?.id;
2186
+ if (!userId) {
2187
+ fail("Could not resolve the signed-in user. Run: feeef signin");
2188
+ }
2189
+ let page = 1;
2190
+ const accumulated = [];
2191
+ let total;
2192
+ for (; ; ) {
2193
+ const s = spinner2();
2194
+ s.start(page === 1 ? "Loading your stores\u2026" : `Loading more (page ${page})\u2026`);
2195
+ const res = await ff.stores.list({
2196
+ page,
2197
+ limit: PAGE_SIZE2,
2198
+ userId
2199
+ });
2200
+ s.stop(page === 1 ? "Stores loaded" : "More stores loaded");
2201
+ const batch = res.data || [];
2202
+ if (typeof res.total === "number") total = res.total;
2203
+ accumulated.push(...batch);
2204
+ if (accumulated.length === 0) {
2205
+ fail("No stores on this account");
2206
+ }
2207
+ const hasMore = batch.length >= PAGE_SIZE2 && (total == null || accumulated.length < total);
2208
+ if (accumulated.length === 1 && !hasMore) {
2209
+ return accumulated[0];
2210
+ }
2211
+ const options = [
2212
+ ...accumulated.map((st) => ({
2213
+ value: st.id,
2214
+ label: formatStoreLabel(st),
2215
+ hint: st.id
2216
+ }))
2217
+ ];
2218
+ if (hasMore) {
2219
+ const hint = total != null ? `${accumulated.length} of ${total}` : `loaded ${accumulated.length}`;
2220
+ options.push({
2221
+ value: LOAD_MORE2,
2222
+ label: `${pc.dim("\u2192")} Load more\u2026`,
2223
+ hint
2224
+ });
2225
+ }
2226
+ const chosen = await select2({
2227
+ message: "Pick a store",
2228
+ options
2229
+ });
2230
+ if (chosen === LOAD_MORE2) {
2231
+ page += 1;
2232
+ continue;
2233
+ }
2234
+ const picked = accumulated.find((st) => st.id === chosen);
2235
+ if (!picked) fail("Selected store not found");
2236
+ return picked;
2237
+ }
2238
+ }
2239
+ async function runUse(storeArg, opts) {
2240
+ intro2("feeef use");
2241
+ try {
2242
+ const themeRoot = findThemeRoot(process.cwd());
2243
+ if (!themeRoot) {
2244
+ fail(
2245
+ "No feeef.template.json nearby. Run from a theme folder or feeef template init first."
2246
+ );
2247
+ }
2248
+ const { ff } = createClient();
2249
+ let storeId;
2250
+ let slug;
2251
+ if (!storeArg) {
2252
+ const picked = await pickStoreInteractive(ff);
2253
+ storeId = picked.id;
2254
+ slug = picked.slug;
2255
+ if (slug || picked.name) {
2256
+ info(`Selected ${formatStoreLabel(picked)}`);
2257
+ }
2258
+ } else {
2259
+ try {
2260
+ const bySlug = await ff.stores.find({ id: storeArg, by: "slug" });
2261
+ storeId = bySlug.id;
2262
+ slug = bySlug.slug;
2263
+ } catch {
2264
+ const byId = await ff.stores.find({ id: storeArg });
2265
+ storeId = byId.id;
2266
+ slug = byId.slug;
2267
+ }
2268
+ }
2269
+ const patch = { store: storeId };
2270
+ if (slug) patch.slug = slug;
2271
+ if (opts.apiUrl) patch.apiUrl = opts.apiUrl;
2272
+ const { rcPath } = saveProjectRc(themeRoot, patch);
2273
+ saveConfig({ defaultStoreId: storeId });
2274
+ ok(`Linked theme \u2192 store ${slug || storeId}`);
2275
+ info(rcPath);
2276
+ outro2("Next: feeef template publish --apply");
2277
+ } catch (e) {
2278
+ fail(e);
2279
+ }
2280
+ }
2281
+ async function askApply(slug, force) {
2282
+ if (force === true) return true;
2283
+ if (force === false) return false;
2284
+ return confirm2({
2285
+ message: `Apply template to store "${slug}"?`,
2286
+ initialValue: true
2287
+ });
2288
+ }
2289
+
2290
+ // src/commands/template/publish.ts
2291
+ var SOURCE_TAG_PREFIX = "feeef-source:";
2292
+ async function runTemplatePublish(dirArg, opts) {
2293
+ intro2("feeef template publish");
2294
+ try {
2295
+ const { ff, http: http2, cfg } = createClient();
2296
+ const srcDir = resolveThemeDir(dirArg);
2297
+ const meta = loadTemplateMeta(srcDir);
2298
+ const rc = loadProjectRc(srcDir);
2299
+ const policy = opts.public ? "public" : opts.unlisted ? "unlisted" : "private";
2300
+ let storeId = resolveStoreIdFromProject({
2301
+ themeRoot: srcDir,
2302
+ globalCfg: cfg,
2303
+ ...opts.store ? { storeFlag: opts.store } : {}
2304
+ });
2305
+ if (!storeId) {
2306
+ const listed = await ff.stores.list({ limit: 50 });
2307
+ const stores = listed.data || [];
2308
+ if (stores.length === 0) fail("No stores on this account");
2309
+ if (stores.length === 1 || opts.yes) {
2310
+ storeId = stores[0].id;
2311
+ } else {
2312
+ fail("No store linked. Run: feeef use <storeId|slug>");
2313
+ }
2314
+ }
2315
+ const store = await ff.stores.find({ id: storeId });
2316
+ const title = opts.title || rc.title || meta.name || path13.basename(srcDir);
2317
+ info(`Theme ${srcDir}`);
2318
+ info(`Store ${store.slug || storeId} (${policy})`);
2319
+ const outPath = path13.join(srcDir, "dist", "data.json");
2320
+ const s = spinner2();
2321
+ s.start("Building\u2026");
2322
+ const { buildTemplate } = await loadBuild();
2323
+ const built = buildTemplate({ srcDir, outPath, write: true });
2324
+ for (const w of built.warnings || []) {
2325
+ warn(`[${w.path}] ${w.message}`);
2326
+ }
2327
+ s.stop(`Built (${built.localeCount ?? 0} locales)`);
2328
+ const dataRaw = JSON.parse(fs11.readFileSync(outPath, "utf8"));
2329
+ const i18n = dataRaw["i18n"];
2330
+ const defaultLocale = i18n?.defaultLocale || (typeof meta.defaultLocale === "string" ? meta.defaultLocale : "en");
2331
+ const distSchemaPath = path13.join(srcDir, "dist", "schema.json");
2332
+ let schema;
2333
+ if (fs11.existsSync(distSchemaPath)) {
2334
+ schema = JSON.parse(fs11.readFileSync(distSchemaPath, "utf8"));
2335
+ } else {
2336
+ const { buildPackagedSchema } = await import(pathToFileURL2(
2337
+ path13.join(cliRoot(), "vendor/template-kit/lib/theme-schema.mjs")
2338
+ ).href);
2339
+ const { schema: packed } = buildPackagedSchema({
2340
+ srcDir,
2341
+ libraryDoc: fs11.existsSync(path13.join(srcDir, "dist", "schema.library.json")) ? JSON.parse(
2342
+ fs11.readFileSync(
2343
+ path13.join(srcDir, "dist", "schema.library.json"),
2344
+ "utf8"
2345
+ )
2346
+ ) : { components: [] }
2347
+ });
2348
+ schema = packed;
2349
+ }
2350
+ const libraryPath = path13.join(srcDir, "dist", "schema.library.json");
2351
+ if (fs11.existsSync(libraryPath) && schema["library"] == null) {
2352
+ const libDoc = JSON.parse(fs11.readFileSync(libraryPath, "utf8"));
2353
+ schema = {
2354
+ ...schema,
2355
+ library: libDoc.library ?? libDoc.components ?? libDoc
2356
+ };
2357
+ }
2358
+ if (typeof schema["name"] !== "string" || !schema["name"]) {
2359
+ schema = { ...schema, name: title };
2360
+ }
2361
+ s.start("Publishing template_components\u2026");
2362
+ const publishables = collectPublishableComponents(srcDir);
2363
+ const codeToRefId = /* @__PURE__ */ new Map();
2364
+ const sourceKeyToRefId = /* @__PURE__ */ new Map();
2365
+ const existingList = await ff.templateComponents.list({
2366
+ storeId,
2367
+ limit: 200
2368
+ });
2369
+ const existingBySource = /* @__PURE__ */ new Map();
2370
+ for (const row of existingList.data || []) {
2371
+ const tag = (row.tags || []).find(
2372
+ (t) => String(t).startsWith(SOURCE_TAG_PREFIX)
2373
+ );
2374
+ if (tag) {
2375
+ existingBySource.set(
2376
+ String(tag).slice(SOURCE_TAG_PREFIX.length),
2377
+ row
2378
+ );
2379
+ }
2380
+ }
2381
+ for (const comp of publishables) {
2382
+ const tags = [
2383
+ ...Array.isArray(comp.meta["tags"]) ? comp.meta["tags"] : [],
2384
+ `${SOURCE_TAG_PREFIX}${comp.sourceKey}`,
2385
+ `feeef-kind:${comp.kind}`
2386
+ ];
2387
+ const payload = {
2388
+ storeId,
2389
+ title: comp.meta["title"] || comp.sourceKey,
2390
+ subtitle: comp.meta["subtitle"] ?? null,
2391
+ category: comp.meta["category"] ?? comp.kind,
2392
+ tags,
2393
+ imageUrl: comp.meta["imageUrl"] ?? null,
2394
+ code: comp.code,
2395
+ propsSchema: comp.meta["propsSchema"] || {},
2396
+ slotsSchema: comp.meta["slotsSchema"] ?? null,
2397
+ propsDefault: comp.meta["props"] || comp.meta["propsDefault"] || {},
2398
+ slotsDefault: comp.meta["slotsDefault"] ?? null,
2399
+ slotsLayout: comp.meta["slotsLayout"] ?? null,
2400
+ policy,
2401
+ price: 0
2402
+ };
2403
+ const existing = existingBySource.get(comp.sourceKey);
2404
+ let row;
2405
+ if (existing) {
2406
+ row = await ff.templateComponents.update({
2407
+ id: existing.id,
2408
+ data: {
2409
+ title: payload.title,
2410
+ subtitle: payload.subtitle,
2411
+ category: payload.category,
2412
+ tags: payload.tags,
2413
+ imageUrl: payload.imageUrl,
2414
+ code: payload.code,
2415
+ propsSchema: payload.propsSchema,
2416
+ slotsSchema: payload.slotsSchema,
2417
+ propsDefault: payload.propsDefault,
2418
+ slotsDefault: payload.slotsDefault,
2419
+ slotsLayout: payload.slotsLayout,
2420
+ policy: payload.policy
2421
+ }
2422
+ });
2423
+ } else {
2424
+ row = await ff.templateComponents.create(payload);
2425
+ }
2426
+ codeToRefId.set(comp.code.trim(), row.id);
2427
+ sourceKeyToRefId.set(comp.sourceKey, row.id);
2428
+ }
2429
+ s.stop(`${publishables.length} components`);
2430
+ let data = rewriteDataToReferences(dataRaw, codeToRefId);
2431
+ data = stripDataI18n(data);
2432
+ if (Array.isArray(schema["library"])) {
2433
+ schema["library"] = schema["library"].map(
2434
+ (entry) => {
2435
+ const key = entry["libraryId"] || entry["id"];
2436
+ if (key && sourceKeyToRefId.has(key)) {
2437
+ return { ...entry, refId: sourceKeyToRefId.get(key) };
2438
+ }
2439
+ return entry;
2440
+ }
2441
+ );
2442
+ }
2443
+ s.start("Publishing store_templates\u2026");
2444
+ let templateId = rc.templateId || cfg.lastPublishedTemplateId || void 0;
2445
+ let templateRow = null;
2446
+ if (templateId) {
2447
+ try {
2448
+ templateRow = await ff.storeTemplates.find({ id: templateId });
2449
+ if (templateRow && templateRow.storeId !== storeId) {
2450
+ templateRow = null;
2451
+ templateId = void 0;
2452
+ }
2453
+ } catch {
2454
+ templateRow = null;
2455
+ templateId = void 0;
2456
+ }
2457
+ }
2458
+ if (!templateRow) {
2459
+ const listed = await ff.storeTemplates.list({
2460
+ storeId,
2461
+ limit: 50,
2462
+ q: title
2463
+ });
2464
+ templateRow = (listed.data || []).find(
2465
+ (t) => t.storeId === storeId && t.title === title
2466
+ ) || null;
2467
+ templateId = templateRow?.id;
2468
+ }
2469
+ if (templateRow) {
2470
+ templateRow = await ff.storeTemplates.update({
2471
+ id: templateRow.id,
2472
+ data: { title, schema, data, policy }
2473
+ });
2474
+ s.stop(`Updated ${templateRow.id}`);
2475
+ } else {
2476
+ templateRow = await ff.storeTemplates.create({
2477
+ storeId,
2478
+ title,
2479
+ schema,
2480
+ data,
2481
+ policy,
2482
+ price: 0
2483
+ });
2484
+ s.stop(`Created ${templateRow.id}`);
2485
+ }
2486
+ templateId = templateRow.id;
2487
+ saveConfig({
2488
+ defaultStoreId: storeId,
2489
+ lastPublishedTemplateId: templateId
2490
+ });
2491
+ saveProjectRc(srcDir, {
2492
+ store: storeId,
2493
+ slug: store.slug,
2494
+ templateId,
2495
+ title
2496
+ });
2497
+ const locales = loadDistLocales(path13.join(srcDir, "dist"), defaultLocale);
2498
+ if (locales.length) {
2499
+ s.start(`Uploading ${locales.length} locales\u2026`);
2500
+ if (typeof ff.storeTemplates.replaceLocales === "function") {
2501
+ await ff.storeTemplates.replaceLocales(templateId, locales);
2502
+ } else {
2503
+ await http2.put(`/store_templates/${templateId}/locales`, { locales });
2504
+ }
2505
+ s.stop(`Locales: ${locales.map((l) => l.locale).join(", ")}`);
2506
+ }
2507
+ const shouldApply = opts.yes && opts.apply !== false ? true : await askApply(store.slug || storeId, opts.apply);
2508
+ if (shouldApply) {
2509
+ s.start("Installing on store\u2026");
2510
+ await ff.storeTemplates.install({
2511
+ fromId: templateId,
2512
+ storeId
2513
+ });
2514
+ s.stop("Installed");
2515
+ const preview = `https://${store.slug}.feeef.store`;
2516
+ ok(`Preview ${pc.cyan(preview)}`);
2517
+ outro2(preview);
2518
+ } else {
2519
+ ok(`Template id ${templateId}`);
2520
+ outro2("Skipped install (use --apply next time)");
2521
+ }
2522
+ } catch (e) {
2523
+ fail(e);
2524
+ }
2525
+ }
2526
+
2527
+ // src/commands/template/remove.ts
2528
+ import path14 from "path";
2529
+ async function runTemplateRemovePage(pageId, opts = {}) {
2530
+ intro2("feeef template remove page");
2531
+ try {
2532
+ if (!pageId?.trim()) {
2533
+ fail("Usage: feeef template remove page <pageId>");
2534
+ }
2535
+ const themeRoot = resolveThemeDir(opts.dir);
2536
+ if (!opts.yes) {
2537
+ const go = await confirm2({
2538
+ message: `Delete pages/${pageId}/ and remove it from feeef.template.json?`,
2539
+ initialValue: false
2540
+ });
2541
+ if (!go) {
2542
+ outro2("Cancelled");
2543
+ return;
2544
+ }
2545
+ }
2546
+ const { pageId: id, removedPath } = removePage(themeRoot, pageId);
2547
+ ok(`Removed ${path14.relative(themeRoot, removedPath)}`);
2548
+ outro2(`Page "${id}" deleted`);
2549
+ } catch (err) {
2550
+ fail(err instanceof Error ? err.message : String(err));
2551
+ }
2552
+ }
2553
+ async function runTemplateRemoveComponent(pageId, componentId, opts = {}) {
2554
+ intro2("feeef template remove component");
2555
+ try {
2556
+ if (!pageId?.trim() || !componentId?.trim()) {
2557
+ fail("Usage: feeef template remove component <pageId> <componentId>");
2558
+ }
2559
+ const themeRoot = resolveThemeDir(opts.dir);
2560
+ if (!opts.yes) {
2561
+ const go = await confirm2({
2562
+ message: `Delete component ${pageId}/${componentId}?`,
2563
+ initialValue: false
2564
+ });
2565
+ if (!go) {
2566
+ outro2("Cancelled");
2567
+ return;
2568
+ }
2569
+ }
2570
+ const { removedPath } = removeComponent(themeRoot, pageId, componentId, {
2571
+ ...opts.section ? { section: opts.section } : {}
2572
+ });
2573
+ ok(`Removed ${path14.relative(themeRoot, removedPath)}`);
2574
+ outro2("Done");
2575
+ } catch (err) {
2576
+ fail(err instanceof Error ? err.message : String(err));
2577
+ }
2578
+ }
2579
+
2580
+ // src/commands/template/watch.ts
2581
+ import { spawn as spawn2 } from "child_process";
2582
+ import path15 from "path";
2583
+ async function runTemplateWatch(dirArg) {
2584
+ intro2("feeef template watch");
2585
+ try {
2586
+ const srcDir = resolveThemeDir(dirArg);
2587
+ const outPath = path15.join(srcDir, "dist", "data.json");
2588
+ const s = spinner2();
2589
+ s.start("Initial build\u2026");
2590
+ const { buildTemplate } = await loadBuild();
2591
+ buildTemplate({ srcDir, outPath, write: true });
2592
+ s.stop(`Watching ${srcDir}`);
2593
+ info(`Output \u2192 ${outPath}`);
2594
+ info("Ctrl+C to stop");
2595
+ const child = spawn2(
2596
+ process.execPath,
2597
+ [kitBin(), "watch", srcDir, "-o", outPath],
2598
+ { stdio: "inherit" }
2599
+ );
2600
+ await new Promise((resolve, reject) => {
2601
+ child.on("exit", (code) => {
2602
+ if (code && code !== 0) reject(new Error(`watch exited ${code}`));
2603
+ else resolve();
2604
+ });
2605
+ });
2606
+ outro2("Stopped");
2607
+ } catch (e) {
2608
+ fail(e);
2609
+ }
2610
+ }
2611
+
2612
+ // src/index.ts
2613
+ function createProgram() {
2614
+ const program = new Command();
2615
+ program.name("feeef").description("Feeef developer CLI \u2014 themes today, cloud tools tomorrow").version("0.2.0").hook("preAction", (thisCommand) => {
2616
+ if (thisCommand.args.length === 0) return;
2617
+ banner();
2618
+ });
2619
+ program.command("signin").description("Sign in via browser OAuth (or --password-auth)").option("-e, --email <email>", "Account email (password auth)").option(
2620
+ "--password-auth",
2621
+ "Use email/password instead of browser OAuth (prompts for password)"
2622
+ ).option("--api-url <url>", "API base URL").action(async (opts) => {
2623
+ await runSignin({
2624
+ email: opts.email,
2625
+ apiUrl: opts.apiUrl,
2626
+ passwordAuth: opts.passwordAuth
2627
+ });
2628
+ });
2629
+ program.command("whoami").description("Show the signed-in user").action(async () => {
2630
+ await runWhoami();
2631
+ });
2632
+ program.command("signout").description("Clear saved token").action(() => {
2633
+ runSignout();
2634
+ });
2635
+ const config = program.command("config").description("Show or set CLI config (~/.config/feeef)");
2636
+ config.command("show", { isDefault: true }).description("Print non-secret config").action(() => {
2637
+ runConfigShow();
2638
+ });
2639
+ config.command("set").description("Set a config key").argument("<key>", "apiUrl | defaultStoreId | storefrontPath | devPort | \u2026").argument("<value>", "Value").action((key, value) => {
2640
+ runConfigSet(key, value);
2641
+ });
2642
+ program.command("use").description(
2643
+ "Link theme to a store (.feeefrc); omit store to pick from your list"
2644
+ ).argument("[store]", "Store id or slug (optional \u2014 interactive picker)").option("--api-url <url>", "API override for this theme").action(async (store, opts) => {
2645
+ await runUse(store, opts);
2646
+ });
2647
+ 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) => {
2648
+ await runTemplatePublish(dir, opts);
2649
+ });
2650
+ const template = program.command("template").description(
2651
+ "Theme kit: init, add/remove page|component, build, check, watch, dev, publish"
2652
+ );
2653
+ template.command("init").description(
2654
+ "Scaffold a theme (blank, git kit, or public marketplace)"
2655
+ ).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
+ await runTemplateInit(name, {
2657
+ blank: opts.blank,
2658
+ from: opts.from,
2659
+ search: opts.search,
2660
+ git: opts.git
2661
+ });
2662
+ });
2663
+ const templateAdd = template.command("add").description("Add a page or component to the current theme");
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>", "Theme directory").action(async (pageId, opts) => {
2665
+ await runTemplateAddPage(pageId, { dir: opts.dir });
2666
+ });
2667
+ templateAdd.command("component").description(
2668
+ "Create a starter <name>.tsx under pages/<page>/components/ (or sections/main)"
2669
+ ).argument("<pageId>", "Page id").argument("<componentId>", "Component file id (e.g. hero)").option("--title <title>", "meta.title (default: Title Case of id)").option(
2670
+ "--section <id>",
2671
+ "Legacy: write under sections/<id>/components/ instead of flat"
2672
+ ).option("--dir <path>", "Theme directory").action(async (pageId, componentId, opts) => {
2673
+ await runTemplateAddComponent(pageId, componentId, {
2674
+ dir: opts.dir,
2675
+ title: opts.title,
2676
+ section: opts.section
2677
+ });
2678
+ });
2679
+ const templateRemove = template.command("remove").description("Remove a page or component from the current theme");
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>", "Theme directory").action(async (pageId, opts) => {
2681
+ await runTemplateRemovePage(pageId, { dir: opts.dir, yes: opts.yes });
2682
+ });
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>", "Theme directory").action(async (pageId, componentId, opts) => {
2684
+ await runTemplateRemoveComponent(pageId, componentId, {
2685
+ dir: opts.dir,
2686
+ section: opts.section,
2687
+ yes: opts.yes
2688
+ });
2689
+ });
2690
+ template.command("build").description("Compile theme \u2192 dist/data.json").argument("[dir]", "Theme directory").action(async (dir) => {
2691
+ await runTemplateBuild(dir);
2692
+ });
2693
+ template.command("check").description("Validate theme without writing dist").argument("[dir]", "Theme directory").action(async (dir) => {
2694
+ await runTemplateBuild(dir, { checkOnly: true });
2695
+ });
2696
+ template.command("watch").description("Rebuild on file changes").argument("[dir]", "Theme directory").action(async (dir) => {
2697
+ await runTemplateWatch(dir);
2698
+ });
2699
+ template.command("dev").description(
2700
+ "Watch + push draft preview to hosted storefront (never overwrites live theme)"
2701
+ ).argument("[dir]", "Theme directory").option("-p, --port <port>", "Local Next port (only with --local)").option("--storefront <path>", "Lithium storefront root (implies --local)").option(
2702
+ "--local",
2703
+ "Employee escape hatch: spawn local Next (requires storefront)"
2704
+ ).action(async (dir, opts) => {
2705
+ await runTemplateDev(dir, opts);
2706
+ });
2707
+ 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) => {
2708
+ await runTemplatePublish(dir, opts);
2709
+ });
2710
+ const cloud = program.command("cloud").description("Cloud providers (Azure / GCP / AWS) \u2014 coming soon");
2711
+ cloud.command("azure").description("Azure helpers (coming soon)").action(() => {
2712
+ console.log(pc.dim("Not implemented yet. Tracked for a future release."));
2713
+ });
2714
+ cloud.command("gcp").description("Google Cloud helpers (coming soon)").action(() => {
2715
+ console.log(pc.dim("Not implemented yet."));
2716
+ });
2717
+ cloud.command("aws").description("AWS helpers (coming soon)").action(() => {
2718
+ console.log(pc.dim("Not implemented yet."));
2719
+ });
2720
+ return program;
2721
+ }
2722
+ async function runCli(argv = process.argv) {
2723
+ const program = createProgram();
2724
+ await program.parseAsync(argv);
2725
+ }
2726
+ export {
2727
+ createProgram,
2728
+ runCli
2729
+ };
2730
+ //# sourceMappingURL=index.js.map