appfunnel 0.6.0 → 0.7.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.
package/dist/index.js CHANGED
@@ -175,28 +175,308 @@ var init_projects = __esm({
175
175
  }
176
176
  });
177
177
 
178
+ // src/lib/api.ts
179
+ async function apiFetch(path, options) {
180
+ const { token, apiBaseUrl, ...fetchOpts } = options;
181
+ const base = apiBaseUrl || DEFAULT_API_BASE2;
182
+ const url = `${base}${path}`;
183
+ const isFormData = fetchOpts.body instanceof FormData;
184
+ const headers = {
185
+ Authorization: token,
186
+ ...fetchOpts.headers || {}
187
+ };
188
+ if (!isFormData) {
189
+ headers["Content-Type"] = "application/json";
190
+ }
191
+ const response = await fetch(url, {
192
+ ...fetchOpts,
193
+ headers
194
+ });
195
+ if (!response.ok) {
196
+ const body = await response.text().catch(() => "");
197
+ let message = `API request failed: ${response.status} ${response.statusText}`;
198
+ try {
199
+ const parsed = JSON.parse(body);
200
+ if (parsed.error) message = parsed.error;
201
+ if (parsed.message) message = parsed.message;
202
+ } catch {
203
+ }
204
+ const error2 = new CLIError("API_ERROR", message);
205
+ error2.statusCode = response.status;
206
+ throw error2;
207
+ }
208
+ return response;
209
+ }
210
+ async function fetchPrices(projectId, storePriceIds, options) {
211
+ if (storePriceIds.length === 0) return /* @__PURE__ */ new Map();
212
+ const response = await apiFetch(`/project/${projectId}/headless/prices`, {
213
+ ...options,
214
+ method: "POST",
215
+ body: JSON.stringify({ storePriceIds })
216
+ });
217
+ const data = await response.json();
218
+ return new Map(Object.entries(data.prices || {}));
219
+ }
220
+ async function fetchStores(projectId, options) {
221
+ const response = await apiFetch(`/project/${projectId}/stores`, {
222
+ ...options,
223
+ method: "GET"
224
+ });
225
+ const data = await response.json();
226
+ return data.data || [];
227
+ }
228
+ async function fetchStorePrices(projectId, storeId, options) {
229
+ const response = await apiFetch(
230
+ `/project/${projectId}/stores/${storeId}/prices`,
231
+ { ...options, method: "GET" }
232
+ );
233
+ const data = await response.json();
234
+ return data.data || [];
235
+ }
236
+ async function publishBuild(projectId, funnelId, manifest, assets, options, promote) {
237
+ const formData = new FormData();
238
+ formData.set("manifest", JSON.stringify(manifest));
239
+ if (funnelId) {
240
+ formData.set("funnelId", funnelId);
241
+ }
242
+ if (promote) {
243
+ formData.set("promote", "true");
244
+ }
245
+ for (const asset of assets) {
246
+ formData.append(
247
+ "assets",
248
+ new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
249
+ asset.path
250
+ );
251
+ }
252
+ try {
253
+ const response = await apiFetch(`/project/${projectId}/headless/publish`, {
254
+ ...options,
255
+ method: "POST",
256
+ body: formData
257
+ });
258
+ return await response.json();
259
+ } catch (err) {
260
+ if (err instanceof CLIError && err.code === "API_ERROR") {
261
+ if (err.statusCode === 413) {
262
+ throw new CLIError(
263
+ "BUNDLE_TOO_LARGE",
264
+ err.message,
265
+ "Reduce page bundle sizes. Check for large dependencies."
266
+ );
267
+ }
268
+ if (err.statusCode === 409) {
269
+ throw new CLIError(
270
+ "FUNNEL_NOT_HEADLESS",
271
+ err.message,
272
+ "Remove funnelId from config to create a new headless funnel."
273
+ );
274
+ }
275
+ throw new CLIError("PUBLISH_FAILED", err.message);
276
+ }
277
+ throw err;
278
+ }
279
+ }
280
+ var DEFAULT_API_BASE2;
281
+ var init_api = __esm({
282
+ "src/lib/api.ts"() {
283
+ "use strict";
284
+ init_errors();
285
+ DEFAULT_API_BASE2 = "https://api.appfunnel.net";
286
+ }
287
+ });
288
+
178
289
  // src/commands/init.ts
179
290
  var init_exports = {};
180
291
  __export(init_exports, {
181
292
  initCommand: () => initCommand
182
293
  });
183
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync } from "fs";
184
- import { join as join2 } from "path";
294
+ import { cpSync, existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
295
+ import { join as join2, dirname } from "path";
296
+ import { fileURLToPath } from "url";
185
297
  import pc4 from "picocolors";
186
- async function initCommand(name) {
298
+ import select2 from "@inquirer/select";
299
+ import input from "@inquirer/input";
300
+ function getTemplatesDir() {
301
+ const dir = join2(__dirname, "..", "templates");
302
+ if (!existsSync(dir)) {
303
+ throw new Error(`Templates directory not found at ${dir}`);
304
+ }
305
+ return dir;
306
+ }
307
+ function listTemplates() {
308
+ const root = getTemplatesDir();
309
+ return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
310
+ const configPath = join2(root, d.name, "template.json");
311
+ const config = existsSync(configPath) ? JSON.parse(readFileSync2(configPath, "utf-8")) : { name: d.name, description: "", products: [] };
312
+ return { dir: d.name, config };
313
+ });
314
+ }
315
+ function formatInterval(interval, count) {
316
+ if (!interval) return " one-time";
317
+ const label = INTERVAL_LABELS[interval]?.[count];
318
+ if (label) return `/${label}`;
319
+ return `/${count} ${interval}${count > 1 ? "s" : ""}`;
320
+ }
321
+ function formatPrice(price) {
322
+ const amount = (price.amount / 100).toFixed(2);
323
+ const currency = price.currency.toUpperCase();
324
+ const interval = formatInterval(price.interval, price.intervalCount);
325
+ const productName = price.name || "Unnamed product";
326
+ const priceName = price.priceName ? ` \u2014 ${price.priceName}` : "";
327
+ return `${currency} ${amount}${interval} ${pc4.dim(`${productName}${priceName}`)} ${pc4.dim(`(${price.id})`)}`;
328
+ }
329
+ function formatStoreName(store) {
330
+ const test = store.isTestMode ? pc4.yellow(" (test)") : "";
331
+ return `${store.name || store.type}${test}`;
332
+ }
333
+ async function initCommand() {
187
334
  const creds = requireAuth();
188
- const dir = join2(process.cwd(), name);
189
- if (existsSync(dir)) {
190
- error(`Directory '${name}' already exists.`);
191
- process.exit(1);
192
- }
335
+ const apiOpts = { token: creds.token };
336
+ const name = await input({
337
+ message: "Funnel name",
338
+ validate: (value) => {
339
+ if (!value.trim()) return "Name is required";
340
+ if (!/^[a-z0-9-]+$/.test(value.trim()))
341
+ return "Use lowercase letters, numbers, and hyphens only";
342
+ if (existsSync(join2(process.cwd(), value.trim())))
343
+ return `Directory '${value.trim()}' already exists`;
344
+ return true;
345
+ }
346
+ });
347
+ const dir = join2(process.cwd(), name.trim());
193
348
  const projectId = await promptForProject(creds.token);
194
349
  const projects = await fetchProjects(creds.token);
195
350
  const project = projects.find((p) => p.id === projectId);
351
+ const templates = listTemplates();
352
+ const selectedDir = await select2({
353
+ message: "Choose a template",
354
+ choices: templates.map((t) => ({
355
+ name: t.config.description ? `${t.config.name} \u2014 ${pc4.dim(t.config.description)}` : t.config.name,
356
+ value: t.dir
357
+ }))
358
+ });
359
+ const chosen = templates.find((t) => t.dir === selectedDir);
360
+ const templateConfig = chosen.config;
361
+ const templateDir = join2(getTemplatesDir(), chosen.dir);
362
+ const productBindings = [];
363
+ if (templateConfig.products.length > 0) {
364
+ const storesSpinner = spinner("Fetching stores...");
365
+ const stores = await fetchStores(projectId, apiOpts);
366
+ storesSpinner.stop();
367
+ if (stores.length === 0) {
368
+ warn("No stores found for this project.");
369
+ info("Products will use empty IDs \u2014 create a store in the dashboard and update appfunnel.config.ts.");
370
+ } else {
371
+ let store;
372
+ if (stores.length === 1) {
373
+ store = stores[0];
374
+ info(`Using store: ${formatStoreName(store)}`);
375
+ } else {
376
+ const storeId = await select2({
377
+ message: "Choose a store",
378
+ choices: stores.map((s2) => ({
379
+ name: formatStoreName(s2),
380
+ value: s2.id
381
+ }))
382
+ });
383
+ store = stores.find((s2) => s2.id === storeId);
384
+ }
385
+ const pricesSpinner = spinner("Fetching prices...");
386
+ const prices = await fetchStorePrices(projectId, store.id, apiOpts);
387
+ pricesSpinner.stop();
388
+ if (prices.length === 0) {
389
+ warn("No prices found in this store.");
390
+ info("Import prices in the dashboard, then update appfunnel.config.ts.");
391
+ } else {
392
+ console.log();
393
+ info(`Configuring ${templateConfig.products.length} product(s)...`);
394
+ console.log();
395
+ for (const product of templateConfig.products) {
396
+ if (product.description) {
397
+ console.log(` ${pc4.dim(product.description)}`);
398
+ }
399
+ const storePriceId = await select2({
400
+ message: `${product.label} \u2014 select a price`,
401
+ choices: prices.map((p) => ({
402
+ name: formatPrice(p),
403
+ value: p.id
404
+ }))
405
+ });
406
+ const binding = {
407
+ id: product.id,
408
+ name: product.id,
409
+ storePriceId
410
+ };
411
+ if (product.paidTrial) {
412
+ const trialStorePriceId = await select2({
413
+ message: `${product.label} \u2014 select a trial price`,
414
+ choices: [
415
+ ...prices.map((p) => ({
416
+ name: formatPrice(p),
417
+ value: p.id
418
+ })),
419
+ { name: pc4.dim("Skip (no trial)"), value: "" }
420
+ ]
421
+ });
422
+ if (trialStorePriceId) {
423
+ binding.trialStorePriceId = trialStorePriceId;
424
+ const trialDaysStr = await select2({
425
+ message: `${product.label} \u2014 trial duration`,
426
+ choices: [
427
+ { name: "3 days", value: "3" },
428
+ { name: "7 days", value: "7" },
429
+ { name: "14 days", value: "14" },
430
+ { name: "30 days", value: "30" },
431
+ { name: "90 days", value: "90" }
432
+ ]
433
+ });
434
+ binding.trialDays = parseInt(trialDaysStr, 10);
435
+ }
436
+ }
437
+ productBindings.push(binding);
438
+ }
439
+ }
440
+ }
441
+ }
196
442
  const s = spinner(`Creating ${name}...`);
197
- mkdirSync2(join2(dir, "src", "pages"), { recursive: true });
198
- mkdirSync2(join2(dir, "src", "components"), { recursive: true });
199
- mkdirSync2(join2(dir, "locales"), { recursive: true });
443
+ cpSync(templateDir, dir, { recursive: true });
444
+ const templateJsonPath = join2(dir, "template.json");
445
+ if (existsSync(templateJsonPath)) {
446
+ const { unlinkSync } = await import("fs");
447
+ unlinkSync(templateJsonPath);
448
+ }
449
+ const configPath = join2(dir, "appfunnel.config.ts");
450
+ if (existsSync(configPath)) {
451
+ let config = readFileSync2(configPath, "utf-8");
452
+ config = config.replace("__PROJECT_ID__", projectId);
453
+ config = config.replace("__NAME__", name);
454
+ if (productBindings.length > 0) {
455
+ const itemsStr = productBindings.map((b) => {
456
+ const fields = [
457
+ ` { id: '${b.id}'`,
458
+ `name: '${b.name}'`,
459
+ `storePriceId: '${b.storePriceId}'`
460
+ ];
461
+ if (b.trialDays) fields.push(`trialDays: ${b.trialDays}`);
462
+ if (b.trialStorePriceId)
463
+ fields.push(`trialStorePriceId: '${b.trialStorePriceId}'`);
464
+ return fields.join(", ") + " }";
465
+ }).join(",\n");
466
+ const defaultId = productBindings[0].id;
467
+ config = config.replace(
468
+ /products:\s*\{[^}]*items:\s*\[[^\]]*\][^}]*\}/s,
469
+ `products: {
470
+ items: [
471
+ ${itemsStr},
472
+ ],
473
+ defaultId: '${defaultId}',
474
+ }`
475
+ );
476
+ }
477
+ writeFileSync2(configPath, config);
478
+ }
479
+ const sdkVersion = `^${"0.7.0"}`;
200
480
  writeFileSync2(
201
481
  join2(dir, "package.json"),
202
482
  JSON.stringify(
@@ -211,12 +491,12 @@ async function initCommand(name) {
211
491
  publish: "appfunnel publish"
212
492
  },
213
493
  dependencies: {
214
- "@appfunnel-dev/sdk": "^0.6.0",
494
+ "@appfunnel-dev/sdk": sdkVersion,
215
495
  react: "^18.3.0",
216
496
  "react-dom": "^18.3.0"
217
497
  },
218
498
  devDependencies: {
219
- appfunnel: "^0.6.0",
499
+ appfunnel: sdkVersion,
220
500
  typescript: "^5.4.0",
221
501
  "@types/react": "^18.2.0",
222
502
  "@types/react-dom": "^18.2.0",
@@ -230,112 +510,6 @@ async function initCommand(name) {
230
510
  2
231
511
  ) + "\n"
232
512
  );
233
- writeFileSync2(
234
- join2(dir, "tsconfig.json"),
235
- JSON.stringify(
236
- {
237
- compilerOptions: {
238
- target: "ES2020",
239
- module: "ESNext",
240
- moduleResolution: "bundler",
241
- jsx: "react-jsx",
242
- strict: true,
243
- esModuleInterop: true,
244
- skipLibCheck: true,
245
- paths: {
246
- "@/*": ["./src/*"]
247
- },
248
- baseUrl: "."
249
- },
250
- include: ["src"]
251
- },
252
- null,
253
- 2
254
- ) + "\n"
255
- );
256
- writeFileSync2(join2(dir, "src", "app.css"), `@import "tailwindcss";
257
- `);
258
- writeFileSync2(
259
- join2(dir, "appfunnel.config.ts"),
260
- `import { defineConfig } from '@appfunnel-dev/sdk'
261
-
262
- export default defineConfig({
263
- projectId: '${projectId}',
264
- name: '${name}',
265
- defaultLocale: 'en',
266
-
267
- responses: {
268
- goal: { type: 'string' },
269
- },
270
-
271
- products: {
272
- items: [],
273
- },
274
- })
275
- `
276
- );
277
- writeFileSync2(
278
- join2(dir, "src", "funnel.tsx"),
279
- `import './app.css'
280
-
281
- export default function Funnel({ children }: { children: React.ReactNode }) {
282
- return (
283
- <div className="min-h-screen">
284
- {children}
285
- </div>
286
- )
287
- }
288
- `
289
- );
290
- writeFileSync2(
291
- join2(dir, "src", "pages", "index.tsx"),
292
- `import { definePage, useResponse, useNavigation } from '@appfunnel-dev/sdk'
293
-
294
- export const page = definePage({
295
- name: 'Landing',
296
- type: 'default',
297
- routes: [],
298
- })
299
-
300
- export default function Landing() {
301
- const [goal, setGoal] = useResponse<string>('goal')
302
- const { goToNextPage } = useNavigation()
303
-
304
- return (
305
- <div className="flex min-h-screen items-center justify-center p-4">
306
- <div className="w-full max-w-md space-y-6">
307
- <h1 className="text-3xl font-bold text-center">Welcome</h1>
308
- <input
309
- type="text"
310
- value={goal}
311
- onChange={(e) => setGoal(e.target.value)}
312
- placeholder="What's your goal?"
313
- className="w-full rounded-xl border p-4 text-lg"
314
- />
315
- <button
316
- onClick={goToNextPage}
317
- disabled={!goal.trim()}
318
- className="w-full rounded-xl bg-blue-600 py-4 text-lg font-bold text-white disabled:opacity-50"
319
- >
320
- Continue
321
- </button>
322
- </div>
323
- </div>
324
- )
325
- }
326
- `
327
- );
328
- writeFileSync2(
329
- join2(dir, "locales", "en.json"),
330
- JSON.stringify({ welcome: "Welcome" }, null, 2) + "\n"
331
- );
332
- writeFileSync2(
333
- join2(dir, ".gitignore"),
334
- `node_modules
335
- dist
336
- .appfunnel
337
- `
338
- );
339
513
  s.stop();
340
514
  console.log();
341
515
  success(`Created ${pc4.bold(name)} for project ${pc4.bold(project.name)}`);
@@ -345,12 +519,21 @@ dist
345
519
  console.log(` ${pc4.dim("appfunnel dev")}`);
346
520
  console.log();
347
521
  }
522
+ var __dirname, INTERVAL_LABELS;
348
523
  var init_init = __esm({
349
524
  "src/commands/init.ts"() {
350
525
  "use strict";
351
526
  init_logger();
352
527
  init_auth();
353
528
  init_projects();
529
+ init_api();
530
+ __dirname = dirname(fileURLToPath(import.meta.url));
531
+ INTERVAL_LABELS = {
532
+ day: { 1: "day", 7: "week", 14: "2 weeks", 30: "month" },
533
+ week: { 1: "week", 2: "2 weeks", 4: "month", 12: "quarter", 52: "year" },
534
+ month: { 1: "month", 3: "quarter", 6: "6 months", 12: "year" },
535
+ year: { 1: "year" }
536
+ };
354
537
  }
355
538
  });
356
539
 
@@ -449,7 +632,7 @@ async function whoamiCommand() {
449
632
  const creds = requireAuth();
450
633
  const spin = spinner("Verifying credentials\u2026");
451
634
  try {
452
- const response = await fetch(`${DEFAULT_API_BASE2}/user`, {
635
+ const response = await fetch(`${DEFAULT_API_BASE3}/user`, {
453
636
  headers: {
454
637
  Authorization: creds.token,
455
638
  "Content-Type": "application/json"
@@ -480,19 +663,19 @@ async function whoamiCommand() {
480
663
  );
481
664
  }
482
665
  }
483
- var DEFAULT_API_BASE2;
666
+ var DEFAULT_API_BASE3;
484
667
  var init_whoami = __esm({
485
668
  "src/commands/whoami.ts"() {
486
669
  "use strict";
487
670
  init_auth();
488
671
  init_errors();
489
672
  init_logger();
490
- DEFAULT_API_BASE2 = "https://api.appfunnel.net";
673
+ DEFAULT_API_BASE3 = "https://api.appfunnel.net";
491
674
  }
492
675
  });
493
676
 
494
677
  // src/lib/config.ts
495
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
678
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
496
679
  import { join as join3, resolve } from "path";
497
680
  async function loadConfig(cwd) {
498
681
  const configPath = join3(cwd, CONFIG_FILE);
@@ -504,7 +687,7 @@ async function loadConfig(cwd) {
504
687
  );
505
688
  }
506
689
  const { transform } = await import("esbuild");
507
- const raw = readFileSync2(configPath, "utf-8");
690
+ const raw = readFileSync3(configPath, "utf-8");
508
691
  const result = await transform(raw, {
509
692
  loader: "ts",
510
693
  format: "esm",
@@ -536,10 +719,10 @@ var init_config = __esm({
536
719
  });
537
720
 
538
721
  // src/lib/version.ts
539
- import { readFileSync as readFileSync3 } from "fs";
722
+ import { readFileSync as readFileSync4 } from "fs";
540
723
  import { join as join4 } from "path";
541
724
  function checkVersionCompatibility(cwd) {
542
- const cliVersion = "0.6.0";
725
+ const cliVersion = "0.7.0";
543
726
  const sdkVersion = getSdkVersion(cwd);
544
727
  const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
545
728
  const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
@@ -560,7 +743,7 @@ function getSdkVersion(cwd) {
560
743
  "sdk",
561
744
  "package.json"
562
745
  );
563
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
746
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
564
747
  return pkg.version;
565
748
  } catch {
566
749
  throw new CLIError(
@@ -578,7 +761,7 @@ var init_version = __esm({
578
761
  });
579
762
 
580
763
  // src/extract/pages.ts
581
- import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
764
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
582
765
  import { join as join5, basename } from "path";
583
766
  function scanPages(cwd) {
584
767
  const pagesDir = resolvePagesDir(cwd);
@@ -589,7 +772,7 @@ function scanPages(cwd) {
589
772
  "Create src/pages/ and add at least one .tsx page file."
590
773
  );
591
774
  }
592
- const files = readdirSync(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
775
+ const files = readdirSync2(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
593
776
  if (files.length === 0) {
594
777
  throw new CLIError(
595
778
  "NO_PAGES",
@@ -605,7 +788,7 @@ async function extractPageDefinitions(cwd, pageKeys) {
605
788
  const result = {};
606
789
  for (const key of pageKeys) {
607
790
  const filePath = join5(pagesDir, `${key}.tsx`);
608
- const source = readFileSync4(filePath, "utf-8");
791
+ const source = readFileSync5(filePath, "utf-8");
609
792
  const definition = extractDefinePage(ts, source, filePath);
610
793
  if (definition) {
611
794
  result[key] = definition;
@@ -959,7 +1142,7 @@ var init_html = __esm({
959
1142
 
960
1143
  // src/vite/plugin.ts
961
1144
  import { resolve as resolve2, join as join7 } from "path";
962
- import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
1145
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync6 } from "fs";
963
1146
  function appfunnelPlugin(options) {
964
1147
  const { cwd, config, isDev } = options;
965
1148
  let pages = options.pages;
@@ -980,7 +1163,7 @@ function appfunnelPlugin(options) {
980
1163
  return {
981
1164
  name: "appfunnel",
982
1165
  config() {
983
- mkdirSync3(appfunnelDir, { recursive: true });
1166
+ mkdirSync2(appfunnelDir, { recursive: true });
984
1167
  writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
985
1168
  return {
986
1169
  // Don't let Vite auto-serve index.html — we handle it ourselves
@@ -1057,7 +1240,7 @@ function appfunnelPlugin(options) {
1057
1240
  return next();
1058
1241
  }
1059
1242
  try {
1060
- const rawHtml = readFileSync5(htmlPath, "utf-8");
1243
+ const rawHtml = readFileSync6(htmlPath, "utf-8");
1061
1244
  const html = await devServer.transformIndexHtml(req.url || "/", rawHtml);
1062
1245
  res.statusCode = 200;
1063
1246
  res.setHeader("Content-Type", "text/html");
@@ -1085,104 +1268,12 @@ var init_plugin = __esm({
1085
1268
  }
1086
1269
  });
1087
1270
 
1088
- // src/lib/api.ts
1089
- async function apiFetch(path, options) {
1090
- const { token, apiBaseUrl, ...fetchOpts } = options;
1091
- const base = apiBaseUrl || DEFAULT_API_BASE3;
1092
- const url = `${base}${path}`;
1093
- const isFormData = fetchOpts.body instanceof FormData;
1094
- const headers = {
1095
- Authorization: token,
1096
- ...fetchOpts.headers || {}
1097
- };
1098
- if (!isFormData) {
1099
- headers["Content-Type"] = "application/json";
1100
- }
1101
- const response = await fetch(url, {
1102
- ...fetchOpts,
1103
- headers
1104
- });
1105
- if (!response.ok) {
1106
- const body = await response.text().catch(() => "");
1107
- let message = `API request failed: ${response.status} ${response.statusText}`;
1108
- try {
1109
- const parsed = JSON.parse(body);
1110
- if (parsed.error) message = parsed.error;
1111
- if (parsed.message) message = parsed.message;
1112
- } catch {
1113
- }
1114
- const error2 = new CLIError("API_ERROR", message);
1115
- error2.statusCode = response.status;
1116
- throw error2;
1117
- }
1118
- return response;
1119
- }
1120
- async function fetchPrices(projectId, storePriceIds, options) {
1121
- if (storePriceIds.length === 0) return /* @__PURE__ */ new Map();
1122
- const response = await apiFetch(`/project/${projectId}/headless/prices`, {
1123
- ...options,
1124
- method: "POST",
1125
- body: JSON.stringify({ storePriceIds })
1126
- });
1127
- const data = await response.json();
1128
- return new Map(Object.entries(data.prices || {}));
1129
- }
1130
- async function publishBuild(projectId, funnelId, manifest, assets, options) {
1131
- const formData = new FormData();
1132
- formData.set("manifest", JSON.stringify(manifest));
1133
- if (funnelId) {
1134
- formData.set("funnelId", funnelId);
1135
- }
1136
- for (const asset of assets) {
1137
- formData.append(
1138
- "assets",
1139
- new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
1140
- asset.path
1141
- );
1142
- }
1143
- try {
1144
- const response = await apiFetch(`/project/${projectId}/headless/publish`, {
1145
- ...options,
1146
- method: "POST",
1147
- body: formData
1148
- });
1149
- return await response.json();
1150
- } catch (err) {
1151
- if (err instanceof CLIError && err.code === "API_ERROR") {
1152
- if (err.statusCode === 413) {
1153
- throw new CLIError(
1154
- "BUNDLE_TOO_LARGE",
1155
- err.message,
1156
- "Reduce page bundle sizes. Check for large dependencies."
1157
- );
1158
- }
1159
- if (err.statusCode === 409) {
1160
- throw new CLIError(
1161
- "FUNNEL_NOT_HEADLESS",
1162
- err.message,
1163
- "Remove funnelId from config to create a new headless funnel."
1164
- );
1165
- }
1166
- throw new CLIError("PUBLISH_FAILED", err.message);
1167
- }
1168
- throw err;
1169
- }
1170
- }
1171
- var DEFAULT_API_BASE3;
1172
- var init_api = __esm({
1173
- "src/lib/api.ts"() {
1174
- "use strict";
1175
- init_errors();
1176
- DEFAULT_API_BASE3 = "https://api.appfunnel.net";
1177
- }
1178
- });
1179
-
1180
1271
  // src/commands/dev.ts
1181
1272
  var dev_exports = {};
1182
1273
  __export(dev_exports, {
1183
1274
  devCommand: () => devCommand
1184
1275
  });
1185
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1276
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1186
1277
  import { join as join8 } from "path";
1187
1278
  import pc6 from "picocolors";
1188
1279
  async function devCommand(options) {
@@ -1198,7 +1289,7 @@ async function devCommand(options) {
1198
1289
  const projectId = await promptForProject(creds.token);
1199
1290
  config.projectId = projectId;
1200
1291
  const configPath = join8(cwd, "appfunnel.config.ts");
1201
- const configSource = readFileSync6(configPath, "utf-8");
1292
+ const configSource = readFileSync7(configPath, "utf-8");
1202
1293
  let updated;
1203
1294
  if (/projectId:\s*['"]/.test(configSource)) {
1204
1295
  updated = configSource.replace(
@@ -1320,7 +1411,7 @@ __export(build_exports, {
1320
1411
  });
1321
1412
  import { resolve as resolve3, join as join9 } from "path";
1322
1413
  import { randomUUID as randomUUID2 } from "crypto";
1323
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync2 } from "fs";
1414
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync3 } from "fs";
1324
1415
  import pc7 from "picocolors";
1325
1416
  async function buildCommand() {
1326
1417
  const cwd = process.cwd();
@@ -1439,7 +1530,7 @@ async function buildCommand() {
1439
1530
  console.log(` ${pc7.dim("Pages:")} ${pageKeys.length}`);
1440
1531
  console.log(` ${pc7.dim("Size:")} ${formatSize(totalSize)}`);
1441
1532
  console.log();
1442
- for (const asset of assets.filter((a) => a.path.endsWith(".js"))) {
1533
+ for (const asset of assets) {
1443
1534
  const sizeStr = formatSize(asset.size);
1444
1535
  const isOver = asset.size > MAX_PAGE_SIZE;
1445
1536
  console.log(` ${isOver ? pc7.yellow("!") : pc7.dim("\xB7")} ${pc7.dim(asset.path)} ${isOver ? pc7.yellow(sizeStr) : pc7.dim(sizeStr)}`);
@@ -1531,7 +1622,7 @@ function validateConditionVariables(condition, pageKey, allVariables) {
1531
1622
  function collectAssets(outDir) {
1532
1623
  const assets = [];
1533
1624
  function walk(dir, prefix = "") {
1534
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1625
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1535
1626
  const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1536
1627
  const fullPath = join9(dir, entry.name);
1537
1628
  if (entry.isDirectory()) {
@@ -1551,7 +1642,7 @@ function formatSize(bytes) {
1551
1642
  }
1552
1643
  function getSdkVersion2(cwd) {
1553
1644
  try {
1554
- const pkg = JSON.parse(readFileSync7(join9(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
1645
+ const pkg = JSON.parse(readFileSync8(join9(cwd, "node_modules", "@appfunnel", "sdk", "package.json"), "utf-8"));
1555
1646
  return pkg.version;
1556
1647
  } catch {
1557
1648
  return "0.0.0";
@@ -1584,10 +1675,10 @@ var init_build = __esm({
1584
1675
 
1585
1676
  // src/lib/config-patch.ts
1586
1677
  import { join as join10 } from "path";
1587
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
1678
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
1588
1679
  function patchConfigFunnelId(cwd, funnelId) {
1589
1680
  const configPath = join10(cwd, "appfunnel.config.ts");
1590
- let content = readFileSync8(configPath, "utf-8");
1681
+ let content = readFileSync9(configPath, "utf-8");
1591
1682
  if (content.includes("funnelId")) return;
1592
1683
  const patched = content.replace(
1593
1684
  /(projectId:\s*['"][^'"]+['"],?\s*\n)/,
@@ -1610,7 +1701,7 @@ __export(publish_exports, {
1610
1701
  publishCommand: () => publishCommand
1611
1702
  });
1612
1703
  import { resolve as resolve4, join as join11 } from "path";
1613
- import { readFileSync as readFileSync9, existsSync as existsSync6 } from "fs";
1704
+ import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
1614
1705
  import pc8 from "picocolors";
1615
1706
  function getMimeType(path) {
1616
1707
  const ext = path.substring(path.lastIndexOf("."));
@@ -1621,7 +1712,7 @@ function formatSize2(bytes) {
1621
1712
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1622
1713
  return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
1623
1714
  }
1624
- async function publishCommand() {
1715
+ async function publishCommand(options) {
1625
1716
  const cwd = process.cwd();
1626
1717
  const creds = requireAuth();
1627
1718
  checkVersionCompatibility(cwd);
@@ -1643,7 +1734,7 @@ async function publishCommand() {
1643
1734
  "Run 'appfunnel build' first."
1644
1735
  );
1645
1736
  }
1646
- const manifest = JSON.parse(readFileSync9(manifestPath, "utf-8"));
1737
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
1647
1738
  const assets = manifest.assets || [];
1648
1739
  const s = spinner("Preparing assets...");
1649
1740
  const assetPayloads = [];
@@ -1659,7 +1750,7 @@ async function publishCommand() {
1659
1750
  "Run 'appfunnel build' to regenerate."
1660
1751
  );
1661
1752
  }
1662
- const content = readFileSync9(fullPath);
1753
+ const content = readFileSync10(fullPath);
1663
1754
  totalBytes += content.length;
1664
1755
  assetPayloads.push({
1665
1756
  path: asset.path,
@@ -1674,7 +1765,8 @@ async function publishCommand() {
1674
1765
  config.funnelId || "",
1675
1766
  manifest,
1676
1767
  assetPayloads,
1677
- { token: creds.token }
1768
+ { token: creds.token },
1769
+ options?.promote
1678
1770
  );
1679
1771
  s.stop();
1680
1772
  if (result.created && result.funnelId) {
@@ -1682,14 +1774,18 @@ async function publishCommand() {
1682
1774
  info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
1683
1775
  }
1684
1776
  console.log();
1685
- success("Published successfully");
1777
+ success(result.activated ? "Published and activated" : "Published successfully");
1686
1778
  console.log();
1687
- console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
1779
+ console.log(` ${pc8.dim("Build ID:")} ${result.buildId}`);
1688
1780
  if (result.funnelId && !config.funnelId) {
1689
- console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
1781
+ console.log(` ${pc8.dim("Funnel:")} ${result.funnelId}`);
1782
+ }
1783
+ console.log(` ${pc8.dim("Dashboard:")} ${pc8.cyan(result.dashboardUrl)}`);
1784
+ console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
1785
+ if (!result.activated) {
1786
+ console.log();
1787
+ console.log(` ${pc8.dim("Tip:")} Use ${pc8.cyan("--promote")} to activate immediately, or promote from the dashboard.`);
1690
1788
  }
1691
- console.log(` ${pc8.dim("URL:")} ${pc8.cyan(result.url)}`);
1692
- console.log(` ${pc8.dim("Assets:")} ${assets.length} files ${pc8.dim(`(${formatSize2(totalBytes)})`)}`);
1693
1789
  console.log();
1694
1790
  }
1695
1791
  var MIME_TYPES;
@@ -1722,10 +1818,10 @@ init_errors();
1722
1818
  import { Command } from "commander";
1723
1819
  import pc9 from "picocolors";
1724
1820
  var program = new Command();
1725
- program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.6.0");
1726
- program.command("init <name>").description("Create a new AppFunnel project").action(async (name) => {
1821
+ program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.7.0");
1822
+ program.command("init").description("Create a new AppFunnel project").action(async () => {
1727
1823
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1728
- await initCommand2(name);
1824
+ await initCommand2();
1729
1825
  });
1730
1826
  program.command("login").description("Authenticate with AppFunnel").action(async () => {
1731
1827
  const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
@@ -1743,9 +1839,9 @@ program.command("build").description("Build the funnel for production").action(a
1743
1839
  const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
1744
1840
  await buildCommand2();
1745
1841
  });
1746
- program.command("publish").description("Publish the build to AppFunnel").action(async () => {
1842
+ program.command("publish").description("Publish the build to AppFunnel").option("--promote", "Activate the build immediately after publishing").action(async (options) => {
1747
1843
  const { publishCommand: publishCommand2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
1748
- await publishCommand2();
1844
+ await publishCommand2({ promote: options.promote });
1749
1845
  });
1750
1846
  program.hook("postAction", () => {
1751
1847
  });