appfunnel 0.12.0 → 0.14.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
@@ -231,49 +231,21 @@ async function fetchStorePrices(projectId, storeId, options) {
231
231
  const data = await response.json();
232
232
  return data.data || [];
233
233
  }
234
- async function publishBuild(projectId, funnelId, manifest, assets, options, promote) {
235
- const formData = new FormData();
236
- formData.set("manifest", JSON.stringify(manifest));
237
- if (funnelId) {
238
- formData.set("funnelId", funnelId);
239
- }
240
- if (promote) {
241
- formData.set("promote", "true");
242
- }
243
- for (const asset of assets) {
244
- formData.append(
245
- "assets",
246
- new Blob([new Uint8Array(asset.content)], { type: asset.contentType }),
247
- asset.path
248
- );
249
- }
250
- try {
251
- const response = await apiFetch(`/project/${projectId}/headless/publish`, {
252
- ...options,
253
- method: "POST",
254
- body: formData
255
- });
256
- return await response.json();
257
- } catch (err) {
258
- if (err instanceof CLIError && err.code === "API_ERROR") {
259
- if (err.statusCode === 413) {
260
- throw new CLIError(
261
- "BUNDLE_TOO_LARGE",
262
- err.message,
263
- "Reduce page bundle sizes. Check for large dependencies."
264
- );
265
- }
266
- if (err.statusCode === 409) {
267
- throw new CLIError(
268
- "FUNNEL_NOT_HEADLESS",
269
- err.message,
270
- "Remove funnelId from config to create a new headless funnel."
271
- );
272
- }
273
- throw new CLIError("PUBLISH_FAILED", err.message);
274
- }
275
- throw err;
276
- }
234
+ async function publishInit(projectId, body, options) {
235
+ const response = await apiFetch(`/project/${projectId}/headless/publish-init`, {
236
+ ...options,
237
+ method: "POST",
238
+ body: JSON.stringify(body)
239
+ });
240
+ return await response.json();
241
+ }
242
+ async function publishFinalize(projectId, body, options) {
243
+ const response = await apiFetch(`/project/${projectId}/headless/publish-finalize`, {
244
+ ...options,
245
+ method: "POST",
246
+ body: JSON.stringify(body)
247
+ });
248
+ return await response.json();
277
249
  }
278
250
  var DEFAULT_API_BASE2;
279
251
  var init_api = __esm({
@@ -289,25 +261,47 @@ var init_exports = {};
289
261
  __export(init_exports, {
290
262
  initCommand: () => initCommand
291
263
  });
292
- import { cpSync, existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync } from "fs";
293
- import { join as join2, dirname } from "path";
294
- import { fileURLToPath } from "url";
264
+ import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
265
+ import { join as join2 } from "path";
266
+ import { Readable } from "stream";
267
+ import { extract } from "tar";
295
268
  import pc4 from "picocolors";
296
269
  import select2 from "@inquirer/select";
297
270
  import input from "@inquirer/input";
298
- function getTemplatesDir() {
299
- const dir = join2(__dirname, "..", "templates");
300
- if (!existsSync(dir)) {
301
- throw new Error(`Templates directory not found at ${dir}`);
302
- }
303
- return dir;
304
- }
305
- function listTemplates() {
306
- const root = getTemplatesDir();
307
- return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
308
- const configPath = join2(root, d.name, "template.json");
309
- const config = existsSync(configPath) ? JSON.parse(readFileSync2(configPath, "utf-8")) : { name: d.name, description: "", products: [] };
310
- return { dir: d.name, config };
271
+ async function fetchTemplateIndex() {
272
+ const url = `https://raw.githubusercontent.com/${TEMPLATES_REPO}/main/index.json`;
273
+ const res = await fetch(url);
274
+ if (!res.ok) throw new Error(`Failed to fetch template index: ${res.status}`);
275
+ const index = await res.json();
276
+ const templates = await Promise.all(
277
+ index.map(async (entry) => {
278
+ const configUrl = `https://raw.githubusercontent.com/${TEMPLATES_REPO}/main/${entry.dir}/template.json`;
279
+ const configRes = await fetch(configUrl);
280
+ const config = configRes.ok ? await configRes.json() : { name: entry.name, description: entry.description, products: [] };
281
+ return { dir: entry.dir, config };
282
+ })
283
+ );
284
+ return templates;
285
+ }
286
+ async function downloadTemplate(templateDir, destDir) {
287
+ const tarballUrl = `https://api.github.com/repos/${TEMPLATES_REPO}/tarball/main`;
288
+ const res = await fetch(tarballUrl, {
289
+ headers: { Accept: "application/vnd.github+json" }
290
+ });
291
+ if (!res.ok) throw new Error(`Failed to download template: ${res.status}`);
292
+ mkdirSync2(destDir, { recursive: true });
293
+ const nodeStream = Readable.fromWeb(res.body);
294
+ await new Promise((resolve5, reject) => {
295
+ nodeStream.pipe(
296
+ extract({
297
+ cwd: destDir,
298
+ strip: 2,
299
+ filter: (path) => {
300
+ const parts = path.split("/");
301
+ return parts.length > 2 && parts[1] === templateDir;
302
+ }
303
+ })
304
+ ).on("finish", resolve5).on("error", reject);
311
305
  });
312
306
  }
313
307
  function formatInterval(interval, count) {
@@ -352,7 +346,9 @@ async function initCommand(nameArg) {
352
346
  const projectId = await promptForProject(creds.token);
353
347
  const projects = await fetchProjects(creds.token);
354
348
  const project = projects.find((p) => p.id === projectId);
355
- const templates = listTemplates();
349
+ const templateSpinner = spinner("Fetching templates...");
350
+ const templates = await fetchTemplateIndex();
351
+ templateSpinner.stop();
356
352
  const selectedDir = await select2({
357
353
  message: "Choose a template",
358
354
  choices: templates.map((t) => ({
@@ -362,7 +358,6 @@ async function initCommand(nameArg) {
362
358
  });
363
359
  const chosen = templates.find((t) => t.dir === selectedDir);
364
360
  const templateConfig = chosen.config;
365
- const templateDir = join2(getTemplatesDir(), chosen.dir);
366
361
  const productBindings = [];
367
362
  if (templateConfig.products.length > 0) {
368
363
  const storesSpinner = spinner("Fetching stores...");
@@ -444,7 +439,7 @@ async function initCommand(nameArg) {
444
439
  }
445
440
  }
446
441
  const s = spinner(`Creating ${name}...`);
447
- cpSync(templateDir, dir, { recursive: true });
442
+ await downloadTemplate(selectedDir, dir);
448
443
  const templateJsonPath = join2(dir, "template.json");
449
444
  if (existsSync(templateJsonPath)) {
450
445
  const { unlinkSync } = await import("fs");
@@ -455,6 +450,12 @@ async function initCommand(nameArg) {
455
450
  const { renameSync } = await import("fs");
456
451
  renameSync(gitignoreSrc, join2(dir, ".gitignore"));
457
452
  }
453
+ const readmePath = join2(dir, "README.md");
454
+ if (existsSync(readmePath)) {
455
+ let readme = readFileSync2(readmePath, "utf-8");
456
+ readme = readme.replace("__NAME__", name);
457
+ writeFileSync2(readmePath, readme);
458
+ }
458
459
  const configPath = join2(dir, "appfunnel.config.ts");
459
460
  if (existsSync(configPath)) {
460
461
  let config = readFileSync2(configPath, "utf-8");
@@ -485,7 +486,7 @@ ${itemsStr},
485
486
  }
486
487
  writeFileSync2(configPath, config);
487
488
  }
488
- const sdkVersion = `^${"0.12.0"}`;
489
+ const sdkVersion = `^${"0.14.0"}`;
489
490
  writeFileSync2(
490
491
  join2(dir, "package.json"),
491
492
  JSON.stringify(
@@ -528,7 +529,7 @@ ${itemsStr},
528
529
  console.log(` ${pc4.dim("appfunnel dev")}`);
529
530
  console.log();
530
531
  }
531
- var __dirname, INTERVAL_LABELS;
532
+ var TEMPLATES_REPO, INTERVAL_LABELS;
532
533
  var init_init = __esm({
533
534
  "src/commands/init.ts"() {
534
535
  "use strict";
@@ -536,7 +537,7 @@ var init_init = __esm({
536
537
  init_auth();
537
538
  init_projects();
538
539
  init_api();
539
- __dirname = dirname(fileURLToPath(import.meta.url));
540
+ TEMPLATES_REPO = "appfunnel/templates";
540
541
  INTERVAL_LABELS = {
541
542
  day: { 1: "day", 7: "week", 14: "2 weeks", 30: "month" },
542
543
  week: { 1: "week", 2: "2 weeks", 4: "month", 12: "quarter", 52: "year" },
@@ -731,7 +732,7 @@ var init_config = __esm({
731
732
  import { readFileSync as readFileSync4 } from "fs";
732
733
  import { join as join4 } from "path";
733
734
  function checkVersionCompatibility(cwd) {
734
- const cliVersion = "0.12.0";
735
+ const cliVersion = "0.14.0";
735
736
  const sdkVersion = getSdkVersion(cwd);
736
737
  const [cliMajor, cliMinor] = cliVersion.split(".").map(Number);
737
738
  const [sdkMajor, sdkMinor] = sdkVersion.split(".").map(Number);
@@ -770,7 +771,7 @@ var init_version = __esm({
770
771
  });
771
772
 
772
773
  // src/extract/pages.ts
773
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
774
+ import { readdirSync, readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
774
775
  import { join as join5, basename } from "path";
775
776
  function scanPages(cwd) {
776
777
  const pagesDir = resolvePagesDir(cwd);
@@ -781,7 +782,7 @@ function scanPages(cwd) {
781
782
  "Create src/pages/ and add at least one .tsx page file."
782
783
  );
783
784
  }
784
- const files = readdirSync2(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
785
+ const files = readdirSync(pagesDir).filter((f) => f.endsWith(".tsx") && !f.startsWith("_")).map((f) => basename(f, ".tsx")).sort();
785
786
  if (files.length === 0) {
786
787
  throw new CLIError(
787
788
  "NO_PAGES",
@@ -1154,13 +1155,13 @@ var init_html = __esm({
1154
1155
 
1155
1156
  // src/vite/plugin.ts
1156
1157
  import { resolve as resolve2, join as join7 } from "path";
1157
- import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
1158
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
1158
1159
  function loadTranslations(cwd) {
1159
1160
  const localesDir = join7(cwd, "locales");
1160
1161
  if (!existsSync5(localesDir)) return void 0;
1161
1162
  const translations = {};
1162
1163
  let hasAny = false;
1163
- for (const file of readdirSync3(localesDir)) {
1164
+ for (const file of readdirSync2(localesDir)) {
1164
1165
  if (!file.endsWith(".json")) continue;
1165
1166
  const locale = file.replace(/\.json$/, "");
1166
1167
  try {
@@ -1194,7 +1195,7 @@ function appfunnelPlugin(options) {
1194
1195
  return {
1195
1196
  name: "appfunnel",
1196
1197
  config() {
1197
- mkdirSync2(appfunnelDir, { recursive: true });
1198
+ mkdirSync3(appfunnelDir, { recursive: true });
1198
1199
  writeFileSync3(htmlPath, generateHtml(config.name || "AppFunnel"));
1199
1200
  return {
1200
1201
  // Don't let Vite auto-serve index.html — we handle it ourselves
@@ -1835,7 +1836,7 @@ __export(build_exports, {
1835
1836
  });
1836
1837
  import { resolve as resolve3, join as join9 } from "path";
1837
1838
  import { randomUUID as randomUUID2 } from "crypto";
1838
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync4 } from "fs";
1839
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, statSync, readdirSync as readdirSync3 } from "fs";
1839
1840
  import pc8 from "picocolors";
1840
1841
  async function buildCommand() {
1841
1842
  const cwd = process.cwd();
@@ -1950,6 +1951,7 @@ async function buildCommand() {
1950
1951
  responses: config.responses || {},
1951
1952
  queryParams: { ...BUILTIN_QUERY_PARAMS, ...config.queryParams },
1952
1953
  data: config.data || {},
1954
+ user: BUILTIN_USER_VARS,
1953
1955
  products: config.products || {},
1954
1956
  defaultLocale: config.defaultLocale,
1955
1957
  assets,
@@ -1972,7 +1974,7 @@ async function buildCommand() {
1972
1974
  if (totalSize > MAX_TOTAL_SIZE) {
1973
1975
  console.log(formatWarning(
1974
1976
  "BUNDLE_TOO_LARGE",
1975
- `Total bundle size (${formatSize(totalSize)}) exceeds the 2MB limit.`,
1977
+ `Total bundle size (${formatSize(totalSize)}) exceeds the 10MB limit.`,
1976
1978
  "This will be rejected on publish. Reduce dependencies or code-split."
1977
1979
  ));
1978
1980
  console.log();
@@ -2055,7 +2057,7 @@ function validateConditionVariables(condition, pageKey, allVariables) {
2055
2057
  function collectAssets(outDir) {
2056
2058
  const assets = [];
2057
2059
  function walk(dir, prefix = "") {
2058
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
2060
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2059
2061
  const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2060
2062
  const fullPath = join9(dir, entry.name);
2061
2063
  if (entry.isDirectory()) {
@@ -2081,7 +2083,7 @@ function getSdkVersion2(cwd) {
2081
2083
  return "0.0.0";
2082
2084
  }
2083
2085
  }
2084
- var MAX_TOTAL_SIZE, MAX_PAGE_SIZE, BUILTIN_QUERY_PARAMS;
2086
+ var MAX_TOTAL_SIZE, MAX_PAGE_SIZE, BUILTIN_QUERY_PARAMS, BUILTIN_USER_VARS;
2085
2087
  var init_build = __esm({
2086
2088
  "src/commands/build.ts"() {
2087
2089
  "use strict";
@@ -2094,7 +2096,7 @@ var init_build = __esm({
2094
2096
  init_html();
2095
2097
  init_errors();
2096
2098
  init_errors();
2097
- MAX_TOTAL_SIZE = 2 * 1024 * 1024;
2099
+ MAX_TOTAL_SIZE = 10 * 1024 * 1024;
2098
2100
  MAX_PAGE_SIZE = 500 * 1024;
2099
2101
  BUILTIN_QUERY_PARAMS = {
2100
2102
  utm_source: { type: "string" },
@@ -2103,6 +2105,14 @@ var init_build = __esm({
2103
2105
  utm_content: { type: "string" },
2104
2106
  utm_term: { type: "string" }
2105
2107
  };
2108
+ BUILTIN_USER_VARS = {
2109
+ email: { type: "string" },
2110
+ name: { type: "string" },
2111
+ dateOfBirth: { type: "string" },
2112
+ gender: { type: "string" },
2113
+ stripeCustomerId: { type: "string" },
2114
+ paddleCustomerId: { type: "string" }
2115
+ };
2106
2116
  }
2107
2117
  });
2108
2118
 
@@ -2135,10 +2145,22 @@ __export(publish_exports, {
2135
2145
  });
2136
2146
  import { resolve as resolve4, join as join11 } from "path";
2137
2147
  import { readFileSync as readFileSync10, existsSync as existsSync6 } from "fs";
2148
+ import { execSync } from "child_process";
2138
2149
  import pc9 from "picocolors";
2139
- function getMimeType(path) {
2140
- const ext = path.substring(path.lastIndexOf("."));
2141
- return MIME_TYPES[ext] || "application/octet-stream";
2150
+ import select3 from "@inquirer/select";
2151
+ function tryGit(command, cwd) {
2152
+ try {
2153
+ return execSync(command, { cwd, stdio: ["pipe", "pipe", "ignore"] }).toString().trim() || void 0;
2154
+ } catch {
2155
+ return void 0;
2156
+ }
2157
+ }
2158
+ function getGitMetadata(cwd) {
2159
+ const commitSha = tryGit("git rev-parse HEAD", cwd);
2160
+ if (!commitSha) return {};
2161
+ const branch = tryGit("git rev-parse --abbrev-ref HEAD", cwd);
2162
+ const message = tryGit("git log -1 --pretty=%s", cwd);
2163
+ return { commitSha, branch, message };
2142
2164
  }
2143
2165
  function formatSize2(bytes) {
2144
2166
  if (bytes < 1024) return `${bytes}B`;
@@ -2170,7 +2192,7 @@ async function publishCommand(options) {
2170
2192
  const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
2171
2193
  const assets = manifest.assets || [];
2172
2194
  const s = spinner("Preparing assets...");
2173
- const assetPayloads = [];
2195
+ const assetBuffers = [];
2174
2196
  let totalBytes = 0;
2175
2197
  for (let i = 0; i < assets.length; i++) {
2176
2198
  const asset = assets[i];
@@ -2183,45 +2205,133 @@ async function publishCommand(options) {
2183
2205
  "Run 'appfunnel build' to regenerate."
2184
2206
  );
2185
2207
  }
2186
- const content = readFileSync10(fullPath);
2187
- totalBytes += content.length;
2188
- assetPayloads.push({
2189
- path: asset.path,
2190
- content,
2191
- contentType: getMimeType(asset.path)
2192
- });
2208
+ const body = readFileSync10(fullPath);
2209
+ totalBytes += body.length;
2210
+ assetBuffers.push({ path: asset.path, body, size: body.length });
2193
2211
  s.text = `Preparing assets... ${i + 1}/${assets.length} ${pc9.dim(`(${formatSize2(totalBytes)})`)}`;
2194
2212
  }
2195
- s.text = `Uploading ${assets.length} assets ${pc9.dim(`(${formatSize2(totalBytes)})`)}`;
2196
- const result = await publishBuild(
2213
+ s.stop();
2214
+ const MAX_TOTAL_SIZE2 = 10 * 1024 * 1024;
2215
+ if (totalBytes > MAX_TOTAL_SIZE2) {
2216
+ throw new CLIError(
2217
+ "BUNDLE_TOO_LARGE",
2218
+ `Total bundle size (${formatSize2(totalBytes)}) exceeds the ${formatSize2(MAX_TOTAL_SIZE2)} limit.`,
2219
+ "Reduce page bundle sizes. Check for large dependencies."
2220
+ );
2221
+ }
2222
+ info(`${assets.length} assets prepared ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
2223
+ let promote = options?.promote ?? false;
2224
+ if (!promote) {
2225
+ promote = await select3({
2226
+ message: "Publish as the new active version?",
2227
+ choices: [
2228
+ { name: "Yes", value: true },
2229
+ { name: "No", value: false }
2230
+ ]
2231
+ });
2232
+ }
2233
+ const git = getGitMetadata(cwd);
2234
+ const initSpinner = spinner("Initializing publish...");
2235
+ let initResult;
2236
+ try {
2237
+ initResult = await publishInit(
2238
+ projectId,
2239
+ {
2240
+ manifest,
2241
+ funnelId: config.funnelId || void 0,
2242
+ promote,
2243
+ commitSha: git.commitSha,
2244
+ branch: git.branch,
2245
+ message: git.message,
2246
+ triggeredBy: creds.email || void 0
2247
+ },
2248
+ { token: creds.token }
2249
+ );
2250
+ } catch (err) {
2251
+ initSpinner.stop();
2252
+ if (err instanceof CLIError && err.code === "API_ERROR") {
2253
+ if (err.statusCode === 413) {
2254
+ throw new CLIError(
2255
+ "BUNDLE_TOO_LARGE",
2256
+ err.message,
2257
+ "Reduce page bundle sizes. Check for large dependencies."
2258
+ );
2259
+ }
2260
+ if (err.statusCode === 409) {
2261
+ throw new CLIError(
2262
+ "FUNNEL_NOT_HEADLESS",
2263
+ err.message,
2264
+ "Remove funnelId from config to create a new headless funnel."
2265
+ );
2266
+ }
2267
+ throw new CLIError("PUBLISH_FAILED", err.message);
2268
+ }
2269
+ throw err;
2270
+ }
2271
+ initSpinner.stop();
2272
+ info(`Build ${pc9.dim(initResult.buildId)} initialized`);
2273
+ const uploadSpinner = spinner(`Uploading 0/${assets.length} assets...`);
2274
+ const uploadInfoMap = new Map(initResult.assets.map((a) => [a.path, a]));
2275
+ let uploaded = 0;
2276
+ let uploadedBytes = 0;
2277
+ const uploadAsset = async (asset) => {
2278
+ const info2 = uploadInfoMap.get(asset.path);
2279
+ if (!info2) {
2280
+ throw new CLIError("PUBLISH_FAILED", `No upload URL for asset: ${asset.path}`);
2281
+ }
2282
+ const response = await fetch(info2.uploadUrl, {
2283
+ method: "PUT",
2284
+ body: new Uint8Array(asset.body),
2285
+ headers: {
2286
+ "Content-Type": info2.contentType
2287
+ }
2288
+ });
2289
+ if (!response.ok) {
2290
+ throw new CLIError(
2291
+ "PUBLISH_FAILED",
2292
+ `Failed to upload ${asset.path}: ${response.status} ${response.statusText}`
2293
+ );
2294
+ }
2295
+ uploaded++;
2296
+ uploadedBytes += asset.size;
2297
+ uploadSpinner.text = `Uploading ${uploaded}/${assets.length} assets... ${pc9.dim(`(${formatSize2(uploadedBytes)}/${formatSize2(totalBytes)})`)}`;
2298
+ };
2299
+ const queue = [...assetBuffers];
2300
+ const workers = [];
2301
+ for (let i = 0; i < Math.min(UPLOAD_CONCURRENCY, queue.length); i++) {
2302
+ workers.push((async () => {
2303
+ while (queue.length > 0) {
2304
+ const asset = queue.shift();
2305
+ await uploadAsset(asset);
2306
+ }
2307
+ })());
2308
+ }
2309
+ await Promise.all(workers);
2310
+ uploadSpinner.stop();
2311
+ info(`${assets.length} assets uploaded ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
2312
+ const finalizeSpinner = spinner("Finalizing publish...");
2313
+ const result = await publishFinalize(
2197
2314
  projectId,
2198
- config.funnelId || "",
2199
- manifest,
2200
- assetPayloads,
2201
- { token: creds.token },
2202
- options?.promote
2315
+ { buildId: initResult.buildId, funnelId: initResult.funnelId },
2316
+ { token: creds.token }
2203
2317
  );
2204
- s.stop();
2205
- if (result.created && result.funnelId) {
2206
- patchConfigFunnelId(cwd, result.funnelId);
2318
+ finalizeSpinner.stop();
2319
+ if (initResult.created && initResult.funnelId) {
2320
+ patchConfigFunnelId(cwd, initResult.funnelId);
2207
2321
  info(`Funnel created \u2014 funnelId added to appfunnel.config.ts`);
2208
2322
  }
2209
2323
  console.log();
2210
2324
  success(result.activated ? "Published and activated" : "Published successfully");
2211
2325
  console.log();
2212
2326
  console.log(` ${pc9.dim("Build ID:")} ${result.buildId}`);
2213
- if (result.funnelId && !config.funnelId) {
2214
- console.log(` ${pc9.dim("Funnel:")} ${result.funnelId}`);
2327
+ if (initResult.funnelId && !config.funnelId) {
2328
+ console.log(` ${pc9.dim("Funnel:")} ${initResult.funnelId}`);
2215
2329
  }
2216
2330
  console.log(` ${pc9.dim("Dashboard:")} ${pc9.cyan(result.dashboardUrl)}`);
2217
2331
  console.log(` ${pc9.dim("Assets:")} ${assets.length} files ${pc9.dim(`(${formatSize2(totalBytes)})`)}`);
2218
- if (!result.activated) {
2219
- console.log();
2220
- console.log(` ${pc9.dim("Tip:")} Use ${pc9.cyan("--promote")} to activate immediately, or promote from the dashboard.`);
2221
- }
2222
2332
  console.log();
2223
2333
  }
2224
- var MIME_TYPES;
2334
+ var UPLOAD_CONCURRENCY;
2225
2335
  var init_publish = __esm({
2226
2336
  "src/commands/publish.ts"() {
2227
2337
  "use strict";
@@ -2232,17 +2342,7 @@ var init_publish = __esm({
2232
2342
  init_api();
2233
2343
  init_errors();
2234
2344
  init_config_patch();
2235
- MIME_TYPES = {
2236
- ".js": "application/javascript",
2237
- ".css": "text/css",
2238
- ".html": "text/html",
2239
- ".json": "application/json",
2240
- ".svg": "image/svg+xml",
2241
- ".png": "image/png",
2242
- ".jpg": "image/jpeg",
2243
- ".woff2": "font/woff2",
2244
- ".woff": "font/woff"
2245
- };
2345
+ UPLOAD_CONCURRENCY = 10;
2246
2346
  }
2247
2347
  });
2248
2348
 
@@ -2251,7 +2351,7 @@ init_errors();
2251
2351
  import { Command } from "commander";
2252
2352
  import pc10 from "picocolors";
2253
2353
  var program = new Command();
2254
- program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.12.0");
2354
+ program.name("appfunnel").description("Build and publish headless AppFunnel projects").version("0.14.0");
2255
2355
  program.command("init").argument("[name]", "Project directory name").description("Create a new AppFunnel project").action(async (name) => {
2256
2356
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2257
2357
  await initCommand2(name);