saasfunnels 0.3.1 → 0.3.3

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,12 @@ 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.
110
+
111
+ Discovery follows Git ignore rules (including custom build folders) while keeping tracked application sources and new, non-ignored source files. Standalone folders without Git use the standard source filters.
106
112
 
107
113
  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
@@ -24187,6 +24187,11 @@ function agentCapabilityAllowedAtBoundary(id, boundary, { mutationsEnabled = fal
24187
24187
  return true;
24188
24188
  }
24189
24189
 
24190
+ // src/source-scope.ts
24191
+ import { execFile } from "node:child_process";
24192
+ import { promisify } from "node:util";
24193
+ var exec = promisify(execFile);
24194
+
24190
24195
  // src/feature-setup.ts
24191
24196
  var import_yaml = __toESM(require_dist(), 1);
24192
24197
  var schemaVersion = 1;
@@ -24264,7 +24269,7 @@ function validateFeatureCatalogManifest(value) {
24264
24269
 
24265
24270
  // src/identity.ts
24266
24271
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
24267
- var SAASFUNNELS_CLI_VERSION = "0.3.1";
24272
+ var SAASFUNNELS_CLI_VERSION = "0.3.3";
24268
24273
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
24269
24274
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
24270
24275
  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,19 +219138,30 @@ 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
 
219064
219161
  // src/setup-run.ts
219065
219162
  import { createHash as createHash3 } from "node:crypto";
219066
- import { execFile as execFile2 } from "node:child_process";
219067
- import { promisify as promisify2 } from "node:util";
219163
+ import { execFile as execFile3 } from "node:child_process";
219164
+ import { promisify as promisify3 } from "node:util";
219068
219165
  import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
219069
219166
  import { join as join4 } from "node:path";
219070
219167
 
@@ -233582,6 +233679,56 @@ function date4(params) {
233582
233679
  // node_modules/zod/v4/classic/external.js
233583
233680
  config(en_default());
233584
233681
 
233682
+ // src/source-scope.ts
233683
+ import { execFile as execFile2 } from "node:child_process";
233684
+ import { promisify as promisify2 } from "node:util";
233685
+ var exec2 = promisify2(execFile2);
233686
+ async function repositorySourceScope(cwd2) {
233687
+ try {
233688
+ await exec2("git", ["rev-parse", "--is-inside-work-tree"], { cwd: cwd2 });
233689
+ } catch (error51) {
233690
+ const failure = error51;
233691
+ if (failure.code === "ENOENT" || failure.stderr?.includes("not a git repository"))
233692
+ return (_path) => true;
233693
+ throw new Error(
233694
+ "Cannot read repository source scope. Check Git access and retry."
233695
+ );
233696
+ }
233697
+ let stdout;
233698
+ try {
233699
+ ({ stdout } = await exec2(
233700
+ "git",
233701
+ [
233702
+ "ls-files",
233703
+ "--cached",
233704
+ "--others",
233705
+ "--exclude-standard",
233706
+ "-z",
233707
+ "--",
233708
+ "."
233709
+ ],
233710
+ {
233711
+ cwd: cwd2,
233712
+ maxBuffer: 32 * 1024 * 1024
233713
+ }
233714
+ ));
233715
+ } catch {
233716
+ throw new Error(
233717
+ "Cannot list repository sources. Check Git access and retry."
233718
+ );
233719
+ }
233720
+ const included = /* @__PURE__ */ new Set([""]);
233721
+ for (const path of stdout.split("\0").filter(Boolean)) {
233722
+ included.add(path);
233723
+ let end = path.lastIndexOf("/");
233724
+ while (end > 0) {
233725
+ included.add(path.slice(0, end));
233726
+ end = path.lastIndexOf("/", end - 1);
233727
+ }
233728
+ }
233729
+ return (path) => included.has(path);
233730
+ }
233731
+
233585
233732
  // src/feature-setup.ts
233586
233733
  var import_yaml2 = __toESM(require_dist(), 1);
233587
233734
  import { createHash as createHash2, randomUUID } from "node:crypto";
@@ -233592,7 +233739,7 @@ import {
233592
233739
  readdir,
233593
233740
  rename,
233594
233741
  rm,
233595
- stat,
233742
+ lstat,
233596
233743
  writeFile
233597
233744
  } from "node:fs/promises";
233598
233745
  import { dirname, extname, join, relative, resolve as resolve2 } from "node:path";
@@ -235742,7 +235889,17 @@ var featureBindingKinds = [
235742
235889
  "funnel_trigger",
235743
235890
  "discovery"
235744
235891
  ];
235745
- var defaultRoots = ["app", "src", "lib", "components", "pages"];
235892
+ var defaultRoots = [
235893
+ "app",
235894
+ "src",
235895
+ "lib",
235896
+ "components",
235897
+ "pages",
235898
+ "apps",
235899
+ "packages",
235900
+ "server",
235901
+ "client"
235902
+ ];
235746
235903
  var ignoredSegments = /* @__PURE__ */ new Set([
235747
235904
  ".claude",
235748
235905
  ".git",
@@ -235751,6 +235908,13 @@ var ignoredSegments = /* @__PURE__ */ new Set([
235751
235908
  ".turbo",
235752
235909
  "build",
235753
235910
  "coverage",
235911
+ ".saasfunnels",
235912
+ "docs",
235913
+ "e2e",
235914
+ "fixtures",
235915
+ "__tests__",
235916
+ "tests",
235917
+ "__mocks__",
235754
235918
  "dist",
235755
235919
  "generated",
235756
235920
  "node_modules",
@@ -235849,21 +236013,31 @@ function allowedPath(path, excludes) {
235849
236013
  const normalized = path.replaceAll("\\", "/");
235850
236014
  if (sensitivePathPattern.test(normalized)) return false;
235851
236015
  const segments = normalized.split("/");
235852
- if (segments.some((segment) => ignoredSegments.has(segment))) return false;
236016
+ if (segments.some(
236017
+ (segment) => ignoredSegments.has(segment) || /^\.next(?:[-.]|$)/.test(segment)
236018
+ ))
236019
+ return false;
236020
+ if (/\.(test|spec|stories|d)\.[cm]?[jt]sx?$/.test(normalized)) return false;
235853
236021
  return !excludes.some(
235854
236022
  (exclude) => normalized === exclude || normalized.startsWith(`${exclude}/`)
235855
236023
  );
235856
236024
  }
235857
236025
  async function walkFiles(input) {
236026
+ const inRepository = await repositorySourceScope(input.cwd);
235858
236027
  const files = [];
235859
236028
  const queue = input.roots.map((root) => resolve2(input.cwd, root));
235860
- while (queue.length && files.length < 500) {
236029
+ const seen = /* @__PURE__ */ new Set();
236030
+ let oversizedFileCount = 0;
236031
+ while (queue.length) {
235861
236032
  const current = queue.shift();
236033
+ if (seen.has(current)) continue;
236034
+ seen.add(current);
235862
236035
  const relativePath = safeRelative(input.cwd, current);
235863
- if (!relativePath || !allowedPath(relativePath, input.excludes)) continue;
236036
+ if (!relativePath || !inRepository(relativePath) || !allowedPath(relativePath, input.excludes))
236037
+ continue;
235864
236038
  let currentStat;
235865
236039
  try {
235866
- currentStat = await stat(current);
236040
+ currentStat = await lstat(current);
235867
236041
  } catch {
235868
236042
  continue;
235869
236043
  }
@@ -235872,11 +236046,15 @@ async function walkFiles(input) {
235872
236046
  entries.sort((left, right) => left.name.localeCompare(right.name)).forEach((entry) => queue.push(join(current, entry.name)));
235873
236047
  continue;
235874
236048
  }
235875
- if (currentStat.isFile() && currentStat.size <= 256e3 && supportedExtensions.has(extname(current))) {
236049
+ if (currentStat.isFile() && supportedExtensions.has(extname(current))) {
236050
+ if (currentStat.size > 2e6) {
236051
+ oversizedFileCount++;
236052
+ continue;
236053
+ }
235876
236054
  files.push(current);
235877
236055
  }
235878
236056
  }
235879
- return files.sort();
236057
+ return { files: files.sort(), oversizedFileCount };
235880
236058
  }
235881
236059
  function nearestSymbol(lines, lineIndex) {
235882
236060
  for (let index = lineIndex; index >= Math.max(0, lineIndex - 20); index -= 1) {
@@ -235987,7 +236165,9 @@ async function detectFramework(cwd2, requestedRoots) {
235987
236165
  };
235988
236166
  const framework = typeof dependencies.next === "string" ? "nextjs" : "unsupported";
235989
236167
  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("/"));
236168
+ const roots = (requestedRoots?.length ? requestedRoots : defaultRoots).filter(
236169
+ (root) => !root.startsWith("../") && !root.startsWith("/")
236170
+ );
235991
236171
  const existingRoots = [];
235992
236172
  for (const root of roots) {
235993
236173
  if (await exists(resolve2(cwd2, root))) existingRoots.push(root);
@@ -235995,7 +236175,9 @@ async function detectFramework(cwd2, requestedRoots) {
235995
236175
  return {
235996
236176
  framework,
235997
236177
  packageManager,
235998
- saasFunnelsPackages: Object.keys(dependencies).filter((name) => name === "saasfunnels" || name.startsWith("@saasfunnels/")).sort(),
236178
+ saasFunnelsPackages: Object.keys(dependencies).filter(
236179
+ (name) => name === "saasfunnels" || name.startsWith("@saasfunnels/")
236180
+ ).sort(),
235999
236181
  roots: existingRoots
236000
236182
  };
236001
236183
  }
@@ -236102,11 +236284,16 @@ async function applyFeatureSetupChangesAtomically(cwd2, changes, io = {
236102
236284
  const absolute = resolve2(cwd2, change.path);
236103
236285
  const relativePath = safeRelative(cwd2, absolute);
236104
236286
  if (!relativePath)
236105
- throw new Error(`Refusing to write outside the repository: ${change.path}`);
236287
+ throw new Error(
236288
+ `Refusing to write outside the repository: ${change.path}`
236289
+ );
236106
236290
  await io.mkdir(dirname(absolute), { recursive: true });
236107
236291
  originals.set(absolute, await io.read(absolute));
236108
236292
  const stagedPath = `${absolute}.saasfunnels-${transactionId}.tmp`;
236109
- await io.write(stagedPath, change.contents, { encoding: "utf8", mode: 384 });
236293
+ await io.write(stagedPath, change.contents, {
236294
+ encoding: "utf8",
236295
+ mode: 384
236296
+ });
236110
236297
  staged.push({ absolute, staged: stagedPath });
236111
236298
  }
236112
236299
  for (const entry of staged) await io.rename(entry.staged, entry.absolute);
@@ -236220,10 +236407,12 @@ async function runFeatureSetup(input) {
236220
236407
  `Inspect these local roots only: ${framework.roots.join(", ") || "none"}? [y/N] `
236221
236408
  );
236222
236409
  if (!/^y(?:es)?$/i.test(confirmation.trim())) {
236223
- throw new Error("Feature discovery was cancelled before reading source files.");
236410
+ throw new Error(
236411
+ "Feature discovery was cancelled before reading source files."
236412
+ );
236224
236413
  }
236225
236414
  }
236226
- const files = await walkFiles({
236415
+ const { files, oversizedFileCount } = await walkFiles({
236227
236416
  cwd: input.cwd,
236228
236417
  excludes: input.exclude ?? [],
236229
236418
  roots: framework.roots
@@ -236231,14 +236420,10 @@ async function runFeatureSetup(input) {
236231
236420
  const discovered = [];
236232
236421
  let scannedBytes = 0;
236233
236422
  let scannedFileCount = 0;
236234
- let byteLimitReached = false;
236423
+ const byteLimitReached = oversizedFileCount > 0;
236235
236424
  for (const file2 of files) {
236236
236425
  const source = await readFile2(file2, "utf8");
236237
236426
  scannedBytes += Buffer.byteLength(source);
236238
- if (scannedBytes > 2e6) {
236239
- byteLimitReached = true;
236240
- break;
236241
- }
236242
236427
  const path = safeRelative(input.cwd, file2);
236243
236428
  if (!path) continue;
236244
236429
  scannedFileCount++;
@@ -236249,9 +236434,14 @@ async function runFeatureSetup(input) {
236249
236434
  (left, right) => right.confidenceBasisPoints - left.confidenceBasisPoints || left.file.localeCompare(right.file) || left.line - right.line
236250
236435
  )) {
236251
236436
  const identity = `${candidate.suggestedKey}:${candidate.accessModel}:${candidate.file}:${candidate.line}`;
236252
- if (!uniqueCandidates.has(identity)) uniqueCandidates.set(identity, candidate);
236437
+ if (!uniqueCandidates.has(identity))
236438
+ uniqueCandidates.set(identity, candidate);
236253
236439
  }
236254
- const candidates = [...uniqueCandidates.values()].slice(0, 200);
236440
+ const candidates = [...uniqueCandidates.values()];
236441
+ if (new Set(candidates.map((candidate) => candidate.suggestedKey)).size > 5e3)
236442
+ throw new Error(
236443
+ "Feature discovery found more than 5,000 distinct features. Narrow the source roots before retrying."
236444
+ );
236255
236445
  const manifestPath = ".saasfunnels/catalog.yaml";
236256
236446
  const existingManifestSource = await readable(
236257
236447
  resolve2(input.cwd, manifestPath)
@@ -236271,7 +236461,8 @@ async function runFeatureSetup(input) {
236271
236461
  const accept = new Set(input.accept ?? []);
236272
236462
  const reject = new Set(input.reject ?? []);
236273
236463
  for (const candidate of candidates) {
236274
- if (reject.has(candidate.id) || reject.has(candidate.suggestedKey)) continue;
236464
+ if (reject.has(candidate.id) || reject.has(candidate.suggestedKey))
236465
+ continue;
236275
236466
  if (accept.has("all") || accept.has(candidate.id) || accept.has(candidate.suggestedKey)) {
236276
236467
  accepted.add(candidate.id);
236277
236468
  continue;
@@ -236285,7 +236476,10 @@ async function runFeatureSetup(input) {
236285
236476
  if (/^a(?:ccept)?$/i.test(answer.trim())) accepted.add(candidate.id);
236286
236477
  else if (keyPattern.test(answer.trim())) {
236287
236478
  accepted.add(candidate.id);
236288
- input.mappings = { ...input.mappings ?? {}, [candidate.id]: answer.trim() };
236479
+ input.mappings = {
236480
+ ...input.mappings ?? {},
236481
+ [candidate.id]: answer.trim()
236482
+ };
236289
236483
  }
236290
236484
  }
236291
236485
  }
@@ -236420,7 +236614,15 @@ async function runFeatureSetup(input) {
236420
236614
  reviewUrl,
236421
236615
  runtimeCheck: check2,
236422
236616
  scannedFileCount,
236423
- coverage: { complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 200, matchedFiles: files.length, scannedFiles: scannedFileCount, byteLimitReached, candidateLimitReached: uniqueCandidates.size > 200 },
236617
+ coverage: {
236618
+ complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 1e4,
236619
+ matchedFiles: files.length + oversizedFileCount,
236620
+ scannedFiles: scannedFileCount,
236621
+ scannedBytes,
236622
+ oversizedFileCount,
236623
+ byteLimitReached,
236624
+ candidateLimitReached: uniqueCandidates.size > 1e4
236625
+ },
236424
236626
  validation: { errors: validation.errors, ok: validation.ok }
236425
236627
  };
236426
236628
  }
@@ -236502,7 +236704,7 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
236502
236704
  }
236503
236705
 
236504
236706
  // 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";
236707
+ import { readFile as readFile3, readdir as readdir2, lstat as lstat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236506
236708
  import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
236507
236709
  var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
236508
236710
  var maxPlanSourceFiles = 12;
@@ -236533,10 +236735,9 @@ var planSourceKindByExtension = /* @__PURE__ */ new Map([
236533
236735
  [".yml", "yaml"]
236534
236736
  ]);
236535
236737
  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;
236738
+ var chargeAmountPattern = /\b(unit_amount|unit_amount_decimal|unitAmount|amount|amount_decimal|price|prices|currency|usd|eur|gbp|cents)\b/i;
236537
236739
  var stripePriceIdPattern = /["'`]price_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{14,}["'`]/;
236538
236740
  var nonDefinitionPattern = /(^|\/)(__tests__|__mocks__|tests?|specs?)(\/)|\.(test|spec|d)\.[cm]?[jt]sx?$/i;
236539
- var maxNamedOnlyCandidates = 10;
236540
236741
  function normalize(path) {
236541
236742
  return path.split(sep).join("/");
236542
236743
  }
@@ -236560,22 +236761,27 @@ async function discoverPlanSourceCandidates(input) {
236560
236761
  const roots = input.roots ?? [".", "config", ...defaultRoots];
236561
236762
  const seen = /* @__PURE__ */ new Set();
236562
236763
  const candidates = [];
236764
+ const inRepository = await repositorySourceScope(input.cwd);
236563
236765
  const queue = roots.map((root) => resolve3(input.cwd, root));
236564
- while (queue.length && candidates.length < 200) {
236766
+ while (queue.length) {
236565
236767
  const current = queue.shift();
236566
236768
  if (seen.has(current)) continue;
236567
236769
  seen.add(current);
236568
236770
  const relativePath = normalize(relative2(input.cwd, current));
236569
- if (relativePath && !allowed(relativePath, excludes)) continue;
236771
+ if (!inRepository(relativePath) || relativePath && !allowed(relativePath, excludes))
236772
+ continue;
236570
236773
  let entryStat;
236571
236774
  try {
236572
- entryStat = await stat2(current);
236775
+ entryStat = await lstat2(current);
236573
236776
  } catch {
236574
236777
  continue;
236575
236778
  }
236779
+ if (entryStat.isSymbolicLink()) continue;
236576
236780
  if (entryStat.isDirectory()) {
236577
236781
  const entries = await readdir2(current, { withFileTypes: true });
236578
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
236782
+ for (const entry of entries.sort(
236783
+ (a, b) => a.name.localeCompare(b.name)
236784
+ )) {
236579
236785
  queue.push(join2(current, entry.name));
236580
236786
  }
236581
236787
  continue;
@@ -236583,7 +236789,13 @@ async function discoverPlanSourceCandidates(input) {
236583
236789
  if (!entryStat.isFile()) continue;
236584
236790
  const kind = planSourceKindByExtension.get(extname2(current).toLowerCase());
236585
236791
  if (!kind || !relativePath) continue;
236586
- if (entryStat.size > maxPlanSourceCharacters) continue;
236792
+ if (entryStat.size > 2e6) {
236793
+ if (looksLikePlanSource(relativePath))
236794
+ throw new Error(
236795
+ "Plan discovery found a pricing file above the 2 MB per-file limit. Split large pricing files before retrying."
236796
+ );
236797
+ continue;
236798
+ }
236587
236799
  const contents = await readFile3(current, "utf8");
236588
236800
  const hasStripePriceId = stripePriceIdPattern.test(contents);
236589
236801
  const named = looksLikePlanSource(relativePath);
@@ -236603,13 +236815,15 @@ async function discoverPlanSourceCandidates(input) {
236603
236815
  );
236604
236816
  const confirmed = sorted.filter((candidate) => candidate.hasStripePriceId);
236605
236817
  const namedOnly = sorted.filter((candidate) => !candidate.hasStripePriceId);
236606
- return [...confirmed, ...namedOnly.slice(0, maxNamedOnlyCandidates)];
236818
+ return [...confirmed, ...namedOnly];
236607
236819
  }
236608
236820
  async function readApprovedPlanSources(cwd2) {
236609
236821
  try {
236610
236822
  const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
236611
236823
  const parsed = JSON.parse(raw);
236612
- return Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [];
236824
+ return Array.isArray(parsed.files) ? parsed.files.filter(
236825
+ (value) => typeof value === "string"
236826
+ ) : [];
236613
236827
  } catch {
236614
236828
  return [];
236615
236829
  }
@@ -236646,7 +236860,9 @@ async function buildPlanMappingHandoff(input) {
236646
236860
  if (!allowed(relativePath, [])) {
236647
236861
  throw new Error(`${relativePath} is not a readable plan source path.`);
236648
236862
  }
236649
- const kind = planSourceKindByExtension.get(extname2(relativePath).toLowerCase());
236863
+ const kind = planSourceKindByExtension.get(
236864
+ extname2(relativePath).toLowerCase()
236865
+ );
236650
236866
  if (!kind) {
236651
236867
  throw new Error(`${relativePath} is not a supported plan source type.`);
236652
236868
  }
@@ -236666,7 +236882,7 @@ function planMappingRequestKey(input) {
236666
236882
  }
236667
236883
 
236668
236884
  // src/plan-branches.ts
236669
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
236885
+ import { readdir as readdir3, readFile as readFile4, lstat as lstat3 } from "node:fs/promises";
236670
236886
  import { extname as extname3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
236671
236887
  var maxPlanBranchFileCharacters = 2e6;
236672
236888
  var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -236796,7 +237012,9 @@ function clusterPlanBranches(branches) {
236796
237012
  return {
236797
237013
  branches: grouped,
236798
237014
  location: key2.slice(key2.indexOf("|") + 1),
236799
- planValues: [...new Set(grouped.map((branch) => branch.planValue))].sort(),
237015
+ planValues: [
237016
+ ...new Set(grouped.map((branch) => branch.planValue))
237017
+ ].sort(),
236800
237018
  polarity: decided,
236801
237019
  shape: grouped[0].shape
236802
237020
  };
@@ -236815,29 +237033,37 @@ async function discoverPlanBranches(input) {
236815
237033
  const branches = [];
236816
237034
  const constants = /* @__PURE__ */ new Map();
236817
237035
  const pending = [];
237036
+ const inRepository = await repositorySourceScope(input.cwd);
236818
237037
  const queue = roots.map((root) => resolve4(input.cwd, root));
236819
237038
  while (queue.length) {
236820
237039
  const current = queue.shift();
236821
237040
  if (seen.has(current)) continue;
236822
237041
  seen.add(current);
236823
237042
  const relativePath = normalize2(relative3(input.cwd, current));
236824
- if (relativePath && !allowed2(relativePath, excludes)) continue;
237043
+ if (!inRepository(relativePath) || relativePath && !allowed2(relativePath, excludes))
237044
+ continue;
236825
237045
  let entryStat;
236826
237046
  try {
236827
- entryStat = await stat3(current);
237047
+ entryStat = await lstat3(current);
236828
237048
  } catch {
236829
237049
  continue;
236830
237050
  }
237051
+ if (entryStat.isSymbolicLink()) continue;
236831
237052
  if (entryStat.isDirectory()) {
236832
237053
  const entries = await readdir3(current, { withFileTypes: true });
236833
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
237054
+ for (const entry of entries.sort(
237055
+ (a, b) => a.name.localeCompare(b.name)
237056
+ )) {
236834
237057
  queue.push(join3(current, entry.name));
236835
237058
  }
236836
237059
  continue;
236837
237060
  }
236838
237061
  if (!entryStat.isFile() || !relativePath) continue;
236839
237062
  if (!codeExtensions.has(extname3(current).toLowerCase())) continue;
236840
- if (entryStat.size > maxPlanBranchFileCharacters) continue;
237063
+ if (entryStat.size > maxPlanBranchFileCharacters)
237064
+ throw new Error(
237065
+ "Plan discovery found a source file above the 2 MB per-file limit. Split large source files before retrying."
237066
+ );
236841
237067
  const lines = (await readFile4(current, "utf8")).split(/\r?\n/);
236842
237068
  collectConstants(lines, constants);
236843
237069
  lines.forEach((line, lineIndex) => {
@@ -236878,7 +237104,7 @@ async function discoverPlanBranches(input) {
236878
237104
  }
236879
237105
 
236880
237106
  // src/setup-run.ts
236881
- var exec2 = promisify2(execFile2);
237107
+ var exec3 = promisify3(execFile3);
236882
237108
  var hash2 = (text) => createHash3("sha256").update(text).digest("hex");
236883
237109
  var contextSchema = external_exports.object({
236884
237110
  workspaceId: external_exports.string(),
@@ -236887,7 +237113,13 @@ var contextSchema = external_exports.object({
236887
237113
  integrationId: external_exports.string().nullable(),
236888
237114
  environment: external_exports.enum(["test", "production"]),
236889
237115
  catalogReady: external_exports.boolean(),
236890
- planNames: external_exports.array(external_exports.string())
237116
+ planNames: external_exports.array(external_exports.string()),
237117
+ catalogPrices: external_exports.array(
237118
+ external_exports.object({
237119
+ id: external_exports.string().regex(/^price_[A-Za-z0-9]+$/),
237120
+ lookupKey: external_exports.string().nullable()
237121
+ })
237122
+ ).default([])
236891
237123
  });
236892
237124
  function safeRepositoryKey(remote) {
236893
237125
  const ssh = remote.match(/^[^@\s]+@([^:]+):(.+)$/);
@@ -236945,13 +237177,15 @@ async function runGuidedSetup(options) {
236945
237177
  "Connect Stripe and create app credentials in Setup first."
236946
237178
  );
236947
237179
  try {
236948
- await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
236949
- await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
237180
+ await exec3("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
237181
+ await exec3("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
236950
237182
  } 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.");
237183
+ throw new Error(
237184
+ "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."
237185
+ );
236952
237186
  }
236953
- const head = (await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
236954
- const diff = (await exec2(
237187
+ const head = (await exec3("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
237188
+ const diff = (await exec3(
236955
237189
  "git",
236956
237190
  [
236957
237191
  "diff",
@@ -236964,7 +237198,7 @@ async function runGuidedSetup(options) {
236964
237198
  ],
236965
237199
  { cwd: options.cwd, maxBuffer: 8e6 }
236966
237200
  )).stdout;
236967
- const untracked = (await exec2("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
237201
+ const untracked = (await exec3("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
236968
237202
  cwd: options.cwd
236969
237203
  })).stdout.split("\0").filter(
236970
237204
  (path) => path && !path.startsWith(".saasfunnels/") && !/(^|\/)(\.env|node_modules|dist|\.next)(\/|$|\.)/i.test(path)
@@ -236980,7 +237214,7 @@ async function runGuidedSetup(options) {
236980
237214
  ])
236981
237215
  );
236982
237216
  const revision = `${head}:${hash2(JSON.stringify([diff, sourceHashes]))}`;
236983
- const remote = (await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
237217
+ const remote = (await exec3("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
236984
237218
  const repositoryKey = safeRepositoryKey(remote);
236985
237219
  const statePath = join4(options.cwd, ".saasfunnels", "setup-run.json");
236986
237220
  if (options.resume) {
@@ -236996,7 +237230,11 @@ async function runGuidedSetup(options) {
236996
237230
  )).trim()
236997
237231
  );
236998
237232
  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: "" };
237233
+ return {
237234
+ exitCode: options.prompt ? 0 : 2,
237235
+ stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n",
237236
+ stderr: ""
237237
+ };
237000
237238
  run = (await request("/api/developer-tools/setup/runs", {
237001
237239
  generation: context.generation,
237002
237240
  repositoryKey,
@@ -237040,6 +237278,10 @@ async function runGuidedSetup(options) {
237040
237278
  };
237041
237279
  const send = (evidence) => {
237042
237280
  const currentStage = stage;
237281
+ if (JSON.stringify({ runId: run.id, stage: currentStage, evidence }).length > 1e6)
237282
+ throw new Error(
237283
+ "Feature discovery evidence exceeds the upload size limit. Review the source scope before retrying."
237284
+ );
237043
237285
  heartbeat = heartbeat.then(async () => {
237044
237286
  const result3 = await request("/api/developer-tools/setup/evidence", {
237045
237287
  runId: run.id,
@@ -237049,7 +237291,8 @@ async function runGuidedSetup(options) {
237049
237291
  if (typeof result3.sequence === "number")
237050
237292
  run.sequence = result3.sequence;
237051
237293
  run.receipts[currentStage] = result3.receipt ?? { complete: true };
237052
- if (currentStage === "plans") run.planNames = result3.planNames ?? evidence.plans.flatMap((plan) => [plan.key, plan.name]);
237294
+ if (currentStage === "plans")
237295
+ run.planNames = result3.planNames ?? evidence.plans.flatMap((plan) => [plan.key, plan.name]);
237053
237296
  });
237054
237297
  return heartbeat;
237055
237298
  };
@@ -237063,7 +237306,9 @@ async function runGuidedSetup(options) {
237063
237306
  void update(progressState, progressFailure).catch(() => {
237064
237307
  });
237065
237308
  }, 3e4);
237066
- options.progress?.(run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026");
237309
+ options.progress?.(
237310
+ run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026"
237311
+ );
237067
237312
  if (!run.receipts.features) {
237068
237313
  await update("running");
237069
237314
  const scan = await runFeatureSetup({
@@ -237074,8 +237319,15 @@ async function runGuidedSetup(options) {
237074
237319
  manifestOnly: true,
237075
237320
  accept: ["all"]
237076
237321
  });
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.");
237322
+ if (!scan.coverage.complete) {
237323
+ 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.";
237324
+ throw new Error(
237325
+ `Feature discovery needs attention: ${reason} Scanned ${scan.coverage.scannedFiles} of ${scan.coverage.matchedFiles} files. Nothing was submitted for this stage.`
237326
+ );
237327
+ }
237328
+ options.progress?.(
237329
+ `Scanned ${scan.scannedFileCount} files; found ${scan.manifest.features.length} feature proposals.`
237330
+ );
237079
237331
  if (!scan.ok)
237080
237332
  throw new Error(
237081
237333
  "Feature discovery needs attention. Run features setup to review the local findings."
@@ -237090,7 +237342,10 @@ async function runGuidedSetup(options) {
237090
237342
  );
237091
237343
  }
237092
237344
  stage = "plans";
237093
- if (!context.catalogReady) options.progress?.("Waiting for your Stripe catalog. Setup can stay open while it imports\u2026");
237345
+ if (!context.catalogReady)
237346
+ options.progress?.(
237347
+ "Waiting for your Stripe catalog. Setup can stay open while it imports\u2026"
237348
+ );
237094
237349
  const catalogDeadline = Date.now() + 5 * 6e4;
237095
237350
  const generation = context.generation;
237096
237351
  while (!context.catalogReady && Date.now() < catalogDeadline) {
@@ -237108,53 +237363,83 @@ async function runGuidedSetup(options) {
237108
237363
  stdout: "Stripe is still importing. Run npx --yes saasfunnels@latest setup --resume when the catalog is ready.\n",
237109
237364
  stderr: ""
237110
237365
  };
237111
- options.progress?.(run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026");
237366
+ options.progress?.(
237367
+ run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026"
237368
+ );
237112
237369
  if (!run.receipts.plans) {
237113
237370
  await update("running");
237114
237371
  const candidates = await discoverPlanSourceCandidates({
237115
237372
  cwd: options.cwd
237116
237373
  });
237374
+ let manual = null;
237117
237375
  try {
237118
- if (candidates.length > 12) throw new Error("Too many sources");
237119
- let manual = null;
237376
+ manual = await readFile5(
237377
+ join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237378
+ "utf8"
237379
+ );
237380
+ } catch (error51) {
237381
+ if (error51.code !== "ENOENT") throw error51;
237382
+ }
237383
+ const byKey = /* @__PURE__ */ new Map();
237384
+ const reviewedFiles = [];
237385
+ const issues = [];
237386
+ const sources = manual !== null ? [{ path: ".saasfunnels/setup-pricing.json", kind: "json" }] : candidates;
237387
+ for (const candidate of sources) {
237120
237388
  try {
237121
- manual = await readFile5(
237122
- join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237123
- "utf8"
237389
+ const plans = extractSetupPricing(
237390
+ manual ?? await readFile5(join4(options.cwd, candidate.path), "utf8"),
237391
+ candidate.kind,
237392
+ context.catalogPrices
237124
237393
  );
237394
+ for (const plan of plans) {
237395
+ if (byKey.has(plan.key) && JSON.stringify(byKey.get(plan.key)) !== JSON.stringify(plan))
237396
+ throw new PricingExtractionError(
237397
+ "needs_review",
237398
+ "Conflicting declarations for the same plan."
237399
+ );
237400
+ byKey.set(plan.key, plan);
237401
+ }
237402
+ reviewedFiles.push(candidate.path);
237125
237403
  } catch (error51) {
237126
- if (error51.code !== "ENOENT") throw error51;
237404
+ if (manual === null && error51 instanceof PricingExtractionError && error51.code === "no_declarations")
237405
+ continue;
237406
+ issues.push({
237407
+ file: candidate.path,
237408
+ reason: error51 instanceof PricingExtractionError ? error51.message : "Unsupported pricing syntax."
237409
+ });
237127
237410
  }
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)
237411
+ }
237412
+ if (reviewedFiles.length > 12 || byKey.size > 200)
237413
+ issues.push({
237414
+ file: "pricing",
237415
+ reason: "Combine the pricing declarations into at most 12 files and 200 plans."
237145
237416
  });
237146
- } catch {
237417
+ if (issues.length) {
237418
+ await writeFile3(
237419
+ join4(options.cwd, ".saasfunnels/setup-review.json"),
237420
+ JSON.stringify({ stage: "plans", issues }, null, 2) + "\n",
237421
+ { mode: 384 }
237422
+ );
237147
237423
  await update("waiting", "extraction_unsupported");
237148
237424
  return {
237149
237425
  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",
237426
+ 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.
237427
+ `,
237151
237428
  stderr: ""
237152
237429
  };
237153
237430
  }
237431
+ options.progress?.(
237432
+ `Found ${byKey.size} plan proposals in ${reviewedFiles.length} pricing files.`
237433
+ );
237434
+ await send({ plans: [...byKey.values()], reviewedFiles });
237154
237435
  }
237155
237436
  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");
237437
+ const planValues = [
237438
+ .../* @__PURE__ */ new Set([...context.planNames, ...run.planNames ?? []])
237439
+ ];
237440
+ options.progress?.(
237441
+ run.receipts.branches ? "4/4 Plan checks already submitted." : "4/4 Checking plan conditions\u2026"
237442
+ );
237158
237443
  if (!run.receipts.branches) {
237159
237444
  await update("running");
237160
237445
  const branches = planValues.length ? await discoverPlanBranches({
@@ -237173,6 +237458,7 @@ async function runGuidedSetup(options) {
237173
237458
  symbol: branch.symbol
237174
237459
  }))
237175
237460
  }));
237461
+ options.progress?.(`Found ${clusters.length} groups of plan conditions.`);
237176
237462
  await send({
237177
237463
  clusters,
237178
237464
  environment: context.environment,
@@ -237192,7 +237478,7 @@ async function runGuidedSetup(options) {
237192
237478
  clearInterval(timer);
237193
237479
  await reportFailure?.().catch(() => {
237194
237480
  });
237195
- const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Repository setup)/.test(
237481
+ const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Plan discovery|Repository setup)/.test(
237196
237482
  error51.message
237197
237483
  ) ? error51.message : "Setup could not finish. Check local repository access and developer credentials, then resume.";
237198
237484
  return { exitCode: 2, stdout: "", stderr: safe + "\n" };
@@ -237247,7 +237533,7 @@ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from
237247
237533
 
237248
237534
  // src/identity.ts
237249
237535
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
237250
- var SAASFUNNELS_CLI_VERSION = "0.3.1";
237536
+ var SAASFUNNELS_CLI_VERSION = "0.3.3";
237251
237537
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
237252
237538
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
237253
237539
  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.3";
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.3";
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, }?: {
@@ -0,0 +1 @@
1
+ export declare function repositorySourceScope(cwd: string): Promise<(_path: string) => boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasfunnels",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "SaaSFunnels CLI and local stdio MCP server",
6
6
  "author": "SaaSFunnels",