saasfunnels 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,6 +102,10 @@ Use `saasfunnels setup run` from the application repository after creating app c
102
102
 
103
103
  The command asks before uploading structured discovery proposals. `--send` provides explicit non-interactive approval. It coordinates features, pricing extraction, and plan branches, reports progress, and waits for the Stripe catalog. Run `saasfunnels setup run --resume` after an interruption. Existing accepted stages and customer review decisions are preserved.
104
104
 
105
- If extraction is unsupported, review a `.saasfunnels/setup-pricing.json` file with `{ "plans": [{ "key": "pro", "name": "Pro", "features": { "export": true }, "prices": [{ "key": "price_ACTUAL_ID" }] }] }`, then resume. Use real identifiers from the connected merchant. Raw source files are never uploaded by guided setup or the pricing handoff.
105
+ The scanner reads supported application files one at a time without the old 500-file, 2 MB repository, or 200-candidate cutoffs. Generated code, tests, and symlinks are excluded. Files over 2 MB and uploads over the server limit stop with a specific recovery message.
106
+
107
+ Static pricing supports literal spreads, capability/quota maps, unlimited limits, and exact Stripe lookup-key matching. It never executes customer code. Pricing definitions are inspected beyond the first ten filename matches; unrelated helpers do not count as plans.
108
+
109
+ If extraction is unsupported, `.saasfunnels/setup-review.json` lists the files and reasons. Correct those declarations or review a `.saasfunnels/setup-pricing.json` file with `{ "plans": [{ "key": "pro", "name": "Pro", "features": { "export": true }, "prices": [{ "key": "price_ACTUAL_ID" }] }] }`, then resume. Use real identifiers from the connected merchant. Raw source files are never uploaded by guided setup or the pricing handoff.
106
110
 
107
111
  The command does not implement arbitrary payment code, approve feature access, or verify transactions. Complete **Review and verify** in Setup after deploying the runtime and payment integration. Requires the server's guided setup API release.
package/dist/library.js CHANGED
@@ -24264,7 +24264,7 @@ function validateFeatureCatalogManifest(value) {
24264
24264
 
24265
24265
  // src/identity.ts
24266
24266
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
24267
- var SAASFUNNELS_CLI_VERSION = "0.3.1";
24267
+ var SAASFUNNELS_CLI_VERSION = "0.3.2";
24268
24268
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
24269
24269
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
24270
24270
  var SAASFUNNELS_ENV = Object.freeze({
@@ -8690,9 +8690,9 @@ ${lanes.join("\n")}
8690
8690
  return process.memoryUsage().heapUsed;
8691
8691
  },
8692
8692
  getFileSize(path) {
8693
- const stat4 = statSync(path);
8694
- if (stat4 == null ? void 0 : stat4.isFile()) {
8695
- return stat4.size;
8693
+ const stat = statSync(path);
8694
+ if (stat == null ? void 0 : stat.isFile()) {
8695
+ return stat.size;
8696
8696
  }
8697
8697
  return 0;
8698
8698
  },
@@ -8910,19 +8910,19 @@ ${lanes.join("\n")}
8910
8910
  if (entry === "." || entry === "..") {
8911
8911
  continue;
8912
8912
  }
8913
- let stat4;
8913
+ let stat;
8914
8914
  if (typeof dirent === "string" || dirent.isSymbolicLink()) {
8915
8915
  const name = combinePaths(path, entry);
8916
- stat4 = statSync(name);
8917
- if (!stat4) {
8916
+ stat = statSync(name);
8917
+ if (!stat) {
8918
8918
  continue;
8919
8919
  }
8920
8920
  } else {
8921
- stat4 = dirent;
8921
+ stat = dirent;
8922
8922
  }
8923
- if (stat4.isFile()) {
8923
+ if (stat.isFile()) {
8924
8924
  files.push(entry);
8925
- } else if (stat4.isDirectory()) {
8925
+ } else if (stat.isDirectory()) {
8926
8926
  directories.push(entry);
8927
8927
  }
8928
8928
  }
@@ -8937,15 +8937,15 @@ ${lanes.join("\n")}
8937
8937
  return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
8938
8938
  }
8939
8939
  function fileSystemEntryExists(path, entryKind) {
8940
- const stat4 = statSync(path);
8941
- if (!stat4) {
8940
+ const stat = statSync(path);
8941
+ if (!stat) {
8942
8942
  return false;
8943
8943
  }
8944
8944
  switch (entryKind) {
8945
8945
  case 0:
8946
- return stat4.isFile();
8946
+ return stat.isFile();
8947
8947
  case 1:
8948
- return stat4.isDirectory();
8948
+ return stat.isDirectory();
8949
8949
  default:
8950
8950
  return false;
8951
8951
  }
@@ -218948,8 +218948,20 @@ import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:pa
218948
218948
  // src/setup-pricing.ts
218949
218949
  var import_typescript = __toESM(require_typescript(), 1);
218950
218950
  var import_yaml = __toESM(require_dist(), 1);
218951
+ var PricingExtractionError = class extends Error {
218952
+ constructor(code, message) {
218953
+ super(message);
218954
+ this.code = code;
218955
+ }
218956
+ code;
218957
+ };
218958
+ var unsupported = () => new PricingExtractionError(
218959
+ "needs_review",
218960
+ "Pricing contains a value that needs developer review."
218961
+ );
218951
218962
  var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
218952
218963
  var safeKey = (v) => typeof v === "string" && /^[a-z][a-z0-9_.-]{0,79}$/.test(v);
218964
+ var safeProperty = (key2) => !["__proto__", "constructor", "prototype"].includes(key2);
218953
218965
  function declarations(source) {
218954
218966
  const file2 = import_typescript.default.createSourceFile(
218955
218967
  "pricing.ts",
@@ -218960,47 +218972,107 @@ function declarations(source) {
218960
218972
  );
218961
218973
  const values = /* @__PURE__ */ new Map();
218962
218974
  const roots = [];
218975
+ const planName = /^(plans?|pricing|tiers?|billingCatalog|planCatalog|subscriptionPlans)$/i;
218976
+ const containsAccess = (node, fields = /^(features|capabilities|quotas)$/) => {
218977
+ if (import_typescript.default.isPropertyAssignment(node) && fields.test(node.name.getText(file2).replace(/["']/g, "")))
218978
+ return true;
218979
+ return import_typescript.default.forEachChild(
218980
+ node,
218981
+ (child) => containsAccess(child, fields) ? true : void 0
218982
+ ) ?? false;
218983
+ };
218963
218984
  for (const statement of file2.statements) {
218964
218985
  if (import_typescript.default.isVariableStatement(statement))
218965
218986
  for (const declaration of statement.declarationList.declarations) {
218966
218987
  if (import_typescript.default.isIdentifier(declaration.name) && declaration.initializer) {
218967
218988
  values.set(declaration.name.text, declaration.initializer);
218968
- roots.push(declaration.initializer);
218989
+ if (planName.test(declaration.name.text) || containsAccess(declaration.initializer) && containsAccess(
218990
+ declaration.initializer,
218991
+ /^(prices|stripePriceId)$/
218992
+ ))
218993
+ roots.push(declaration.initializer);
218969
218994
  }
218970
218995
  }
218971
- if (import_typescript.default.isExportAssignment(statement)) roots.push(statement.expression);
218996
+ if (import_typescript.default.isExportAssignment(statement) && containsAccess(statement.expression))
218997
+ roots.push(statement.expression);
218972
218998
  }
218973
- function read(node, depth = 0) {
218974
- if (depth > 20) throw new Error("Unsupported pricing expression");
218999
+ function read(node, depth = 0, locals = /* @__PURE__ */ new Map()) {
219000
+ if (depth > 30) throw unsupported();
219001
+ const next = (value) => read(value, depth + 1, locals);
218975
219002
  if (import_typescript.default.isAsExpression(node) || import_typescript.default.isSatisfiesExpression(node) || import_typescript.default.isParenthesizedExpression(node))
218976
- return read(node.expression, depth + 1);
219003
+ return next(node.expression);
218977
219004
  if (import_typescript.default.isStringLiteral(node) || import_typescript.default.isNoSubstitutionTemplateLiteral(node))
218978
219005
  return node.text;
218979
- if (import_typescript.default.isNumericLiteral(node)) return Number(node.text);
219006
+ if (import_typescript.default.isNumericLiteral(node)) return Number(node.text.replaceAll("_", ""));
218980
219007
  if (node.kind === import_typescript.default.SyntaxKind.TrueKeyword) return true;
218981
219008
  if (node.kind === import_typescript.default.SyntaxKind.FalseKeyword) return false;
218982
- if (import_typescript.default.isIdentifier(node) && values.has(node.text))
218983
- return read(values.get(node.text), depth + 1);
219009
+ if (import_typescript.default.isIdentifier(node)) {
219010
+ if (locals.has(node.text)) return locals.get(node.text);
219011
+ if (values.has(node.text)) return next(values.get(node.text));
219012
+ }
218984
219013
  if (import_typescript.default.isArrayLiteralExpression(node))
218985
- return node.elements.map((n) => read(n, depth + 1));
219014
+ return node.elements.flatMap((n) => {
219015
+ if (!import_typescript.default.isSpreadElement(n)) return [next(n)];
219016
+ const spread = next(n.expression);
219017
+ if (!Array.isArray(spread)) throw unsupported();
219018
+ return spread;
219019
+ });
218986
219020
  if (import_typescript.default.isObjectLiteralExpression(node)) {
218987
219021
  const result3 = /* @__PURE__ */ Object.create(null);
218988
219022
  for (const property of node.properties) {
219023
+ if (import_typescript.default.isSpreadAssignment(property)) {
219024
+ const spread = object(next(property.expression));
219025
+ if (!spread) throw unsupported();
219026
+ Object.assign(result3, spread);
219027
+ continue;
219028
+ }
218989
219029
  if (!import_typescript.default.isPropertyAssignment(property) || import_typescript.default.isComputedPropertyName(property.name))
218990
- throw new Error("Unsupported pricing expression");
218991
- result3[property.name.getText(file2).replace(/^["']|["']$/g, "")] = read(
218992
- property.initializer,
218993
- depth + 1
218994
- );
219030
+ throw unsupported();
219031
+ const key2 = property.name.getText(file2).replace(/^["']|["']$/g, "");
219032
+ if (!safeProperty(key2)) throw unsupported();
219033
+ result3[key2] = next(property.initializer);
218995
219034
  }
218996
219035
  return result3;
218997
219036
  }
218998
- throw new Error("Unsupported pricing expression");
219037
+ if (import_typescript.default.isCallExpression(node) && import_typescript.default.isPropertyAccessExpression(node.expression)) {
219038
+ const callee = node.expression;
219039
+ if (import_typescript.default.isIdentifier(callee.expression) && callee.expression.text === "Object" && !values.has("Object") && callee.name.text === "fromEntries" && node.arguments.length === 1) {
219040
+ const entries = next(node.arguments[0]);
219041
+ if (!Array.isArray(entries) || entries.length > 1e4)
219042
+ throw unsupported();
219043
+ const result3 = /* @__PURE__ */ Object.create(null);
219044
+ for (const entry of entries) {
219045
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || !safeProperty(entry[0]))
219046
+ throw unsupported();
219047
+ result3[entry[0]] = entry[1];
219048
+ }
219049
+ return result3;
219050
+ }
219051
+ if (callee.name.text === "map" && node.arguments.length === 1) {
219052
+ const array2 = next(callee.expression);
219053
+ const callback = node.arguments[0];
219054
+ if (!Array.isArray(array2) || array2.length > 1e4 || !import_typescript.default.isArrowFunction(callback) || callback.parameters.length !== 1 || !import_typescript.default.isIdentifier(callback.parameters[0].name) || import_typescript.default.isBlock(callback.body))
219055
+ throw unsupported();
219056
+ const parameter = callback.parameters[0].name.text;
219057
+ return array2.map(
219058
+ (value) => read(
219059
+ callback.body,
219060
+ depth + 1,
219061
+ new Map([...locals, [parameter, value]])
219062
+ )
219063
+ );
219064
+ }
219065
+ }
219066
+ throw unsupported();
218999
219067
  }
219000
219068
  return roots.map((node) => read(node));
219001
219069
  }
219002
- function extractSetupPricing(source, kind) {
219003
- if (source.length > 512e3) throw new Error("Pricing file exceeds limit");
219070
+ function extractSetupPricing(source, kind, catalogPrices = []) {
219071
+ if (source.length > 2e6)
219072
+ throw new PricingExtractionError(
219073
+ "needs_review",
219074
+ "Pricing file exceeds the 2 MB per-file limit."
219075
+ );
219004
219076
  const documents = kind === "typescript" ? declarations(source) : kind === "json" ? [JSON.parse(source)] : [
219005
219077
  (0, import_yaml.parseDocument)(source, { uniqueKeys: true }).toJS({
219006
219078
  maxAliasCount: 10
@@ -219008,8 +219080,7 @@ function extractSetupPricing(source, kind) {
219008
219080
  ];
219009
219081
  const plans = /* @__PURE__ */ new Map();
219010
219082
  for (const doc of documents) {
219011
- const root = object(doc);
219012
- const candidate = root?.plans ?? doc;
219083
+ const candidate = object(doc)?.plans ?? doc;
219013
219084
  const rows = Array.isArray(candidate) ? candidate : object(candidate) ? Object.entries(candidate).map(
219014
219085
  ([key2, value]) => ({ key: key2, ...object(value) })
219015
219086
  ) : [];
@@ -219020,30 +219091,45 @@ function extractSetupPricing(source, kind) {
219020
219091
  const name = row.name ?? row.displayName ?? key2;
219021
219092
  if (!safeKey(key2) || typeof name !== "string" || !name.length || name.length > 120)
219022
219093
  continue;
219023
- if (!("features" in row) && !("prices" in row) && !("stripePriceId" in row) && !("productKey" in row))
219094
+ if (![
219095
+ "features",
219096
+ "capabilities",
219097
+ "quotas",
219098
+ "prices",
219099
+ "stripePriceId",
219100
+ "productKey"
219101
+ ].some((k) => k in row))
219024
219102
  continue;
219025
- const features = {};
219026
- const rawFeatures = object(row.features);
219027
- if (rawFeatures)
219028
- for (const [feature, access2] of Object.entries(rawFeatures)) {
219029
- if (!safeKey(feature) || !(typeof access2 === "boolean" || typeof access2 === "number" && Number.isFinite(access2) && access2 >= 0))
219030
- throw new Error("Feature access needs manual review");
219103
+ const features = /* @__PURE__ */ Object.create(null);
219104
+ for (const field of ["features", "capabilities", "quotas"]) {
219105
+ const raw = row[field];
219106
+ const entries = object(raw) ? Object.entries(raw) : Array.isArray(raw) && raw.every(safeKey) ? raw.map((key3) => [key3, true]) : raw === void 0 ? [] : null;
219107
+ if (!entries) throw unsupported();
219108
+ for (const [feature, access2] of entries) {
219109
+ if (!safeKey(feature) || !safeProperty(feature) || !(typeof access2 === "boolean" || access2 === "unlimited" || typeof access2 === "number" && Number.isFinite(access2) && access2 >= 0))
219110
+ throw unsupported();
219111
+ if (features[feature] === false && access2 !== false)
219112
+ throw unsupported();
219031
219113
  features[feature] = access2;
219032
219114
  }
219033
- else if (Array.isArray(row.features) && row.features.every(safeKey)) {
219034
- for (const feature of row.features) features[feature] = true;
219035
- } else if (row.features !== void 0)
219036
- throw new Error("Feature access needs manual review");
219037
- const rawPrices = Array.isArray(row.prices) ? row.prices : row.stripePriceId ?? row.priceId ? [{ key: row.stripePriceId ?? row.priceId }] : [];
219115
+ }
219116
+ const rawPrices = Array.isArray(row.prices) ? row.prices : object(row.prices) ? Object.values(row.prices) : row.stripePriceId ?? row.priceId ? [{ key: row.stripePriceId ?? row.priceId }] : [];
219038
219117
  const prices = rawPrices.map((value2) => {
219039
219118
  const price = typeof value2 === "string" ? value2 : object(value2)?.key ?? object(value2)?.stripePriceId;
219040
- if (typeof price !== "string" || !/^price_[A-Za-z0-9]+$/.test(price))
219041
- throw new Error("Price needs manual review");
219042
- return { key: price };
219119
+ if (typeof price === "string" && /^price_[A-Za-z0-9]+$/.test(price))
219120
+ return { key: price };
219121
+ const lookup = object(value2)?.lookupKey ?? object(value2)?.lookup_key;
219122
+ const matches = typeof lookup === "string" ? catalogPrices.filter((p) => p.lookupKey === lookup) : [];
219123
+ if (matches.length !== 1)
219124
+ throw new PricingExtractionError(
219125
+ "needs_review",
219126
+ "A Stripe lookup key has no unique match in the connected catalog."
219127
+ );
219128
+ return { key: matches[0].id };
219043
219129
  });
219044
219130
  const productKey = row.productKey ?? row.stripeProductId;
219045
219131
  if (productKey !== void 0 && (typeof productKey !== "string" || !/^prod_[A-Za-z0-9]+$/.test(productKey)))
219046
- throw new Error("Product needs manual review");
219132
+ throw unsupported();
219047
219133
  const plan = {
219048
219134
  key: key2,
219049
219135
  name,
@@ -219052,12 +219138,23 @@ function extractSetupPricing(source, kind) {
219052
219138
  ...typeof productKey === "string" ? { productKey } : {}
219053
219139
  };
219054
219140
  if (plans.has(key2) && JSON.stringify(plans.get(key2)) !== JSON.stringify(plan))
219055
- throw new Error("Conflicting plan declarations");
219141
+ throw new PricingExtractionError(
219142
+ "needs_review",
219143
+ "Conflicting plan declarations."
219144
+ );
219056
219145
  plans.set(key2, plan);
219057
219146
  }
219058
219147
  }
219059
- if (!plans.size) throw new Error("No supported pricing declaration found");
219060
- if (plans.size > 200) throw new Error("Too many pricing declarations");
219148
+ if (!plans.size)
219149
+ throw new PricingExtractionError(
219150
+ "no_declarations",
219151
+ "No supported pricing declaration found."
219152
+ );
219153
+ if (plans.size > 200)
219154
+ throw new PricingExtractionError(
219155
+ "needs_review",
219156
+ "More than 200 plan declarations need review."
219157
+ );
219061
219158
  return [...plans.values()];
219062
219159
  }
219063
219160
 
@@ -233592,7 +233689,7 @@ import {
233592
233689
  readdir,
233593
233690
  rename,
233594
233691
  rm,
233595
- stat,
233692
+ lstat,
233596
233693
  writeFile
233597
233694
  } from "node:fs/promises";
233598
233695
  import { dirname, extname, join, relative, resolve as resolve2 } from "node:path";
@@ -235742,7 +235839,17 @@ var featureBindingKinds = [
235742
235839
  "funnel_trigger",
235743
235840
  "discovery"
235744
235841
  ];
235745
- var defaultRoots = ["app", "src", "lib", "components", "pages"];
235842
+ var defaultRoots = [
235843
+ "app",
235844
+ "src",
235845
+ "lib",
235846
+ "components",
235847
+ "pages",
235848
+ "apps",
235849
+ "packages",
235850
+ "server",
235851
+ "client"
235852
+ ];
235746
235853
  var ignoredSegments = /* @__PURE__ */ new Set([
235747
235854
  ".claude",
235748
235855
  ".git",
@@ -235751,6 +235858,13 @@ var ignoredSegments = /* @__PURE__ */ new Set([
235751
235858
  ".turbo",
235752
235859
  "build",
235753
235860
  "coverage",
235861
+ ".saasfunnels",
235862
+ "docs",
235863
+ "e2e",
235864
+ "fixtures",
235865
+ "__tests__",
235866
+ "tests",
235867
+ "__mocks__",
235754
235868
  "dist",
235755
235869
  "generated",
235756
235870
  "node_modules",
@@ -235849,7 +235963,11 @@ function allowedPath(path, excludes) {
235849
235963
  const normalized = path.replaceAll("\\", "/");
235850
235964
  if (sensitivePathPattern.test(normalized)) return false;
235851
235965
  const segments = normalized.split("/");
235852
- if (segments.some((segment) => ignoredSegments.has(segment))) return false;
235966
+ if (segments.some(
235967
+ (segment) => ignoredSegments.has(segment) || /^\.next(?:[-.]|$)/.test(segment)
235968
+ ))
235969
+ return false;
235970
+ if (/\.(test|spec|stories|d)\.[cm]?[jt]sx?$/.test(normalized)) return false;
235853
235971
  return !excludes.some(
235854
235972
  (exclude) => normalized === exclude || normalized.startsWith(`${exclude}/`)
235855
235973
  );
@@ -235857,13 +235975,17 @@ function allowedPath(path, excludes) {
235857
235975
  async function walkFiles(input) {
235858
235976
  const files = [];
235859
235977
  const queue = input.roots.map((root) => resolve2(input.cwd, root));
235860
- while (queue.length && files.length < 500) {
235978
+ const seen = /* @__PURE__ */ new Set();
235979
+ let oversizedFileCount = 0;
235980
+ while (queue.length) {
235861
235981
  const current = queue.shift();
235982
+ if (seen.has(current)) continue;
235983
+ seen.add(current);
235862
235984
  const relativePath = safeRelative(input.cwd, current);
235863
235985
  if (!relativePath || !allowedPath(relativePath, input.excludes)) continue;
235864
235986
  let currentStat;
235865
235987
  try {
235866
- currentStat = await stat(current);
235988
+ currentStat = await lstat(current);
235867
235989
  } catch {
235868
235990
  continue;
235869
235991
  }
@@ -235872,11 +235994,15 @@ async function walkFiles(input) {
235872
235994
  entries.sort((left, right) => left.name.localeCompare(right.name)).forEach((entry) => queue.push(join(current, entry.name)));
235873
235995
  continue;
235874
235996
  }
235875
- if (currentStat.isFile() && currentStat.size <= 256e3 && supportedExtensions.has(extname(current))) {
235997
+ if (currentStat.isFile() && supportedExtensions.has(extname(current))) {
235998
+ if (currentStat.size > 2e6) {
235999
+ oversizedFileCount++;
236000
+ continue;
236001
+ }
235876
236002
  files.push(current);
235877
236003
  }
235878
236004
  }
235879
- return files.sort();
236005
+ return { files: files.sort(), oversizedFileCount };
235880
236006
  }
235881
236007
  function nearestSymbol(lines, lineIndex) {
235882
236008
  for (let index = lineIndex; index >= Math.max(0, lineIndex - 20); index -= 1) {
@@ -235987,7 +236113,9 @@ async function detectFramework(cwd2, requestedRoots) {
235987
236113
  };
235988
236114
  const framework = typeof dependencies.next === "string" ? "nextjs" : "unsupported";
235989
236115
  const packageManager = await exists(join(cwd2, "pnpm-lock.yaml")) ? "pnpm" : await exists(join(cwd2, "yarn.lock")) ? "yarn" : await exists(join(cwd2, "bun.lockb")) || await exists(join(cwd2, "bun.lock")) ? "bun" : await exists(join(cwd2, "package-lock.json")) ? "npm" : "unknown";
235990
- const roots = (requestedRoots?.length ? requestedRoots : defaultRoots).filter((root) => !root.startsWith("../") && !root.startsWith("/"));
236116
+ const roots = (requestedRoots?.length ? requestedRoots : defaultRoots).filter(
236117
+ (root) => !root.startsWith("../") && !root.startsWith("/")
236118
+ );
235991
236119
  const existingRoots = [];
235992
236120
  for (const root of roots) {
235993
236121
  if (await exists(resolve2(cwd2, root))) existingRoots.push(root);
@@ -235995,7 +236123,9 @@ async function detectFramework(cwd2, requestedRoots) {
235995
236123
  return {
235996
236124
  framework,
235997
236125
  packageManager,
235998
- saasFunnelsPackages: Object.keys(dependencies).filter((name) => name === "saasfunnels" || name.startsWith("@saasfunnels/")).sort(),
236126
+ saasFunnelsPackages: Object.keys(dependencies).filter(
236127
+ (name) => name === "saasfunnels" || name.startsWith("@saasfunnels/")
236128
+ ).sort(),
235999
236129
  roots: existingRoots
236000
236130
  };
236001
236131
  }
@@ -236102,11 +236232,16 @@ async function applyFeatureSetupChangesAtomically(cwd2, changes, io = {
236102
236232
  const absolute = resolve2(cwd2, change.path);
236103
236233
  const relativePath = safeRelative(cwd2, absolute);
236104
236234
  if (!relativePath)
236105
- throw new Error(`Refusing to write outside the repository: ${change.path}`);
236235
+ throw new Error(
236236
+ `Refusing to write outside the repository: ${change.path}`
236237
+ );
236106
236238
  await io.mkdir(dirname(absolute), { recursive: true });
236107
236239
  originals.set(absolute, await io.read(absolute));
236108
236240
  const stagedPath = `${absolute}.saasfunnels-${transactionId}.tmp`;
236109
- await io.write(stagedPath, change.contents, { encoding: "utf8", mode: 384 });
236241
+ await io.write(stagedPath, change.contents, {
236242
+ encoding: "utf8",
236243
+ mode: 384
236244
+ });
236110
236245
  staged.push({ absolute, staged: stagedPath });
236111
236246
  }
236112
236247
  for (const entry of staged) await io.rename(entry.staged, entry.absolute);
@@ -236220,10 +236355,12 @@ async function runFeatureSetup(input) {
236220
236355
  `Inspect these local roots only: ${framework.roots.join(", ") || "none"}? [y/N] `
236221
236356
  );
236222
236357
  if (!/^y(?:es)?$/i.test(confirmation.trim())) {
236223
- throw new Error("Feature discovery was cancelled before reading source files.");
236358
+ throw new Error(
236359
+ "Feature discovery was cancelled before reading source files."
236360
+ );
236224
236361
  }
236225
236362
  }
236226
- const files = await walkFiles({
236363
+ const { files, oversizedFileCount } = await walkFiles({
236227
236364
  cwd: input.cwd,
236228
236365
  excludes: input.exclude ?? [],
236229
236366
  roots: framework.roots
@@ -236231,14 +236368,10 @@ async function runFeatureSetup(input) {
236231
236368
  const discovered = [];
236232
236369
  let scannedBytes = 0;
236233
236370
  let scannedFileCount = 0;
236234
- let byteLimitReached = false;
236371
+ const byteLimitReached = oversizedFileCount > 0;
236235
236372
  for (const file2 of files) {
236236
236373
  const source = await readFile2(file2, "utf8");
236237
236374
  scannedBytes += Buffer.byteLength(source);
236238
- if (scannedBytes > 2e6) {
236239
- byteLimitReached = true;
236240
- break;
236241
- }
236242
236375
  const path = safeRelative(input.cwd, file2);
236243
236376
  if (!path) continue;
236244
236377
  scannedFileCount++;
@@ -236249,9 +236382,14 @@ async function runFeatureSetup(input) {
236249
236382
  (left, right) => right.confidenceBasisPoints - left.confidenceBasisPoints || left.file.localeCompare(right.file) || left.line - right.line
236250
236383
  )) {
236251
236384
  const identity = `${candidate.suggestedKey}:${candidate.accessModel}:${candidate.file}:${candidate.line}`;
236252
- if (!uniqueCandidates.has(identity)) uniqueCandidates.set(identity, candidate);
236385
+ if (!uniqueCandidates.has(identity))
236386
+ uniqueCandidates.set(identity, candidate);
236253
236387
  }
236254
- const candidates = [...uniqueCandidates.values()].slice(0, 200);
236388
+ const candidates = [...uniqueCandidates.values()];
236389
+ if (new Set(candidates.map((candidate) => candidate.suggestedKey)).size > 5e3)
236390
+ throw new Error(
236391
+ "Feature discovery found more than 5,000 distinct features. Narrow the source roots before retrying."
236392
+ );
236255
236393
  const manifestPath = ".saasfunnels/catalog.yaml";
236256
236394
  const existingManifestSource = await readable(
236257
236395
  resolve2(input.cwd, manifestPath)
@@ -236271,7 +236409,8 @@ async function runFeatureSetup(input) {
236271
236409
  const accept = new Set(input.accept ?? []);
236272
236410
  const reject = new Set(input.reject ?? []);
236273
236411
  for (const candidate of candidates) {
236274
- if (reject.has(candidate.id) || reject.has(candidate.suggestedKey)) continue;
236412
+ if (reject.has(candidate.id) || reject.has(candidate.suggestedKey))
236413
+ continue;
236275
236414
  if (accept.has("all") || accept.has(candidate.id) || accept.has(candidate.suggestedKey)) {
236276
236415
  accepted.add(candidate.id);
236277
236416
  continue;
@@ -236285,7 +236424,10 @@ async function runFeatureSetup(input) {
236285
236424
  if (/^a(?:ccept)?$/i.test(answer.trim())) accepted.add(candidate.id);
236286
236425
  else if (keyPattern.test(answer.trim())) {
236287
236426
  accepted.add(candidate.id);
236288
- input.mappings = { ...input.mappings ?? {}, [candidate.id]: answer.trim() };
236427
+ input.mappings = {
236428
+ ...input.mappings ?? {},
236429
+ [candidate.id]: answer.trim()
236430
+ };
236289
236431
  }
236290
236432
  }
236291
236433
  }
@@ -236420,7 +236562,15 @@ async function runFeatureSetup(input) {
236420
236562
  reviewUrl,
236421
236563
  runtimeCheck: check2,
236422
236564
  scannedFileCount,
236423
- coverage: { complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 200, matchedFiles: files.length, scannedFiles: scannedFileCount, byteLimitReached, candidateLimitReached: uniqueCandidates.size > 200 },
236565
+ coverage: {
236566
+ complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 1e4,
236567
+ matchedFiles: files.length + oversizedFileCount,
236568
+ scannedFiles: scannedFileCount,
236569
+ scannedBytes,
236570
+ oversizedFileCount,
236571
+ byteLimitReached,
236572
+ candidateLimitReached: uniqueCandidates.size > 1e4
236573
+ },
236424
236574
  validation: { errors: validation.errors, ok: validation.ok }
236425
236575
  };
236426
236576
  }
@@ -236502,7 +236652,7 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
236502
236652
  }
236503
236653
 
236504
236654
  // src/plan-sources.ts
236505
- import { readFile as readFile3, readdir as readdir2, stat as stat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236655
+ import { readFile as readFile3, readdir as readdir2, lstat as lstat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236506
236656
  import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
236507
236657
  var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
236508
236658
  var maxPlanSourceFiles = 12;
@@ -236533,10 +236683,9 @@ var planSourceKindByExtension = /* @__PURE__ */ new Map([
236533
236683
  [".yml", "yaml"]
236534
236684
  ]);
236535
236685
  var billingIntervalPattern = /\b(month|monthly|year|yearly|annual|annually|week|weekly|day|daily|recurring|interval|billing_period)\b/i;
236536
- var chargeAmountPattern = /\b(unit_amount|unit_amount_decimal|amount|amount_decimal|price|prices|currency|usd|eur|gbp|cents)\b/i;
236686
+ var chargeAmountPattern = /\b(unit_amount|unit_amount_decimal|unitAmount|amount|amount_decimal|price|prices|currency|usd|eur|gbp|cents)\b/i;
236537
236687
  var stripePriceIdPattern = /["'`]price_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{14,}["'`]/;
236538
236688
  var nonDefinitionPattern = /(^|\/)(__tests__|__mocks__|tests?|specs?)(\/)|\.(test|spec|d)\.[cm]?[jt]sx?$/i;
236539
- var maxNamedOnlyCandidates = 10;
236540
236689
  function normalize(path) {
236541
236690
  return path.split(sep).join("/");
236542
236691
  }
@@ -236561,7 +236710,7 @@ async function discoverPlanSourceCandidates(input) {
236561
236710
  const seen = /* @__PURE__ */ new Set();
236562
236711
  const candidates = [];
236563
236712
  const queue = roots.map((root) => resolve3(input.cwd, root));
236564
- while (queue.length && candidates.length < 200) {
236713
+ while (queue.length) {
236565
236714
  const current = queue.shift();
236566
236715
  if (seen.has(current)) continue;
236567
236716
  seen.add(current);
@@ -236569,13 +236718,16 @@ async function discoverPlanSourceCandidates(input) {
236569
236718
  if (relativePath && !allowed(relativePath, excludes)) continue;
236570
236719
  let entryStat;
236571
236720
  try {
236572
- entryStat = await stat2(current);
236721
+ entryStat = await lstat2(current);
236573
236722
  } catch {
236574
236723
  continue;
236575
236724
  }
236725
+ if (entryStat.isSymbolicLink()) continue;
236576
236726
  if (entryStat.isDirectory()) {
236577
236727
  const entries = await readdir2(current, { withFileTypes: true });
236578
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
236728
+ for (const entry of entries.sort(
236729
+ (a, b) => a.name.localeCompare(b.name)
236730
+ )) {
236579
236731
  queue.push(join2(current, entry.name));
236580
236732
  }
236581
236733
  continue;
@@ -236583,7 +236735,13 @@ async function discoverPlanSourceCandidates(input) {
236583
236735
  if (!entryStat.isFile()) continue;
236584
236736
  const kind = planSourceKindByExtension.get(extname2(current).toLowerCase());
236585
236737
  if (!kind || !relativePath) continue;
236586
- if (entryStat.size > maxPlanSourceCharacters) continue;
236738
+ if (entryStat.size > 2e6) {
236739
+ if (looksLikePlanSource(relativePath))
236740
+ throw new Error(
236741
+ "Plan discovery found a pricing file above the 2 MB per-file limit. Split large pricing files before retrying."
236742
+ );
236743
+ continue;
236744
+ }
236587
236745
  const contents = await readFile3(current, "utf8");
236588
236746
  const hasStripePriceId = stripePriceIdPattern.test(contents);
236589
236747
  const named = looksLikePlanSource(relativePath);
@@ -236603,13 +236761,15 @@ async function discoverPlanSourceCandidates(input) {
236603
236761
  );
236604
236762
  const confirmed = sorted.filter((candidate) => candidate.hasStripePriceId);
236605
236763
  const namedOnly = sorted.filter((candidate) => !candidate.hasStripePriceId);
236606
- return [...confirmed, ...namedOnly.slice(0, maxNamedOnlyCandidates)];
236764
+ return [...confirmed, ...namedOnly];
236607
236765
  }
236608
236766
  async function readApprovedPlanSources(cwd2) {
236609
236767
  try {
236610
236768
  const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
236611
236769
  const parsed = JSON.parse(raw);
236612
- return Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [];
236770
+ return Array.isArray(parsed.files) ? parsed.files.filter(
236771
+ (value) => typeof value === "string"
236772
+ ) : [];
236613
236773
  } catch {
236614
236774
  return [];
236615
236775
  }
@@ -236646,7 +236806,9 @@ async function buildPlanMappingHandoff(input) {
236646
236806
  if (!allowed(relativePath, [])) {
236647
236807
  throw new Error(`${relativePath} is not a readable plan source path.`);
236648
236808
  }
236649
- const kind = planSourceKindByExtension.get(extname2(relativePath).toLowerCase());
236809
+ const kind = planSourceKindByExtension.get(
236810
+ extname2(relativePath).toLowerCase()
236811
+ );
236650
236812
  if (!kind) {
236651
236813
  throw new Error(`${relativePath} is not a supported plan source type.`);
236652
236814
  }
@@ -236666,7 +236828,7 @@ function planMappingRequestKey(input) {
236666
236828
  }
236667
236829
 
236668
236830
  // src/plan-branches.ts
236669
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
236831
+ import { readdir as readdir3, readFile as readFile4, lstat as lstat3 } from "node:fs/promises";
236670
236832
  import { extname as extname3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
236671
236833
  var maxPlanBranchFileCharacters = 2e6;
236672
236834
  var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -236796,7 +236958,9 @@ function clusterPlanBranches(branches) {
236796
236958
  return {
236797
236959
  branches: grouped,
236798
236960
  location: key2.slice(key2.indexOf("|") + 1),
236799
- planValues: [...new Set(grouped.map((branch) => branch.planValue))].sort(),
236961
+ planValues: [
236962
+ ...new Set(grouped.map((branch) => branch.planValue))
236963
+ ].sort(),
236800
236964
  polarity: decided,
236801
236965
  shape: grouped[0].shape
236802
236966
  };
@@ -236824,20 +236988,26 @@ async function discoverPlanBranches(input) {
236824
236988
  if (relativePath && !allowed2(relativePath, excludes)) continue;
236825
236989
  let entryStat;
236826
236990
  try {
236827
- entryStat = await stat3(current);
236991
+ entryStat = await lstat3(current);
236828
236992
  } catch {
236829
236993
  continue;
236830
236994
  }
236995
+ if (entryStat.isSymbolicLink()) continue;
236831
236996
  if (entryStat.isDirectory()) {
236832
236997
  const entries = await readdir3(current, { withFileTypes: true });
236833
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
236998
+ for (const entry of entries.sort(
236999
+ (a, b) => a.name.localeCompare(b.name)
237000
+ )) {
236834
237001
  queue.push(join3(current, entry.name));
236835
237002
  }
236836
237003
  continue;
236837
237004
  }
236838
237005
  if (!entryStat.isFile() || !relativePath) continue;
236839
237006
  if (!codeExtensions.has(extname3(current).toLowerCase())) continue;
236840
- if (entryStat.size > maxPlanBranchFileCharacters) continue;
237007
+ if (entryStat.size > maxPlanBranchFileCharacters)
237008
+ throw new Error(
237009
+ "Plan discovery found a source file above the 2 MB per-file limit. Split large source files before retrying."
237010
+ );
236841
237011
  const lines = (await readFile4(current, "utf8")).split(/\r?\n/);
236842
237012
  collectConstants(lines, constants);
236843
237013
  lines.forEach((line, lineIndex) => {
@@ -236887,7 +237057,13 @@ var contextSchema = external_exports.object({
236887
237057
  integrationId: external_exports.string().nullable(),
236888
237058
  environment: external_exports.enum(["test", "production"]),
236889
237059
  catalogReady: external_exports.boolean(),
236890
- planNames: external_exports.array(external_exports.string())
237060
+ planNames: external_exports.array(external_exports.string()),
237061
+ catalogPrices: external_exports.array(
237062
+ external_exports.object({
237063
+ id: external_exports.string().regex(/^price_[A-Za-z0-9]+$/),
237064
+ lookupKey: external_exports.string().nullable()
237065
+ })
237066
+ ).default([])
236891
237067
  });
236892
237068
  function safeRepositoryKey(remote) {
236893
237069
  const ssh = remote.match(/^[^@\s]+@([^:]+):(.+)$/);
@@ -236948,7 +237124,9 @@ async function runGuidedSetup(options) {
236948
237124
  await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
236949
237125
  await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
236950
237126
  } catch {
236951
- throw new Error("Repository setup needed: run this command inside your app\u2019s Git repository with an initial commit and an origin remote. No push or production deploy is required for discovery.");
237127
+ throw new Error(
237128
+ "Repository setup needed: run this command inside your app\u2019s Git repository with an initial commit and an origin remote. No push or production deploy is required for discovery."
237129
+ );
236952
237130
  }
236953
237131
  const head = (await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
236954
237132
  const diff = (await exec2(
@@ -236996,7 +237174,11 @@ async function runGuidedSetup(options) {
236996
237174
  )).trim()
236997
237175
  );
236998
237176
  if (!approved)
236999
- return { exitCode: options.prompt ? 0 : 2, stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n", stderr: "" };
237177
+ return {
237178
+ exitCode: options.prompt ? 0 : 2,
237179
+ stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n",
237180
+ stderr: ""
237181
+ };
237000
237182
  run = (await request("/api/developer-tools/setup/runs", {
237001
237183
  generation: context.generation,
237002
237184
  repositoryKey,
@@ -237040,6 +237222,10 @@ async function runGuidedSetup(options) {
237040
237222
  };
237041
237223
  const send = (evidence) => {
237042
237224
  const currentStage = stage;
237225
+ if (JSON.stringify({ runId: run.id, stage: currentStage, evidence }).length > 1e6)
237226
+ throw new Error(
237227
+ "Feature discovery evidence exceeds the upload size limit. Review the source scope before retrying."
237228
+ );
237043
237229
  heartbeat = heartbeat.then(async () => {
237044
237230
  const result3 = await request("/api/developer-tools/setup/evidence", {
237045
237231
  runId: run.id,
@@ -237049,7 +237235,8 @@ async function runGuidedSetup(options) {
237049
237235
  if (typeof result3.sequence === "number")
237050
237236
  run.sequence = result3.sequence;
237051
237237
  run.receipts[currentStage] = result3.receipt ?? { complete: true };
237052
- if (currentStage === "plans") run.planNames = result3.planNames ?? evidence.plans.flatMap((plan) => [plan.key, plan.name]);
237238
+ if (currentStage === "plans")
237239
+ run.planNames = result3.planNames ?? evidence.plans.flatMap((plan) => [plan.key, plan.name]);
237053
237240
  });
237054
237241
  return heartbeat;
237055
237242
  };
@@ -237063,7 +237250,9 @@ async function runGuidedSetup(options) {
237063
237250
  void update(progressState, progressFailure).catch(() => {
237064
237251
  });
237065
237252
  }, 3e4);
237066
- options.progress?.(run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026");
237253
+ options.progress?.(
237254
+ run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026"
237255
+ );
237067
237256
  if (!run.receipts.features) {
237068
237257
  await update("running");
237069
237258
  const scan = await runFeatureSetup({
@@ -237074,8 +237263,15 @@ async function runGuidedSetup(options) {
237074
237263
  manifestOnly: true,
237075
237264
  accept: ["all"]
237076
237265
  });
237077
- if (!scan.coverage.complete)
237078
- throw new Error("Feature discovery needs attention: no supported source was scanned or the scan limit was reached. No feature evidence was submitted. Ask your developer to review scanner coverage before retrying.");
237266
+ if (!scan.coverage.complete) {
237267
+ const reason = scan.coverage.byteLimitReached ? `${scan.coverage.oversizedFileCount} source file(s) exceed the 2 MB per-file limit. Split large source files before retrying.` : !scan.coverage.scannedFiles ? "No JavaScript or TypeScript app files were found in the source roots. Run from the application folder." : "More than 10,000 feature bindings were found. Review the source scope before retrying.";
237268
+ throw new Error(
237269
+ `Feature discovery needs attention: ${reason} Scanned ${scan.coverage.scannedFiles} of ${scan.coverage.matchedFiles} files. Nothing was submitted for this stage.`
237270
+ );
237271
+ }
237272
+ options.progress?.(
237273
+ `Scanned ${scan.scannedFileCount} files; found ${scan.manifest.features.length} feature proposals.`
237274
+ );
237079
237275
  if (!scan.ok)
237080
237276
  throw new Error(
237081
237277
  "Feature discovery needs attention. Run features setup to review the local findings."
@@ -237090,7 +237286,10 @@ async function runGuidedSetup(options) {
237090
237286
  );
237091
237287
  }
237092
237288
  stage = "plans";
237093
- if (!context.catalogReady) options.progress?.("Waiting for your Stripe catalog. Setup can stay open while it imports\u2026");
237289
+ if (!context.catalogReady)
237290
+ options.progress?.(
237291
+ "Waiting for your Stripe catalog. Setup can stay open while it imports\u2026"
237292
+ );
237094
237293
  const catalogDeadline = Date.now() + 5 * 6e4;
237095
237294
  const generation = context.generation;
237096
237295
  while (!context.catalogReady && Date.now() < catalogDeadline) {
@@ -237108,53 +237307,83 @@ async function runGuidedSetup(options) {
237108
237307
  stdout: "Stripe is still importing. Run npx --yes saasfunnels@latest setup --resume when the catalog is ready.\n",
237109
237308
  stderr: ""
237110
237309
  };
237111
- options.progress?.(run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026");
237310
+ options.progress?.(
237311
+ run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026"
237312
+ );
237112
237313
  if (!run.receipts.plans) {
237113
237314
  await update("running");
237114
237315
  const candidates = await discoverPlanSourceCandidates({
237115
237316
  cwd: options.cwd
237116
237317
  });
237318
+ let manual = null;
237117
237319
  try {
237118
- if (candidates.length > 12) throw new Error("Too many sources");
237119
- let manual = null;
237320
+ manual = await readFile5(
237321
+ join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237322
+ "utf8"
237323
+ );
237324
+ } catch (error51) {
237325
+ if (error51.code !== "ENOENT") throw error51;
237326
+ }
237327
+ const byKey = /* @__PURE__ */ new Map();
237328
+ const reviewedFiles = [];
237329
+ const issues = [];
237330
+ const sources = manual !== null ? [{ path: ".saasfunnels/setup-pricing.json", kind: "json" }] : candidates;
237331
+ for (const candidate of sources) {
237120
237332
  try {
237121
- manual = await readFile5(
237122
- join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237123
- "utf8"
237333
+ const plans = extractSetupPricing(
237334
+ manual ?? await readFile5(join4(options.cwd, candidate.path), "utf8"),
237335
+ candidate.kind,
237336
+ context.catalogPrices
237124
237337
  );
237338
+ for (const plan of plans) {
237339
+ if (byKey.has(plan.key) && JSON.stringify(byKey.get(plan.key)) !== JSON.stringify(plan))
237340
+ throw new PricingExtractionError(
237341
+ "needs_review",
237342
+ "Conflicting declarations for the same plan."
237343
+ );
237344
+ byKey.set(plan.key, plan);
237345
+ }
237346
+ reviewedFiles.push(candidate.path);
237125
237347
  } catch (error51) {
237126
- if (error51.code !== "ENOENT") throw error51;
237348
+ if (manual === null && error51 instanceof PricingExtractionError && error51.code === "no_declarations")
237349
+ continue;
237350
+ issues.push({
237351
+ file: candidate.path,
237352
+ reason: error51 instanceof PricingExtractionError ? error51.message : "Unsupported pricing syntax."
237353
+ });
237127
237354
  }
237128
- const discovered = manual !== null ? extractSetupPricing(manual, "json") : (await Promise.all(
237129
- candidates.map(
237130
- async (candidate) => extractSetupPricing(
237131
- await readFile5(join4(options.cwd, candidate.path), "utf8"),
237132
- candidate.kind
237133
- )
237134
- )
237135
- )).flat();
237136
- const byKey = /* @__PURE__ */ new Map();
237137
- for (const plan of discovered) {
237138
- if (byKey.has(plan.key) && JSON.stringify(byKey.get(plan.key)) !== JSON.stringify(plan))
237139
- throw new Error("Conflicting pricing sources");
237140
- byKey.set(plan.key, plan);
237141
- }
237142
- await send({
237143
- plans: [...byKey.values()],
237144
- reviewedFiles: manual !== null ? [".saasfunnels/setup-pricing.json"] : candidates.map((c) => c.path)
237355
+ }
237356
+ if (reviewedFiles.length > 12 || byKey.size > 200)
237357
+ issues.push({
237358
+ file: "pricing",
237359
+ reason: "Combine the pricing declarations into at most 12 files and 200 plans."
237145
237360
  });
237146
- } catch {
237361
+ if (issues.length) {
237362
+ await writeFile3(
237363
+ join4(options.cwd, ".saasfunnels/setup-review.json"),
237364
+ JSON.stringify({ stage: "plans", issues }, null, 2) + "\n",
237365
+ { mode: 384 }
237366
+ );
237147
237367
  await update("waiting", "extraction_unsupported");
237148
237368
  return {
237149
237369
  exitCode: 2,
237150
- stdout: "Pricing discovery needs manual review. Create .saasfunnels/setup-pricing.json with a plans array containing key, name, features (boolean or numeric access), and prices (Stripe price IDs). Review it, then run npx --yes saasfunnels@latest setup --resume. No source files were uploaded.\n",
237370
+ stdout: `Pricing needs review in ${issues.length} file(s). See .saasfunnels/setup-review.json for the exact files and reasons. Ask your coding agent to review those declarations, or prepare .saasfunnels/setup-pricing.json, then run npx --yes saasfunnels@latest setup --resume. No pricing evidence was submitted.
237371
+ `,
237151
237372
  stderr: ""
237152
237373
  };
237153
237374
  }
237375
+ options.progress?.(
237376
+ `Found ${byKey.size} plan proposals in ${reviewedFiles.length} pricing files.`
237377
+ );
237378
+ await send({ plans: [...byKey.values()], reviewedFiles });
237154
237379
  }
237155
237380
  stage = "branches";
237156
- const planValues = [.../* @__PURE__ */ new Set([...context.planNames, ...run.planNames ?? []])];
237157
- options.progress?.(run.receipts.branches ? "4/4 Plan checks already submitted." : "4/4 Checking plan conditions\u2026");
237381
+ const planValues = [
237382
+ .../* @__PURE__ */ new Set([...context.planNames, ...run.planNames ?? []])
237383
+ ];
237384
+ options.progress?.(
237385
+ run.receipts.branches ? "4/4 Plan checks already submitted." : "4/4 Checking plan conditions\u2026"
237386
+ );
237158
237387
  if (!run.receipts.branches) {
237159
237388
  await update("running");
237160
237389
  const branches = planValues.length ? await discoverPlanBranches({
@@ -237173,6 +237402,7 @@ async function runGuidedSetup(options) {
237173
237402
  symbol: branch.symbol
237174
237403
  }))
237175
237404
  }));
237405
+ options.progress?.(`Found ${clusters.length} groups of plan conditions.`);
237176
237406
  await send({
237177
237407
  clusters,
237178
237408
  environment: context.environment,
@@ -237192,7 +237422,7 @@ async function runGuidedSetup(options) {
237192
237422
  clearInterval(timer);
237193
237423
  await reportFailure?.().catch(() => {
237194
237424
  });
237195
- const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Repository setup)/.test(
237425
+ const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Plan discovery|Repository setup)/.test(
237196
237426
  error51.message
237197
237427
  ) ? error51.message : "Setup could not finish. Check local repository access and developer credentials, then resume.";
237198
237428
  return { exitCode: 2, stdout: "", stderr: safe + "\n" };
@@ -237247,7 +237477,7 @@ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from
237247
237477
 
237248
237478
  // src/identity.ts
237249
237479
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
237250
- var SAASFUNNELS_CLI_VERSION = "0.3.1";
237480
+ var SAASFUNNELS_CLI_VERSION = "0.3.2";
237251
237481
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
237252
237482
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
237253
237483
  var SAASFUNNELS_ENV = Object.freeze({
@@ -125,6 +125,8 @@ export type FeatureSetupResult = {
125
125
  complete: boolean;
126
126
  matchedFiles: number;
127
127
  scannedFiles: number;
128
+ scannedBytes: number;
129
+ oversizedFileCount: number;
128
130
  byteLimitReached: boolean;
129
131
  candidateLimitReached: boolean;
130
132
  };
@@ -1,5 +1,5 @@
1
1
  export declare const SAASFUNNELS_CLI_NAME = "saasfunnels";
2
- export declare const SAASFUNNELS_CLI_VERSION = "0.3.1";
2
+ export declare const SAASFUNNELS_CLI_VERSION = "0.3.2";
3
3
  export declare const SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
4
4
  export declare const SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
5
5
  export declare const SAASFUNNELS_ENV: Readonly<{
@@ -77,7 +77,7 @@ export type SaaSFunnelsMcpOptions = {
77
77
  output?: Pick<NodeJS.WritableStream, "write">;
78
78
  };
79
79
  declare const mcpProtocolVersion = "2025-06-18";
80
- declare const mcpServerVersion = "0.3.1";
80
+ declare const mcpServerVersion = "0.3.2";
81
81
  export declare function saasFunnelsMcpResources(): McpResourceDefinition[];
82
82
  export declare function readSaaSFunnelsMcpResource(uri: string, options?: SaaSFunnelsMcpOptions): string;
83
83
  export declare function saasFunnelsMcpToolDefinitions({ allowFunnelTests, allowFunnelWrites, allowTestWrites, includeHostedAccountTools, }?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasfunnels",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "SaaSFunnels CLI and local stdio MCP server",
6
6
  "author": "SaaSFunnels",