saasfunnels 0.3.0 → 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.
@@ -8649,7 +8649,7 @@ ${lanes.join("\n")}
8649
8649
  writeOutputIsTTY() {
8650
8650
  return process.stdout.isTTY;
8651
8651
  },
8652
- readFile: readFile6,
8652
+ readFile: readFile7,
8653
8653
  writeFile: writeFile22,
8654
8654
  watchFile: watchFile2,
8655
8655
  watchDirectory,
@@ -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
  },
@@ -8855,7 +8855,7 @@ ${lanes.join("\n")}
8855
8855
  callback
8856
8856
  );
8857
8857
  }
8858
- function readFile6(fileName, _encoding) {
8858
+ function readFile7(fileName, _encoding) {
8859
8859
  let buffer;
8860
8860
  try {
8861
8861
  buffer = _fs.readFileSync(fileName);
@@ -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
  }
@@ -45538,7 +45538,7 @@ ${lanes.join("\n")}
45538
45538
  const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName);
45539
45539
  return possibleOption ? createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption);
45540
45540
  }
45541
- function parseCommandLineWorker(diagnostics, commandLine, readFile6) {
45541
+ function parseCommandLineWorker(diagnostics, commandLine, readFile7) {
45542
45542
  const options = {};
45543
45543
  let watchOptions;
45544
45544
  const fileNames = [];
@@ -45586,7 +45586,7 @@ ${lanes.join("\n")}
45586
45586
  }
45587
45587
  }
45588
45588
  function parseResponseFile(fileName) {
45589
- const text = tryReadFile(fileName, readFile6 || ((fileName2) => sys.readFile(fileName2)));
45589
+ const text = tryReadFile(fileName, readFile7 || ((fileName2) => sys.readFile(fileName2)));
45590
45590
  if (!isString(text)) {
45591
45591
  errors.push(text);
45592
45592
  return;
@@ -45689,8 +45689,8 @@ ${lanes.join("\n")}
45689
45689
  unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
45690
45690
  optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument
45691
45691
  };
45692
- function parseCommandLine(commandLine, readFile6) {
45693
- return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile6);
45692
+ function parseCommandLine(commandLine, readFile7) {
45693
+ return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile7);
45694
45694
  }
45695
45695
  function getOptionFromName(optionName, allowShort) {
45696
45696
  return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort);
@@ -45772,8 +45772,8 @@ ${lanes.join("\n")}
45772
45772
  watchOptionsToExtend
45773
45773
  );
45774
45774
  }
45775
- function readConfigFile(fileName, readFile6) {
45776
- const textOrDiagnostic = tryReadFile(fileName, readFile6);
45775
+ function readConfigFile(fileName, readFile7) {
45776
+ const textOrDiagnostic = tryReadFile(fileName, readFile7);
45777
45777
  return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
45778
45778
  }
45779
45779
  function parseConfigFileTextToJson(fileName, jsonText) {
@@ -45788,14 +45788,14 @@ ${lanes.join("\n")}
45788
45788
  error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : void 0
45789
45789
  };
45790
45790
  }
45791
- function readJsonConfigFile(fileName, readFile6) {
45792
- const textOrDiagnostic = tryReadFile(fileName, readFile6);
45791
+ function readJsonConfigFile(fileName, readFile7) {
45792
+ const textOrDiagnostic = tryReadFile(fileName, readFile7);
45793
45793
  return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] };
45794
45794
  }
45795
- function tryReadFile(fileName, readFile6) {
45795
+ function tryReadFile(fileName, readFile7) {
45796
45796
  let text;
45797
45797
  try {
45798
- text = readFile6(fileName);
45798
+ text = readFile7(fileName);
45799
45799
  } catch (e) {
45800
45800
  return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message);
45801
45801
  }
@@ -124332,7 +124332,7 @@ ${lanes.join("\n")}
124332
124332
  }
124333
124333
  }
124334
124334
  function createImportCallExpressionAMD(arg, containsLexicalThis) {
124335
- const resolve5 = factory2.createUniqueName("resolve");
124335
+ const resolve6 = factory2.createUniqueName("resolve");
124336
124336
  const reject = factory2.createUniqueName("reject");
124337
124337
  const parameters = [
124338
124338
  factory2.createParameterDeclaration(
@@ -124341,7 +124341,7 @@ ${lanes.join("\n")}
124341
124341
  /*dotDotDotToken*/
124342
124342
  void 0,
124343
124343
  /*name*/
124344
- resolve5
124344
+ resolve6
124345
124345
  ),
124346
124346
  factory2.createParameterDeclaration(
124347
124347
  /*modifiers*/
@@ -124358,7 +124358,7 @@ ${lanes.join("\n")}
124358
124358
  factory2.createIdentifier("require"),
124359
124359
  /*typeArguments*/
124360
124360
  void 0,
124361
- [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve5, reject]
124361
+ [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve6, reject]
124362
124362
  )
124363
124363
  )
124364
124364
  ]);
@@ -135899,12 +135899,12 @@ ${lanes.join("\n")}
135899
135899
  function createCompilerHost(options, setParentNodes) {
135900
135900
  return createCompilerHostWorker(options, setParentNodes);
135901
135901
  }
135902
- function createGetSourceFile(readFile6, setParentNodes) {
135902
+ function createGetSourceFile(readFile7, setParentNodes) {
135903
135903
  return (fileName, languageVersionOrOptions, onError) => {
135904
135904
  let text;
135905
135905
  try {
135906
135906
  mark("beforeIORead");
135907
- text = readFile6(fileName);
135907
+ text = readFile7(fileName);
135908
135908
  mark("afterIORead");
135909
135909
  measure("I/O Read", "beforeIORead", "afterIORead");
135910
135910
  } catch (e) {
@@ -136808,7 +136808,7 @@ ${lanes.join("\n")}
136808
136808
  getRedirectFromOutput,
136809
136809
  forEachResolvedProjectReference: forEachResolvedProjectReference2
136810
136810
  });
136811
- const readFile6 = host.readFile.bind(host);
136811
+ const readFile7 = host.readFile.bind(host);
136812
136812
  (_e = tracing) == null ? void 0 : _e.push(tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram });
136813
136813
  const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
136814
136814
  (_f = tracing) == null ? void 0 : _f.pop();
@@ -137029,7 +137029,7 @@ ${lanes.join("\n")}
137029
137029
  shouldTransformImportCall,
137030
137030
  emitBuildInfo,
137031
137031
  fileExists,
137032
- readFile: readFile6,
137032
+ readFile: readFile7,
137033
137033
  directoryExists,
137034
137034
  getSymlinkCache,
137035
137035
  realpath: (_o = host.realpath) == null ? void 0 : _o.bind(host),
@@ -211289,8 +211289,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
211289
211289
  installPackage(options) {
211290
211290
  this.packageInstallId++;
211291
211291
  const request = { kind: "installPackage", ...options, id: this.packageInstallId };
211292
- const promise2 = new Promise((resolve5, reject) => {
211293
- (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve5, reject });
211292
+ const promise2 = new Promise((resolve6, reject) => {
211293
+ (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve6, reject });
211294
211294
  });
211295
211295
  this.installer.send(request);
211296
211296
  return promise2;
@@ -218902,14 +218902,66 @@ var require_dist = __commonJS({
218902
218902
  // src/saasfunnels.ts
218903
218903
  import { createInterface as createInterface2 } from "node:readline/promises";
218904
218904
 
218905
+ // src/setup-access.ts
218906
+ import { execFile } from "node:child_process";
218907
+ import { readFile } from "node:fs/promises";
218908
+ import { resolve } from "node:path";
218909
+ import { parseEnv, promisify } from "node:util";
218910
+ var exec = promisify(execFile);
218911
+ async function resolveSetupAccess(options) {
218912
+ if (options.key?.trim()) return { key: options.key.trim() };
218913
+ let file2 = options.envFile;
218914
+ if (!file2 && options.prompt) {
218915
+ const choice = (await options.prompt(
218916
+ "Developer access is not loaded. Where did you save SAASFUNNELS_API_KEY?\n 1. Doppler (this folder\u2019s configured project)\n 2. .env.local in this app\n 3. I haven\u2019t saved it yet\nChoose [1/2/3]: "
218917
+ )).trim();
218918
+ if (choice === "1") {
218919
+ try {
218920
+ const key2 = options.loadDoppler ? await options.loadDoppler() : (await exec("doppler", ["secrets", "get", "SAASFUNNELS_API_KEY", "--plain"], {
218921
+ cwd: options.cwd,
218922
+ timeout: 3e4,
218923
+ maxBuffer: 64e3
218924
+ })).stdout;
218925
+ if (key2.trim()) return { key: key2.trim() };
218926
+ } catch {
218927
+ }
218928
+ return { error: "Couldn\u2019t load developer access from Doppler. Check this folder\u2019s Doppler project/config and save SAASFUNNELS_API_KEY there, then rerun this command." };
218929
+ }
218930
+ if (choice === "2") file2 = ".env.local";
218931
+ }
218932
+ if (file2) {
218933
+ try {
218934
+ const source = await readFile(resolve(options.cwd, file2), "utf8");
218935
+ if (source.length > 64e3) throw new Error("Oversized env file");
218936
+ const key2 = parseEnv(source).SAASFUNNELS_API_KEY?.trim();
218937
+ if (key2) return { key: key2 };
218938
+ } catch {
218939
+ }
218940
+ return { error: "Couldn\u2019t find SAASFUNNELS_API_KEY in the selected env file. Save the developer access key there, then rerun this command. No file was changed." };
218941
+ }
218942
+ return { error: "Open SaaSFunnels \u2192 Setup \u2192 Environment values \u2192 Developer setup access. Save SAASFUNNELS_API_KEY in your secret manager or .env.local, then run npx --yes saasfunnels@latest setup again. Never paste the key into a command. For automation, load it into the process or use --env-file .env.local." };
218943
+ }
218944
+
218905
218945
  // src/cli.ts
218906
- import { dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
218946
+ import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
218907
218947
 
218908
218948
  // src/setup-pricing.ts
218909
218949
  var import_typescript = __toESM(require_typescript(), 1);
218910
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
+ );
218911
218962
  var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
218912
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);
218913
218965
  function declarations(source) {
218914
218966
  const file2 = import_typescript.default.createSourceFile(
218915
218967
  "pricing.ts",
@@ -218920,47 +218972,107 @@ function declarations(source) {
218920
218972
  );
218921
218973
  const values = /* @__PURE__ */ new Map();
218922
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
+ };
218923
218984
  for (const statement of file2.statements) {
218924
218985
  if (import_typescript.default.isVariableStatement(statement))
218925
218986
  for (const declaration of statement.declarationList.declarations) {
218926
218987
  if (import_typescript.default.isIdentifier(declaration.name) && declaration.initializer) {
218927
218988
  values.set(declaration.name.text, declaration.initializer);
218928
- 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);
218929
218994
  }
218930
218995
  }
218931
- 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);
218932
218998
  }
218933
- function read(node, depth = 0) {
218934
- 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);
218935
219002
  if (import_typescript.default.isAsExpression(node) || import_typescript.default.isSatisfiesExpression(node) || import_typescript.default.isParenthesizedExpression(node))
218936
- return read(node.expression, depth + 1);
219003
+ return next(node.expression);
218937
219004
  if (import_typescript.default.isStringLiteral(node) || import_typescript.default.isNoSubstitutionTemplateLiteral(node))
218938
219005
  return node.text;
218939
- if (import_typescript.default.isNumericLiteral(node)) return Number(node.text);
219006
+ if (import_typescript.default.isNumericLiteral(node)) return Number(node.text.replaceAll("_", ""));
218940
219007
  if (node.kind === import_typescript.default.SyntaxKind.TrueKeyword) return true;
218941
219008
  if (node.kind === import_typescript.default.SyntaxKind.FalseKeyword) return false;
218942
- if (import_typescript.default.isIdentifier(node) && values.has(node.text))
218943
- 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
+ }
218944
219013
  if (import_typescript.default.isArrayLiteralExpression(node))
218945
- 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
+ });
218946
219020
  if (import_typescript.default.isObjectLiteralExpression(node)) {
218947
219021
  const result3 = /* @__PURE__ */ Object.create(null);
218948
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
+ }
218949
219029
  if (!import_typescript.default.isPropertyAssignment(property) || import_typescript.default.isComputedPropertyName(property.name))
218950
- throw new Error("Unsupported pricing expression");
218951
- result3[property.name.getText(file2).replace(/^["']|["']$/g, "")] = read(
218952
- property.initializer,
218953
- depth + 1
218954
- );
219030
+ throw unsupported();
219031
+ const key2 = property.name.getText(file2).replace(/^["']|["']$/g, "");
219032
+ if (!safeProperty(key2)) throw unsupported();
219033
+ result3[key2] = next(property.initializer);
218955
219034
  }
218956
219035
  return result3;
218957
219036
  }
218958
- 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();
218959
219067
  }
218960
219068
  return roots.map((node) => read(node));
218961
219069
  }
218962
- function extractSetupPricing(source, kind) {
218963
- 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
+ );
218964
219076
  const documents = kind === "typescript" ? declarations(source) : kind === "json" ? [JSON.parse(source)] : [
218965
219077
  (0, import_yaml.parseDocument)(source, { uniqueKeys: true }).toJS({
218966
219078
  maxAliasCount: 10
@@ -218968,8 +219080,7 @@ function extractSetupPricing(source, kind) {
218968
219080
  ];
218969
219081
  const plans = /* @__PURE__ */ new Map();
218970
219082
  for (const doc of documents) {
218971
- const root = object(doc);
218972
- const candidate = root?.plans ?? doc;
219083
+ const candidate = object(doc)?.plans ?? doc;
218973
219084
  const rows = Array.isArray(candidate) ? candidate : object(candidate) ? Object.entries(candidate).map(
218974
219085
  ([key2, value]) => ({ key: key2, ...object(value) })
218975
219086
  ) : [];
@@ -218980,30 +219091,45 @@ function extractSetupPricing(source, kind) {
218980
219091
  const name = row.name ?? row.displayName ?? key2;
218981
219092
  if (!safeKey(key2) || typeof name !== "string" || !name.length || name.length > 120)
218982
219093
  continue;
218983
- 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))
218984
219102
  continue;
218985
- const features = {};
218986
- const rawFeatures = object(row.features);
218987
- if (rawFeatures)
218988
- for (const [feature, access2] of Object.entries(rawFeatures)) {
218989
- if (!safeKey(feature) || !(typeof access2 === "boolean" || typeof access2 === "number" && Number.isFinite(access2) && access2 >= 0))
218990
- 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();
218991
219113
  features[feature] = access2;
218992
219114
  }
218993
- else if (Array.isArray(row.features) && row.features.every(safeKey)) {
218994
- for (const feature of row.features) features[feature] = true;
218995
- } else if (row.features !== void 0)
218996
- throw new Error("Feature access needs manual review");
218997
- 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 }] : [];
218998
219117
  const prices = rawPrices.map((value2) => {
218999
219118
  const price = typeof value2 === "string" ? value2 : object(value2)?.key ?? object(value2)?.stripePriceId;
219000
- if (typeof price !== "string" || !/^price_[A-Za-z0-9]+$/.test(price))
219001
- throw new Error("Price needs manual review");
219002
- 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 };
219003
219129
  });
219004
219130
  const productKey = row.productKey ?? row.stripeProductId;
219005
219131
  if (productKey !== void 0 && (typeof productKey !== "string" || !/^prod_[A-Za-z0-9]+$/.test(productKey)))
219006
- throw new Error("Product needs manual review");
219132
+ throw unsupported();
219007
219133
  const plan = {
219008
219134
  key: key2,
219009
219135
  name,
@@ -219012,20 +219138,31 @@ function extractSetupPricing(source, kind) {
219012
219138
  ...typeof productKey === "string" ? { productKey } : {}
219013
219139
  };
219014
219140
  if (plans.has(key2) && JSON.stringify(plans.get(key2)) !== JSON.stringify(plan))
219015
- throw new Error("Conflicting plan declarations");
219141
+ throw new PricingExtractionError(
219142
+ "needs_review",
219143
+ "Conflicting plan declarations."
219144
+ );
219016
219145
  plans.set(key2, plan);
219017
219146
  }
219018
219147
  }
219019
- if (!plans.size) throw new Error("No supported pricing declaration found");
219020
- 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
+ );
219021
219158
  return [...plans.values()];
219022
219159
  }
219023
219160
 
219024
219161
  // src/setup-run.ts
219025
219162
  import { createHash as createHash3 } from "node:crypto";
219026
- import { execFile } from "node:child_process";
219027
- import { promisify } from "node:util";
219028
- import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
219163
+ import { execFile as execFile2 } from "node:child_process";
219164
+ import { promisify as promisify2 } from "node:util";
219165
+ import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
219029
219166
  import { join as join4 } from "node:path";
219030
219167
 
219031
219168
  // node_modules/zod/v4/classic/external.js
@@ -233548,14 +233685,14 @@ import { createHash as createHash2, randomUUID } from "node:crypto";
233548
233685
  import {
233549
233686
  access,
233550
233687
  mkdir,
233551
- readFile,
233688
+ readFile as readFile2,
233552
233689
  readdir,
233553
233690
  rename,
233554
233691
  rm,
233555
- stat,
233692
+ lstat,
233556
233693
  writeFile
233557
233694
  } from "node:fs/promises";
233558
- import { dirname, extname, join, relative, resolve } from "node:path";
233695
+ import { dirname, extname, join, relative, resolve as resolve2 } from "node:path";
233559
233696
 
233560
233697
  // src/runtime-contracts.js
233561
233698
  import { createHash } from "node:crypto";
@@ -235702,7 +235839,17 @@ var featureBindingKinds = [
235702
235839
  "funnel_trigger",
235703
235840
  "discovery"
235704
235841
  ];
235705
- 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
+ ];
235706
235853
  var ignoredSegments = /* @__PURE__ */ new Set([
235707
235854
  ".claude",
235708
235855
  ".git",
@@ -235711,6 +235858,13 @@ var ignoredSegments = /* @__PURE__ */ new Set([
235711
235858
  ".turbo",
235712
235859
  "build",
235713
235860
  "coverage",
235861
+ ".saasfunnels",
235862
+ "docs",
235863
+ "e2e",
235864
+ "fixtures",
235865
+ "__tests__",
235866
+ "tests",
235867
+ "__mocks__",
235714
235868
  "dist",
235715
235869
  "generated",
235716
235870
  "node_modules",
@@ -235809,21 +235963,29 @@ function allowedPath(path, excludes) {
235809
235963
  const normalized = path.replaceAll("\\", "/");
235810
235964
  if (sensitivePathPattern.test(normalized)) return false;
235811
235965
  const segments = normalized.split("/");
235812
- 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;
235813
235971
  return !excludes.some(
235814
235972
  (exclude) => normalized === exclude || normalized.startsWith(`${exclude}/`)
235815
235973
  );
235816
235974
  }
235817
235975
  async function walkFiles(input) {
235818
235976
  const files = [];
235819
- const queue = input.roots.map((root) => resolve(input.cwd, root));
235820
- while (queue.length && files.length < 500) {
235977
+ const queue = input.roots.map((root) => resolve2(input.cwd, root));
235978
+ const seen = /* @__PURE__ */ new Set();
235979
+ let oversizedFileCount = 0;
235980
+ while (queue.length) {
235821
235981
  const current = queue.shift();
235982
+ if (seen.has(current)) continue;
235983
+ seen.add(current);
235822
235984
  const relativePath = safeRelative(input.cwd, current);
235823
235985
  if (!relativePath || !allowedPath(relativePath, input.excludes)) continue;
235824
235986
  let currentStat;
235825
235987
  try {
235826
- currentStat = await stat(current);
235988
+ currentStat = await lstat(current);
235827
235989
  } catch {
235828
235990
  continue;
235829
235991
  }
@@ -235832,11 +235994,15 @@ async function walkFiles(input) {
235832
235994
  entries.sort((left, right) => left.name.localeCompare(right.name)).forEach((entry) => queue.push(join(current, entry.name)));
235833
235995
  continue;
235834
235996
  }
235835
- 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
+ }
235836
236002
  files.push(current);
235837
236003
  }
235838
236004
  }
235839
- return files.sort();
236005
+ return { files: files.sort(), oversizedFileCount };
235840
236006
  }
235841
236007
  function nearestSymbol(lines, lineIndex) {
235842
236008
  for (let index = lineIndex; index >= Math.max(0, lineIndex - 20); index -= 1) {
@@ -235938,7 +236104,7 @@ function candidatesFromFile(path, source) {
235938
236104
  async function detectFramework(cwd2, requestedRoots) {
235939
236105
  let packageJson = {};
235940
236106
  try {
235941
- packageJson = JSON.parse(await readFile(join(cwd2, "package.json"), "utf8"));
236107
+ packageJson = JSON.parse(await readFile2(join(cwd2, "package.json"), "utf8"));
235942
236108
  } catch {
235943
236109
  }
235944
236110
  const dependencies = {
@@ -235947,15 +236113,19 @@ async function detectFramework(cwd2, requestedRoots) {
235947
236113
  };
235948
236114
  const framework = typeof dependencies.next === "string" ? "nextjs" : "unsupported";
235949
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";
235950
- 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
+ );
235951
236119
  const existingRoots = [];
235952
236120
  for (const root of roots) {
235953
- if (await exists(resolve(cwd2, root))) existingRoots.push(root);
236121
+ if (await exists(resolve2(cwd2, root))) existingRoots.push(root);
235954
236122
  }
235955
236123
  return {
235956
236124
  framework,
235957
236125
  packageManager,
235958
- 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(),
235959
236129
  roots: existingRoots
235960
236130
  };
235961
236131
  }
@@ -236042,7 +236212,7 @@ export type SaaSFunnelsFeatureKey =
236042
236212
  }
236043
236213
  async function readable(path) {
236044
236214
  try {
236045
- return await readFile(path, "utf8");
236215
+ return await readFile2(path, "utf8");
236046
236216
  } catch {
236047
236217
  return null;
236048
236218
  }
@@ -236059,14 +236229,19 @@ async function applyFeatureSetupChangesAtomically(cwd2, changes, io = {
236059
236229
  const staged = [];
236060
236230
  try {
236061
236231
  for (const change of changes) {
236062
- const absolute = resolve(cwd2, change.path);
236232
+ const absolute = resolve2(cwd2, change.path);
236063
236233
  const relativePath = safeRelative(cwd2, absolute);
236064
236234
  if (!relativePath)
236065
- 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
+ );
236066
236238
  await io.mkdir(dirname(absolute), { recursive: true });
236067
236239
  originals.set(absolute, await io.read(absolute));
236068
236240
  const stagedPath = `${absolute}.saasfunnels-${transactionId}.tmp`;
236069
- await io.write(stagedPath, change.contents, { encoding: "utf8", mode: 384 });
236241
+ await io.write(stagedPath, change.contents, {
236242
+ encoding: "utf8",
236243
+ mode: 384
236244
+ });
236070
236245
  staged.push({ absolute, staged: stagedPath });
236071
236246
  }
236072
236247
  for (const entry of staged) await io.rename(entry.staged, entry.absolute);
@@ -236169,7 +236344,7 @@ async function runFeatureRuntimeCheck(input) {
236169
236344
  }
236170
236345
  }
236171
236346
  async function runFeatureSetup(input) {
236172
- if (input.environment !== "test") {
236347
+ if (input.environment !== "test" && !(input.manifestOnly && !input.apply && !input.prompt)) {
236173
236348
  throw new Error(
236174
236349
  "Feature setup is Test-only. Review and promote Feature catalogs from Stripe Products."
236175
236350
  );
@@ -236180,22 +236355,26 @@ async function runFeatureSetup(input) {
236180
236355
  `Inspect these local roots only: ${framework.roots.join(", ") || "none"}? [y/N] `
236181
236356
  );
236182
236357
  if (!/^y(?:es)?$/i.test(confirmation.trim())) {
236183
- 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
+ );
236184
236361
  }
236185
236362
  }
236186
- const files = await walkFiles({
236363
+ const { files, oversizedFileCount } = await walkFiles({
236187
236364
  cwd: input.cwd,
236188
236365
  excludes: input.exclude ?? [],
236189
236366
  roots: framework.roots
236190
236367
  });
236191
236368
  const discovered = [];
236192
236369
  let scannedBytes = 0;
236370
+ let scannedFileCount = 0;
236371
+ const byteLimitReached = oversizedFileCount > 0;
236193
236372
  for (const file2 of files) {
236194
- const source = await readFile(file2, "utf8");
236373
+ const source = await readFile2(file2, "utf8");
236195
236374
  scannedBytes += Buffer.byteLength(source);
236196
- if (scannedBytes > 2e6) break;
236197
236375
  const path = safeRelative(input.cwd, file2);
236198
236376
  if (!path) continue;
236377
+ scannedFileCount++;
236199
236378
  discovered.push(...candidatesFromFile(path, source));
236200
236379
  }
236201
236380
  const uniqueCandidates = /* @__PURE__ */ new Map();
@@ -236203,12 +236382,17 @@ async function runFeatureSetup(input) {
236203
236382
  (left, right) => right.confidenceBasisPoints - left.confidenceBasisPoints || left.file.localeCompare(right.file) || left.line - right.line
236204
236383
  )) {
236205
236384
  const identity = `${candidate.suggestedKey}:${candidate.accessModel}:${candidate.file}:${candidate.line}`;
236206
- if (!uniqueCandidates.has(identity)) uniqueCandidates.set(identity, candidate);
236385
+ if (!uniqueCandidates.has(identity))
236386
+ uniqueCandidates.set(identity, candidate);
236207
236387
  }
236208
- 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
+ );
236209
236393
  const manifestPath = ".saasfunnels/catalog.yaml";
236210
236394
  const existingManifestSource = await readable(
236211
- resolve(input.cwd, manifestPath)
236395
+ resolve2(input.cwd, manifestPath)
236212
236396
  );
236213
236397
  const existingManifestValidation = existingManifestSource ? parseFeatureManifestSource(existingManifestSource) : null;
236214
236398
  if (existingManifestValidation && !existingManifestValidation.ok) {
@@ -236225,7 +236409,8 @@ async function runFeatureSetup(input) {
236225
236409
  const accept = new Set(input.accept ?? []);
236226
236410
  const reject = new Set(input.reject ?? []);
236227
236411
  for (const candidate of candidates) {
236228
- if (reject.has(candidate.id) || reject.has(candidate.suggestedKey)) continue;
236412
+ if (reject.has(candidate.id) || reject.has(candidate.suggestedKey))
236413
+ continue;
236229
236414
  if (accept.has("all") || accept.has(candidate.id) || accept.has(candidate.suggestedKey)) {
236230
236415
  accepted.add(candidate.id);
236231
236416
  continue;
@@ -236239,7 +236424,10 @@ async function runFeatureSetup(input) {
236239
236424
  if (/^a(?:ccept)?$/i.test(answer.trim())) accepted.add(candidate.id);
236240
236425
  else if (keyPattern.test(answer.trim())) {
236241
236426
  accepted.add(candidate.id);
236242
- input.mappings = { ...input.mappings ?? {}, [candidate.id]: answer.trim() };
236427
+ input.mappings = {
236428
+ ...input.mappings ?? {},
236429
+ [candidate.id]: answer.trim()
236430
+ };
236243
236431
  }
236244
236432
  }
236245
236433
  }
@@ -236263,7 +236451,7 @@ async function runFeatureSetup(input) {
236263
236451
  const existingManifest = existingManifestSource;
236264
236452
  const constantsPath = ".saasfunnels/features.ts";
236265
236453
  const constantsContents = generatedConstants(manifest);
236266
- const existingConstants = await readable(resolve(input.cwd, constantsPath));
236454
+ const existingConstants = await readable(resolve2(input.cwd, constantsPath));
236267
236455
  const constantsSafe = existingConstants === null || existingConstants.startsWith(generatedMarker);
236268
236456
  const editInputs = [
236269
236457
  {
@@ -236373,7 +236561,16 @@ async function runFeatureSetup(input) {
236373
236561
  ok: validation.ok,
236374
236562
  reviewUrl,
236375
236563
  runtimeCheck: check2,
236376
- scannedFileCount: files.length,
236564
+ scannedFileCount,
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
+ },
236377
236574
  validation: { errors: validation.errors, ok: validation.ok }
236378
236575
  };
236379
236576
  }
@@ -236455,8 +236652,8 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
236455
236652
  }
236456
236653
 
236457
236654
  // src/plan-sources.ts
236458
- import { readFile as readFile2, readdir as readdir2, stat as stat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236459
- import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
236655
+ import { readFile as readFile3, readdir as readdir2, lstat as lstat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236656
+ import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
236460
236657
  var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
236461
236658
  var maxPlanSourceFiles = 12;
236462
236659
  var maxPlanSourceCharacters = 512e3;
@@ -236486,10 +236683,9 @@ var planSourceKindByExtension = /* @__PURE__ */ new Map([
236486
236683
  [".yml", "yaml"]
236487
236684
  ]);
236488
236685
  var billingIntervalPattern = /\b(month|monthly|year|yearly|annual|annually|week|weekly|day|daily|recurring|interval|billing_period)\b/i;
236489
- 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;
236490
236687
  var stripePriceIdPattern = /["'`]price_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{14,}["'`]/;
236491
236688
  var nonDefinitionPattern = /(^|\/)(__tests__|__mocks__|tests?|specs?)(\/)|\.(test|spec|d)\.[cm]?[jt]sx?$/i;
236492
- var maxNamedOnlyCandidates = 10;
236493
236689
  function normalize(path) {
236494
236690
  return path.split(sep).join("/");
236495
236691
  }
@@ -236513,8 +236709,8 @@ async function discoverPlanSourceCandidates(input) {
236513
236709
  const roots = input.roots ?? [".", "config", ...defaultRoots];
236514
236710
  const seen = /* @__PURE__ */ new Set();
236515
236711
  const candidates = [];
236516
- const queue = roots.map((root) => resolve2(input.cwd, root));
236517
- while (queue.length && candidates.length < 200) {
236712
+ const queue = roots.map((root) => resolve3(input.cwd, root));
236713
+ while (queue.length) {
236518
236714
  const current = queue.shift();
236519
236715
  if (seen.has(current)) continue;
236520
236716
  seen.add(current);
@@ -236522,13 +236718,16 @@ async function discoverPlanSourceCandidates(input) {
236522
236718
  if (relativePath && !allowed(relativePath, excludes)) continue;
236523
236719
  let entryStat;
236524
236720
  try {
236525
- entryStat = await stat2(current);
236721
+ entryStat = await lstat2(current);
236526
236722
  } catch {
236527
236723
  continue;
236528
236724
  }
236725
+ if (entryStat.isSymbolicLink()) continue;
236529
236726
  if (entryStat.isDirectory()) {
236530
236727
  const entries = await readdir2(current, { withFileTypes: true });
236531
- 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
+ )) {
236532
236731
  queue.push(join2(current, entry.name));
236533
236732
  }
236534
236733
  continue;
@@ -236536,8 +236735,14 @@ async function discoverPlanSourceCandidates(input) {
236536
236735
  if (!entryStat.isFile()) continue;
236537
236736
  const kind = planSourceKindByExtension.get(extname2(current).toLowerCase());
236538
236737
  if (!kind || !relativePath) continue;
236539
- if (entryStat.size > maxPlanSourceCharacters) continue;
236540
- const contents = await readFile2(current, "utf8");
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
+ }
236745
+ const contents = await readFile3(current, "utf8");
236541
236746
  const hasStripePriceId = stripePriceIdPattern.test(contents);
236542
236747
  const named = looksLikePlanSource(relativePath);
236543
236748
  const looksPriced = billingIntervalPattern.test(contents) && chargeAmountPattern.test(contents);
@@ -236556,19 +236761,21 @@ async function discoverPlanSourceCandidates(input) {
236556
236761
  );
236557
236762
  const confirmed = sorted.filter((candidate) => candidate.hasStripePriceId);
236558
236763
  const namedOnly = sorted.filter((candidate) => !candidate.hasStripePriceId);
236559
- return [...confirmed, ...namedOnly.slice(0, maxNamedOnlyCandidates)];
236764
+ return [...confirmed, ...namedOnly];
236560
236765
  }
236561
236766
  async function readApprovedPlanSources(cwd2) {
236562
236767
  try {
236563
- const raw = await readFile2(resolve2(cwd2, planSourceApprovalPath), "utf8");
236768
+ const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
236564
236769
  const parsed = JSON.parse(raw);
236565
- 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
+ ) : [];
236566
236773
  } catch {
236567
236774
  return [];
236568
236775
  }
236569
236776
  }
236570
236777
  async function writeApprovedPlanSources(cwd2, files) {
236571
- const target = resolve2(cwd2, planSourceApprovalPath);
236778
+ const target = resolve3(cwd2, planSourceApprovalPath);
236572
236779
  await mkdir2(dirname2(target), { recursive: true });
236573
236780
  await writeFile2(
236574
236781
  target,
@@ -236599,11 +236806,13 @@ async function buildPlanMappingHandoff(input) {
236599
236806
  if (!allowed(relativePath, [])) {
236600
236807
  throw new Error(`${relativePath} is not a readable plan source path.`);
236601
236808
  }
236602
- const kind = planSourceKindByExtension.get(extname2(relativePath).toLowerCase());
236809
+ const kind = planSourceKindByExtension.get(
236810
+ extname2(relativePath).toLowerCase()
236811
+ );
236603
236812
  if (!kind) {
236604
236813
  throw new Error(`${relativePath} is not a supported plan source type.`);
236605
236814
  }
236606
- const content = await readFile2(resolve2(input.cwd, relativePath), "utf8");
236815
+ const content = await readFile3(resolve3(input.cwd, relativePath), "utf8");
236607
236816
  if (content.length > maxPlanSourceCharacters) {
236608
236817
  throw new Error(
236609
236818
  `${relativePath} is ${content.length} characters; the limit is ${maxPlanSourceCharacters}.`
@@ -236619,8 +236828,8 @@ function planMappingRequestKey(input) {
236619
236828
  }
236620
236829
 
236621
236830
  // src/plan-branches.ts
236622
- import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "node:fs/promises";
236623
- import { extname as extname3, join as join3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
236831
+ import { readdir as readdir3, readFile as readFile4, lstat as lstat3 } from "node:fs/promises";
236832
+ import { extname as extname3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
236624
236833
  var maxPlanBranchFileCharacters = 2e6;
236625
236834
  var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
236626
236835
  var nonDefinitionPattern2 = /(^|\/)(__tests__|__mocks__|tests?|specs?|fixtures?|testing|mocks?|mock-data|stories|__stories__)(\/)|\.(test|spec|d|stories)\.[cm]?[jt]sx?$/i;
@@ -236749,7 +236958,9 @@ function clusterPlanBranches(branches) {
236749
236958
  return {
236750
236959
  branches: grouped,
236751
236960
  location: key2.slice(key2.indexOf("|") + 1),
236752
- planValues: [...new Set(grouped.map((branch) => branch.planValue))].sort(),
236961
+ planValues: [
236962
+ ...new Set(grouped.map((branch) => branch.planValue))
236963
+ ].sort(),
236753
236964
  polarity: decided,
236754
236965
  shape: grouped[0].shape
236755
236966
  };
@@ -236768,7 +236979,7 @@ async function discoverPlanBranches(input) {
236768
236979
  const branches = [];
236769
236980
  const constants = /* @__PURE__ */ new Map();
236770
236981
  const pending = [];
236771
- const queue = roots.map((root) => resolve3(input.cwd, root));
236982
+ const queue = roots.map((root) => resolve4(input.cwd, root));
236772
236983
  while (queue.length) {
236773
236984
  const current = queue.shift();
236774
236985
  if (seen.has(current)) continue;
@@ -236777,21 +236988,27 @@ async function discoverPlanBranches(input) {
236777
236988
  if (relativePath && !allowed2(relativePath, excludes)) continue;
236778
236989
  let entryStat;
236779
236990
  try {
236780
- entryStat = await stat3(current);
236991
+ entryStat = await lstat3(current);
236781
236992
  } catch {
236782
236993
  continue;
236783
236994
  }
236995
+ if (entryStat.isSymbolicLink()) continue;
236784
236996
  if (entryStat.isDirectory()) {
236785
236997
  const entries = await readdir3(current, { withFileTypes: true });
236786
- 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
+ )) {
236787
237001
  queue.push(join3(current, entry.name));
236788
237002
  }
236789
237003
  continue;
236790
237004
  }
236791
237005
  if (!entryStat.isFile() || !relativePath) continue;
236792
237006
  if (!codeExtensions.has(extname3(current).toLowerCase())) continue;
236793
- if (entryStat.size > maxPlanBranchFileCharacters) continue;
236794
- const lines = (await readFile3(current, "utf8")).split(/\r?\n/);
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
+ );
237011
+ const lines = (await readFile4(current, "utf8")).split(/\r?\n/);
236795
237012
  collectConstants(lines, constants);
236796
237013
  lines.forEach((line, lineIndex) => {
236797
237014
  comparisonPattern.lastIndex = 0;
@@ -236831,7 +237048,7 @@ async function discoverPlanBranches(input) {
236831
237048
  }
236832
237049
 
236833
237050
  // src/setup-run.ts
236834
- var exec = promisify(execFile);
237051
+ var exec2 = promisify2(execFile2);
236835
237052
  var hash2 = (text) => createHash3("sha256").update(text).digest("hex");
236836
237053
  var contextSchema = external_exports.object({
236837
237054
  workspaceId: external_exports.string(),
@@ -236840,7 +237057,13 @@ var contextSchema = external_exports.object({
236840
237057
  integrationId: external_exports.string().nullable(),
236841
237058
  environment: external_exports.enum(["test", "production"]),
236842
237059
  catalogReady: external_exports.boolean(),
236843
- 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([])
236844
237067
  });
236845
237068
  function safeRepositoryKey(remote) {
236846
237069
  const ssh = remote.match(/^[^@\s]+@([^:]+):(.+)$/);
@@ -236889,6 +237112,7 @@ async function runGuidedSetup(options) {
236889
237112
  let progressState = "running";
236890
237113
  let progressFailure = null;
236891
237114
  try {
237115
+ options.progress?.("1/4 Checking app connection\u2026");
236892
237116
  let context = contextSchema.parse(
236893
237117
  await request("/api/developer-tools/setup/context")
236894
237118
  );
@@ -236896,8 +237120,16 @@ async function runGuidedSetup(options) {
236896
237120
  throw new Error(
236897
237121
  "Connect Stripe and create app credentials in Setup first."
236898
237122
  );
236899
- const head = (await exec("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
236900
- const diff = (await exec(
237123
+ try {
237124
+ await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
237125
+ await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
237126
+ } catch {
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
+ );
237130
+ }
237131
+ const head = (await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
237132
+ const diff = (await exec2(
236901
237133
  "git",
236902
237134
  [
236903
237135
  "diff",
@@ -236910,7 +237142,7 @@ async function runGuidedSetup(options) {
236910
237142
  ],
236911
237143
  { cwd: options.cwd, maxBuffer: 8e6 }
236912
237144
  )).stdout;
236913
- const untracked = (await exec("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
237145
+ const untracked = (await exec2("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
236914
237146
  cwd: options.cwd
236915
237147
  })).stdout.split("\0").filter(
236916
237148
  (path) => path && !path.startsWith(".saasfunnels/") && !/(^|\/)(\.env|node_modules|dist|\.next)(\/|$|\.)/i.test(path)
@@ -236922,15 +237154,15 @@ async function runGuidedSetup(options) {
236922
237154
  const sourceHashes = await Promise.all(
236923
237155
  untracked.map(async (path) => [
236924
237156
  path,
236925
- hash2(await readFile4(join4(options.cwd, path), "utf8"))
237157
+ hash2(await readFile5(join4(options.cwd, path), "utf8"))
236926
237158
  ])
236927
237159
  );
236928
237160
  const revision = `${head}:${hash2(JSON.stringify([diff, sourceHashes]))}`;
236929
- const remote = (await exec("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
237161
+ const remote = (await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
236930
237162
  const repositoryKey = safeRepositoryKey(remote);
236931
237163
  const statePath = join4(options.cwd, ".saasfunnels", "setup-run.json");
236932
237164
  if (options.resume) {
236933
- const saved = JSON.parse(await readFile4(statePath, "utf8"));
237165
+ const saved = JSON.parse(await readFile5(statePath, "utf8"));
236934
237166
  if (saved.generation !== context.generation || saved.repositoryKey !== repositoryKey || saved.revision !== revision)
236935
237167
  throw new Error(
236936
237168
  "The app connection or revision changed. Run setup again without --resume."
@@ -236943,9 +237175,9 @@ async function runGuidedSetup(options) {
236943
237175
  );
236944
237176
  if (!approved)
236945
237177
  return {
236946
- exitCode: 2,
236947
- stdout: "",
236948
- stderr: "No evidence uploaded. Review the installation instructions, then run setup run --send to approve discovery uploads.\n"
237178
+ exitCode: options.prompt ? 0 : 2,
237179
+ stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n",
237180
+ stderr: ""
236949
237181
  };
236950
237182
  run = (await request("/api/developer-tools/setup/runs", {
236951
237183
  generation: context.generation,
@@ -236990,6 +237222,10 @@ async function runGuidedSetup(options) {
236990
237222
  };
236991
237223
  const send = (evidence) => {
236992
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
+ );
236993
237229
  heartbeat = heartbeat.then(async () => {
236994
237230
  const result3 = await request("/api/developer-tools/setup/evidence", {
236995
237231
  runId: run.id,
@@ -236999,7 +237235,8 @@ async function runGuidedSetup(options) {
236999
237235
  if (typeof result3.sequence === "number")
237000
237236
  run.sequence = result3.sequence;
237001
237237
  run.receipts[currentStage] = result3.receipt ?? { complete: true };
237002
- 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]);
237003
237240
  });
237004
237241
  return heartbeat;
237005
237242
  };
@@ -237013,6 +237250,9 @@ async function runGuidedSetup(options) {
237013
237250
  void update(progressState, progressFailure).catch(() => {
237014
237251
  });
237015
237252
  }, 3e4);
237253
+ options.progress?.(
237254
+ run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026"
237255
+ );
237016
237256
  if (!run.receipts.features) {
237017
237257
  await update("running");
237018
237258
  const scan = await runFeatureSetup({
@@ -237023,6 +237263,15 @@ async function runGuidedSetup(options) {
237023
237263
  manifestOnly: true,
237024
237264
  accept: ["all"]
237025
237265
  });
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
+ );
237026
237275
  if (!scan.ok)
237027
237276
  throw new Error(
237028
237277
  "Feature discovery needs attention. Run features setup to review the local findings."
@@ -237037,11 +237286,15 @@ async function runGuidedSetup(options) {
237037
237286
  );
237038
237287
  }
237039
237288
  stage = "plans";
237289
+ if (!context.catalogReady)
237290
+ options.progress?.(
237291
+ "Waiting for your Stripe catalog. Setup can stay open while it imports\u2026"
237292
+ );
237040
237293
  const catalogDeadline = Date.now() + 5 * 6e4;
237041
237294
  const generation = context.generation;
237042
237295
  while (!context.catalogReady && Date.now() < catalogDeadline) {
237043
237296
  await update("waiting", "catalog_pending");
237044
- await new Promise((resolve5) => setTimeout(resolve5, 1e4));
237297
+ await new Promise((resolve6) => setTimeout(resolve6, 1e4));
237045
237298
  context = contextSchema.parse(
237046
237299
  await request("/api/developer-tools/setup/context")
237047
237300
  );
@@ -237051,54 +237304,86 @@ async function runGuidedSetup(options) {
237051
237304
  if (!context.catalogReady)
237052
237305
  return {
237053
237306
  exitCode: 2,
237054
- stdout: "Stripe is still importing. Run setup run --resume --send when the catalog is ready.\n",
237307
+ stdout: "Stripe is still importing. Run npx --yes saasfunnels@latest setup --resume when the catalog is ready.\n",
237055
237308
  stderr: ""
237056
237309
  };
237310
+ options.progress?.(
237311
+ run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026"
237312
+ );
237057
237313
  if (!run.receipts.plans) {
237058
237314
  await update("running");
237059
237315
  const candidates = await discoverPlanSourceCandidates({
237060
237316
  cwd: options.cwd
237061
237317
  });
237318
+ let manual = null;
237062
237319
  try {
237063
- if (candidates.length > 12) throw new Error("Too many sources");
237064
- 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) {
237065
237332
  try {
237066
- manual = await readFile4(
237067
- join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237068
- "utf8"
237333
+ const plans = extractSetupPricing(
237334
+ manual ?? await readFile5(join4(options.cwd, candidate.path), "utf8"),
237335
+ candidate.kind,
237336
+ context.catalogPrices
237069
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);
237070
237347
  } catch (error51) {
237071
- 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
+ });
237072
237354
  }
237073
- const discovered = manual !== null ? extractSetupPricing(manual, "json") : (await Promise.all(
237074
- candidates.map(
237075
- async (candidate) => extractSetupPricing(
237076
- await readFile4(join4(options.cwd, candidate.path), "utf8"),
237077
- candidate.kind
237078
- )
237079
- )
237080
- )).flat();
237081
- const byKey = /* @__PURE__ */ new Map();
237082
- for (const plan of discovered) {
237083
- if (byKey.has(plan.key) && JSON.stringify(byKey.get(plan.key)) !== JSON.stringify(plan))
237084
- throw new Error("Conflicting pricing sources");
237085
- byKey.set(plan.key, plan);
237086
- }
237087
- await send({
237088
- plans: [...byKey.values()],
237089
- 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."
237090
237360
  });
237091
- } 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
+ );
237092
237367
  await update("waiting", "extraction_unsupported");
237093
237368
  return {
237094
237369
  exitCode: 2,
237095
- 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 setup run --resume --send. 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
+ `,
237096
237372
  stderr: ""
237097
237373
  };
237098
237374
  }
237375
+ options.progress?.(
237376
+ `Found ${byKey.size} plan proposals in ${reviewedFiles.length} pricing files.`
237377
+ );
237378
+ await send({ plans: [...byKey.values()], reviewedFiles });
237099
237379
  }
237100
237380
  stage = "branches";
237101
- const planValues = [.../* @__PURE__ */ new Set([...context.planNames, ...run.planNames ?? []])];
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
+ );
237102
237387
  if (!run.receipts.branches) {
237103
237388
  await update("running");
237104
237389
  const branches = planValues.length ? await discoverPlanBranches({
@@ -237117,6 +237402,7 @@ async function runGuidedSetup(options) {
237117
237402
  symbol: branch.symbol
237118
237403
  }))
237119
237404
  }));
237405
+ options.progress?.(`Found ${clusters.length} groups of plan conditions.`);
237120
237406
  await send({
237121
237407
  clusters,
237122
237408
  environment: context.environment,
@@ -237129,14 +237415,14 @@ async function runGuidedSetup(options) {
237129
237415
  }
237130
237416
  return {
237131
237417
  exitCode: 0,
237132
- stdout: "Discovery submitted. Return to Setup to review findings and verify installation. Payment transactions have not been tested by this command.\n",
237418
+ stdout: "Discovery submitted. Open https://app.saasfunnels.ai/app/setup to review findings and verify installation. Payment transactions have not been tested by this command.\n",
237133
237419
  stderr: ""
237134
237420
  };
237135
237421
  } catch (error51) {
237136
237422
  clearInterval(timer);
237137
237423
  await reportFailure?.().catch(() => {
237138
237424
  });
237139
- const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery)/.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(
237140
237426
  error51.message
237141
237427
  ) ? error51.message : "Setup could not finish. Check local repository access and developer credentials, then resume.";
237142
237428
  return { exitCode: 2, stdout: "", stderr: safe + "\n" };
@@ -237187,11 +237473,11 @@ async function submitApprovedSetupPlans(input) {
237187
237473
  }
237188
237474
 
237189
237475
  // src/cli.ts
237190
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
237476
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
237191
237477
 
237192
237478
  // src/identity.ts
237193
237479
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
237194
- var SAASFUNNELS_CLI_VERSION = "0.3.0";
237480
+ var SAASFUNNELS_CLI_VERSION = "0.3.2";
237195
237481
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
237196
237482
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
237197
237483
  var SAASFUNNELS_ENV = Object.freeze({
@@ -238979,7 +239265,8 @@ function usage() {
238979
239265
  return `${SAASFUNNELS_PRODUCT_NAME} CLI
238980
239266
 
238981
239267
  Usage:
238982
- ${SAASFUNNELS_CLI_NAME} setup run [--resume] [--send]
239268
+ ${SAASFUNNELS_CLI_NAME} setup [--resume] [--env-file <path>]
239269
+ ${SAASFUNNELS_CLI_NAME} setup run [--resume] [--send] [--non-interactive]
238983
239270
  ${SAASFUNNELS_CLI_NAME} init [--api-base-url <url>] [--force]
238984
239271
  ${SAASFUNNELS_CLI_NAME} agent install --target codex|claude-code|cursor|markdown [--endpoint <url>]
238985
239272
  ${SAASFUNNELS_CLI_NAME} events validate <file> [--source direct|posthog|segment] [--json]
@@ -239016,7 +239303,7 @@ function filesystem(options) {
239016
239303
  mkdir: async (path, mkdirOptions) => {
239017
239304
  await mkdir4(path, mkdirOptions);
239018
239305
  },
239019
- readFile: readFile5,
239306
+ readFile: readFile6,
239020
239307
  writeFile: writeFile4
239021
239308
  };
239022
239309
  }
@@ -239047,7 +239334,7 @@ function sourceFlag(flags) {
239047
239334
  }
239048
239335
  async function readJsonFile(filePath, options) {
239049
239336
  const raw = await filesystem(options).readFile(
239050
- resolve4(cwd(options), filePath),
239337
+ resolve5(cwd(options), filePath),
239051
239338
  "utf8"
239052
239339
  );
239053
239340
  return JSON.parse(raw);
@@ -239236,7 +239523,7 @@ async function commandFeatures(args, flags, options) {
239236
239523
  }
239237
239524
  const discoveryRoots = (flagString(flags, "discovery-roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean);
239238
239525
  const source = await filesystem(options).readFile(
239239
- resolve4(cwd(options), path),
239526
+ resolve5(cwd(options), path),
239240
239527
  "utf8"
239241
239528
  );
239242
239529
  const validation = parseFeatureManifestSource(source);
@@ -239252,7 +239539,7 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
239252
239539
  let sdkVersions = [];
239253
239540
  try {
239254
239541
  const packageSource = await filesystem(options).readFile(
239255
- resolve4(cwd(options), "package.json"),
239542
+ resolve5(cwd(options), "package.json"),
239256
239543
  "utf8"
239257
239544
  );
239258
239545
  sdkVersions = featureSdkVersionsFromPackageJson(
@@ -239323,7 +239610,7 @@ async function commandCatalog(args, flags, options) {
239323
239610
  if (action === "validate") {
239324
239611
  const path = args[1] ?? ".saasfunnels/catalog.yaml";
239325
239612
  const source = await filesystem(options).readFile(
239326
- resolve4(cwd(options), path),
239613
+ resolve5(cwd(options), path),
239327
239614
  "utf8"
239328
239615
  );
239329
239616
  const validation = parseFeatureManifestSource(source);
@@ -239573,7 +239860,7 @@ async function commandVerify(flags, options) {
239573
239860
  if (catalogPath) {
239574
239861
  try {
239575
239862
  const source = await filesystem(options).readFile(
239576
- resolve4(cwd(options), catalogPath),
239863
+ resolve5(cwd(options), catalogPath),
239577
239864
  "utf8"
239578
239865
  );
239579
239866
  const validation = parseFeatureManifestSource(source);
@@ -239916,7 +240203,15 @@ async function runSaaSFunnelsCli(argv2, options = {}) {
239916
240203
  if (!command || command === "help" || command === "--help")
239917
240204
  return result(0, usage());
239918
240205
  if (command === "init") return await commandInit(parsed.flags, options);
239919
- if (command === "setup" && args[0] === "run") return runGuidedSetup({ cwd: cwd(options), apiBaseUrl: apiBaseUrl2(options, parsed.flags), key: envValue2(options, SAASFUNNELS_ENV.apiKey), send: hasFlag(parsed.flags, "send"), resume: hasFlag(parsed.flags, "resume"), fetch: options.fetch, prompt: jsonMode(parsed.flags) ? void 0 : options.prompt });
240206
+ if (command === "setup") {
240207
+ const help = "SaaSFunnels setup\n\nRun npx --yes saasfunnels@latest setup in your app repository.\nIt checks access, asks before sending findings, and discovers features and plans for review.\nUse --resume after an interruption, or --env-file .env.local to load developer access.\nFor automation, supply SAASFUNNELS_API_KEY and explicit upload approval with --send --non-interactive.\nApp implementation, deployment and payment verification remain in https://app.saasfunnels.ai/app/setup.\n";
240208
+ if (hasFlag(parsed.flags, "help") || args[0] === "help") return result(0, help);
240209
+ if (args.length > 1 || args[0] && args[0] !== "run") return result(2, "", help);
240210
+ const interactive = !jsonMode(parsed.flags) && !hasFlag(parsed.flags, "non-interactive");
240211
+ const access2 = await resolveSetupAccess({ cwd: cwd(options), key: envValue2(options, SAASFUNNELS_ENV.apiKey), envFile: flagString(parsed.flags, "env-file"), prompt: interactive ? options.prompt : void 0 });
240212
+ if (!access2.key) return result(2, "", access2.error + "\n");
240213
+ return runGuidedSetup({ cwd: cwd(options), apiBaseUrl: apiBaseUrl2(options, parsed.flags), key: access2.key, send: hasFlag(parsed.flags, "send"), resume: hasFlag(parsed.flags, "resume"), fetch: options.fetch, prompt: interactive ? options.prompt : void 0, progress: jsonMode(parsed.flags) ? void 0 : options.progress });
240214
+ }
239920
240215
  if (command === "agent")
239921
240216
  return await commandAgentInstall(args, parsed.flags, options);
239922
240217
  if (command === "events")
@@ -239944,13 +240239,14 @@ ${usage()}`);
239944
240239
 
239945
240240
  // src/saasfunnels.ts
239946
240241
  var argv = process.argv.slice(2);
239947
- var interactiveFeatureSetup = !argv.includes("--json") && !argv.includes("--non-interactive") && (argv[0] === "setup" && argv[1] === "run" || argv[0] === "features" && argv[1] === "setup" || argv[0] === "catalog" && ["discover", "diff"].includes(argv[1] ?? ""));
240242
+ var interactiveFeatureSetup = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !argv.includes("--json") && !argv.includes("--non-interactive") && (argv[0] === "setup" || argv[0] === "features" && argv[1] === "setup" || argv[0] === "catalog" && ["discover", "diff"].includes(argv[1] ?? ""));
239948
240243
  var prompts = interactiveFeatureSetup ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
239949
240244
  var result2;
239950
240245
  try {
239951
240246
  result2 = await runSaaSFunnelsCli(argv, {
239952
240247
  cwd: process.cwd(),
239953
240248
  env: process.env,
240249
+ progress: (message) => process.stdout.write(message + "\n"),
239954
240250
  prompt: prompts ? (message) => prompts.question(message) : void 0
239955
240251
  });
239956
240252
  } finally {