saasfunnels 0.3.0 → 0.3.1

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
@@ -10,12 +10,16 @@ This repository contains only the MIT-licensed CLI and its bounded runtime contr
10
10
 
11
11
  ## Install
12
12
 
13
- Install the stable CLI:
13
+ Start in your app repository:
14
14
 
15
15
  ```bash
16
- npm install --global saasfunnels
16
+ npx --yes saasfunnels@latest setup
17
17
  ```
18
18
 
19
+ The explicit version tag avoids running an older project-local CLI. Setup asks where developer access is saved (Doppler or `.env.local`) when needed, requests approval before uploading findings, and shows each discovery stage. It never prints keys, rewrites env files, deploys your app, or approves inferred plans. Connect Stripe and create app credentials in the web Setup first. `setup run` remains a compatible entry point.
20
+
21
+ For scripts, use `--env-file .env.local --send --non-interactive`, or inject `SAASFUNNELS_API_KEY` from your secret manager. Without explicit approval, non-interactive runs stop without uploading. Run `setup --help` for setup-only guidance. Runtime implementation and payment verification remain in the web Setup.
22
+
19
23
  ## Use
20
24
 
21
25
  ```bash
@@ -51,7 +55,7 @@ approved set is reviewable.
51
55
  discovery-roots: app,lib
52
56
  ```
53
57
 
54
- On a pull request the Action scans as a `candidate`: the result is compared
58
+ The Action uploads an existing feature manifest; it does not freshly scan application source. On a pull request the upload is a `candidate`: the result is compared
55
59
  against the branch's baseline and discarded. Only the default branch advances
56
60
  the lineage. Changing `discovery-roots` starts a new lineage, so drift is
57
61
  measured against a comparable scan rather than a wider or narrower one.
package/dist/library.js CHANGED
@@ -24264,7 +24264,7 @@ function validateFeatureCatalogManifest(value) {
24264
24264
 
24265
24265
  // src/identity.ts
24266
24266
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
24267
- var SAASFUNNELS_CLI_VERSION = "0.3.0";
24267
+ var SAASFUNNELS_CLI_VERSION = "0.3.1";
24268
24268
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
24269
24269
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
24270
24270
  var SAASFUNNELS_ENV = Object.freeze({
@@ -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,
@@ -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);
@@ -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,8 +218902,48 @@ 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);
@@ -219023,9 +219063,9 @@ function extractSetupPricing(source, kind) {
219023
219063
 
219024
219064
  // src/setup-run.ts
219025
219065
  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";
219066
+ import { execFile as execFile2 } from "node:child_process";
219067
+ import { promisify as promisify2 } from "node:util";
219068
+ import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
219029
219069
  import { join as join4 } from "node:path";
219030
219070
 
219031
219071
  // node_modules/zod/v4/classic/external.js
@@ -233548,14 +233588,14 @@ import { createHash as createHash2, randomUUID } from "node:crypto";
233548
233588
  import {
233549
233589
  access,
233550
233590
  mkdir,
233551
- readFile,
233591
+ readFile as readFile2,
233552
233592
  readdir,
233553
233593
  rename,
233554
233594
  rm,
233555
233595
  stat,
233556
233596
  writeFile
233557
233597
  } from "node:fs/promises";
233558
- import { dirname, extname, join, relative, resolve } from "node:path";
233598
+ import { dirname, extname, join, relative, resolve as resolve2 } from "node:path";
233559
233599
 
233560
233600
  // src/runtime-contracts.js
233561
233601
  import { createHash } from "node:crypto";
@@ -235816,7 +235856,7 @@ function allowedPath(path, excludes) {
235816
235856
  }
235817
235857
  async function walkFiles(input) {
235818
235858
  const files = [];
235819
- const queue = input.roots.map((root) => resolve(input.cwd, root));
235859
+ const queue = input.roots.map((root) => resolve2(input.cwd, root));
235820
235860
  while (queue.length && files.length < 500) {
235821
235861
  const current = queue.shift();
235822
235862
  const relativePath = safeRelative(input.cwd, current);
@@ -235938,7 +235978,7 @@ function candidatesFromFile(path, source) {
235938
235978
  async function detectFramework(cwd2, requestedRoots) {
235939
235979
  let packageJson = {};
235940
235980
  try {
235941
- packageJson = JSON.parse(await readFile(join(cwd2, "package.json"), "utf8"));
235981
+ packageJson = JSON.parse(await readFile2(join(cwd2, "package.json"), "utf8"));
235942
235982
  } catch {
235943
235983
  }
235944
235984
  const dependencies = {
@@ -235950,7 +235990,7 @@ async function detectFramework(cwd2, requestedRoots) {
235950
235990
  const roots = (requestedRoots?.length ? requestedRoots : defaultRoots).filter((root) => !root.startsWith("../") && !root.startsWith("/"));
235951
235991
  const existingRoots = [];
235952
235992
  for (const root of roots) {
235953
- if (await exists(resolve(cwd2, root))) existingRoots.push(root);
235993
+ if (await exists(resolve2(cwd2, root))) existingRoots.push(root);
235954
235994
  }
235955
235995
  return {
235956
235996
  framework,
@@ -236042,7 +236082,7 @@ export type SaaSFunnelsFeatureKey =
236042
236082
  }
236043
236083
  async function readable(path) {
236044
236084
  try {
236045
- return await readFile(path, "utf8");
236085
+ return await readFile2(path, "utf8");
236046
236086
  } catch {
236047
236087
  return null;
236048
236088
  }
@@ -236059,7 +236099,7 @@ async function applyFeatureSetupChangesAtomically(cwd2, changes, io = {
236059
236099
  const staged = [];
236060
236100
  try {
236061
236101
  for (const change of changes) {
236062
- const absolute = resolve(cwd2, change.path);
236102
+ const absolute = resolve2(cwd2, change.path);
236063
236103
  const relativePath = safeRelative(cwd2, absolute);
236064
236104
  if (!relativePath)
236065
236105
  throw new Error(`Refusing to write outside the repository: ${change.path}`);
@@ -236169,7 +236209,7 @@ async function runFeatureRuntimeCheck(input) {
236169
236209
  }
236170
236210
  }
236171
236211
  async function runFeatureSetup(input) {
236172
- if (input.environment !== "test") {
236212
+ if (input.environment !== "test" && !(input.manifestOnly && !input.apply && !input.prompt)) {
236173
236213
  throw new Error(
236174
236214
  "Feature setup is Test-only. Review and promote Feature catalogs from Stripe Products."
236175
236215
  );
@@ -236190,12 +236230,18 @@ async function runFeatureSetup(input) {
236190
236230
  });
236191
236231
  const discovered = [];
236192
236232
  let scannedBytes = 0;
236233
+ let scannedFileCount = 0;
236234
+ let byteLimitReached = false;
236193
236235
  for (const file2 of files) {
236194
- const source = await readFile(file2, "utf8");
236236
+ const source = await readFile2(file2, "utf8");
236195
236237
  scannedBytes += Buffer.byteLength(source);
236196
- if (scannedBytes > 2e6) break;
236238
+ if (scannedBytes > 2e6) {
236239
+ byteLimitReached = true;
236240
+ break;
236241
+ }
236197
236242
  const path = safeRelative(input.cwd, file2);
236198
236243
  if (!path) continue;
236244
+ scannedFileCount++;
236199
236245
  discovered.push(...candidatesFromFile(path, source));
236200
236246
  }
236201
236247
  const uniqueCandidates = /* @__PURE__ */ new Map();
@@ -236208,7 +236254,7 @@ async function runFeatureSetup(input) {
236208
236254
  const candidates = [...uniqueCandidates.values()].slice(0, 200);
236209
236255
  const manifestPath = ".saasfunnels/catalog.yaml";
236210
236256
  const existingManifestSource = await readable(
236211
- resolve(input.cwd, manifestPath)
236257
+ resolve2(input.cwd, manifestPath)
236212
236258
  );
236213
236259
  const existingManifestValidation = existingManifestSource ? parseFeatureManifestSource(existingManifestSource) : null;
236214
236260
  if (existingManifestValidation && !existingManifestValidation.ok) {
@@ -236263,7 +236309,7 @@ async function runFeatureSetup(input) {
236263
236309
  const existingManifest = existingManifestSource;
236264
236310
  const constantsPath = ".saasfunnels/features.ts";
236265
236311
  const constantsContents = generatedConstants(manifest);
236266
- const existingConstants = await readable(resolve(input.cwd, constantsPath));
236312
+ const existingConstants = await readable(resolve2(input.cwd, constantsPath));
236267
236313
  const constantsSafe = existingConstants === null || existingConstants.startsWith(generatedMarker);
236268
236314
  const editInputs = [
236269
236315
  {
@@ -236373,7 +236419,8 @@ async function runFeatureSetup(input) {
236373
236419
  ok: validation.ok,
236374
236420
  reviewUrl,
236375
236421
  runtimeCheck: check2,
236376
- scannedFileCount: files.length,
236422
+ scannedFileCount,
236423
+ coverage: { complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 200, matchedFiles: files.length, scannedFiles: scannedFileCount, byteLimitReached, candidateLimitReached: uniqueCandidates.size > 200 },
236377
236424
  validation: { errors: validation.errors, ok: validation.ok }
236378
236425
  };
236379
236426
  }
@@ -236455,8 +236502,8 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
236455
236502
  }
236456
236503
 
236457
236504
  // 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";
236505
+ import { readFile as readFile3, readdir as readdir2, stat as stat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
236506
+ import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
236460
236507
  var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
236461
236508
  var maxPlanSourceFiles = 12;
236462
236509
  var maxPlanSourceCharacters = 512e3;
@@ -236513,7 +236560,7 @@ async function discoverPlanSourceCandidates(input) {
236513
236560
  const roots = input.roots ?? [".", "config", ...defaultRoots];
236514
236561
  const seen = /* @__PURE__ */ new Set();
236515
236562
  const candidates = [];
236516
- const queue = roots.map((root) => resolve2(input.cwd, root));
236563
+ const queue = roots.map((root) => resolve3(input.cwd, root));
236517
236564
  while (queue.length && candidates.length < 200) {
236518
236565
  const current = queue.shift();
236519
236566
  if (seen.has(current)) continue;
@@ -236537,7 +236584,7 @@ async function discoverPlanSourceCandidates(input) {
236537
236584
  const kind = planSourceKindByExtension.get(extname2(current).toLowerCase());
236538
236585
  if (!kind || !relativePath) continue;
236539
236586
  if (entryStat.size > maxPlanSourceCharacters) continue;
236540
- const contents = await readFile2(current, "utf8");
236587
+ const contents = await readFile3(current, "utf8");
236541
236588
  const hasStripePriceId = stripePriceIdPattern.test(contents);
236542
236589
  const named = looksLikePlanSource(relativePath);
236543
236590
  const looksPriced = billingIntervalPattern.test(contents) && chargeAmountPattern.test(contents);
@@ -236560,7 +236607,7 @@ async function discoverPlanSourceCandidates(input) {
236560
236607
  }
236561
236608
  async function readApprovedPlanSources(cwd2) {
236562
236609
  try {
236563
- const raw = await readFile2(resolve2(cwd2, planSourceApprovalPath), "utf8");
236610
+ const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
236564
236611
  const parsed = JSON.parse(raw);
236565
236612
  return Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [];
236566
236613
  } catch {
@@ -236568,7 +236615,7 @@ async function readApprovedPlanSources(cwd2) {
236568
236615
  }
236569
236616
  }
236570
236617
  async function writeApprovedPlanSources(cwd2, files) {
236571
- const target = resolve2(cwd2, planSourceApprovalPath);
236618
+ const target = resolve3(cwd2, planSourceApprovalPath);
236572
236619
  await mkdir2(dirname2(target), { recursive: true });
236573
236620
  await writeFile2(
236574
236621
  target,
@@ -236603,7 +236650,7 @@ async function buildPlanMappingHandoff(input) {
236603
236650
  if (!kind) {
236604
236651
  throw new Error(`${relativePath} is not a supported plan source type.`);
236605
236652
  }
236606
- const content = await readFile2(resolve2(input.cwd, relativePath), "utf8");
236653
+ const content = await readFile3(resolve3(input.cwd, relativePath), "utf8");
236607
236654
  if (content.length > maxPlanSourceCharacters) {
236608
236655
  throw new Error(
236609
236656
  `${relativePath} is ${content.length} characters; the limit is ${maxPlanSourceCharacters}.`
@@ -236619,8 +236666,8 @@ function planMappingRequestKey(input) {
236619
236666
  }
236620
236667
 
236621
236668
  // 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";
236669
+ import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
236670
+ import { extname as extname3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
236624
236671
  var maxPlanBranchFileCharacters = 2e6;
236625
236672
  var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
236626
236673
  var nonDefinitionPattern2 = /(^|\/)(__tests__|__mocks__|tests?|specs?|fixtures?|testing|mocks?|mock-data|stories|__stories__)(\/)|\.(test|spec|d|stories)\.[cm]?[jt]sx?$/i;
@@ -236768,7 +236815,7 @@ async function discoverPlanBranches(input) {
236768
236815
  const branches = [];
236769
236816
  const constants = /* @__PURE__ */ new Map();
236770
236817
  const pending = [];
236771
- const queue = roots.map((root) => resolve3(input.cwd, root));
236818
+ const queue = roots.map((root) => resolve4(input.cwd, root));
236772
236819
  while (queue.length) {
236773
236820
  const current = queue.shift();
236774
236821
  if (seen.has(current)) continue;
@@ -236791,7 +236838,7 @@ async function discoverPlanBranches(input) {
236791
236838
  if (!entryStat.isFile() || !relativePath) continue;
236792
236839
  if (!codeExtensions.has(extname3(current).toLowerCase())) continue;
236793
236840
  if (entryStat.size > maxPlanBranchFileCharacters) continue;
236794
- const lines = (await readFile3(current, "utf8")).split(/\r?\n/);
236841
+ const lines = (await readFile4(current, "utf8")).split(/\r?\n/);
236795
236842
  collectConstants(lines, constants);
236796
236843
  lines.forEach((line, lineIndex) => {
236797
236844
  comparisonPattern.lastIndex = 0;
@@ -236831,7 +236878,7 @@ async function discoverPlanBranches(input) {
236831
236878
  }
236832
236879
 
236833
236880
  // src/setup-run.ts
236834
- var exec = promisify(execFile);
236881
+ var exec2 = promisify2(execFile2);
236835
236882
  var hash2 = (text) => createHash3("sha256").update(text).digest("hex");
236836
236883
  var contextSchema = external_exports.object({
236837
236884
  workspaceId: external_exports.string(),
@@ -236889,6 +236936,7 @@ async function runGuidedSetup(options) {
236889
236936
  let progressState = "running";
236890
236937
  let progressFailure = null;
236891
236938
  try {
236939
+ options.progress?.("1/4 Checking app connection\u2026");
236892
236940
  let context = contextSchema.parse(
236893
236941
  await request("/api/developer-tools/setup/context")
236894
236942
  );
@@ -236896,8 +236944,14 @@ async function runGuidedSetup(options) {
236896
236944
  throw new Error(
236897
236945
  "Connect Stripe and create app credentials in Setup first."
236898
236946
  );
236899
- const head = (await exec("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
236900
- const diff = (await exec(
236947
+ try {
236948
+ await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
236949
+ await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
236950
+ } 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.");
236952
+ }
236953
+ const head = (await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
236954
+ const diff = (await exec2(
236901
236955
  "git",
236902
236956
  [
236903
236957
  "diff",
@@ -236910,7 +236964,7 @@ async function runGuidedSetup(options) {
236910
236964
  ],
236911
236965
  { cwd: options.cwd, maxBuffer: 8e6 }
236912
236966
  )).stdout;
236913
- const untracked = (await exec("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
236967
+ const untracked = (await exec2("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
236914
236968
  cwd: options.cwd
236915
236969
  })).stdout.split("\0").filter(
236916
236970
  (path) => path && !path.startsWith(".saasfunnels/") && !/(^|\/)(\.env|node_modules|dist|\.next)(\/|$|\.)/i.test(path)
@@ -236922,15 +236976,15 @@ async function runGuidedSetup(options) {
236922
236976
  const sourceHashes = await Promise.all(
236923
236977
  untracked.map(async (path) => [
236924
236978
  path,
236925
- hash2(await readFile4(join4(options.cwd, path), "utf8"))
236979
+ hash2(await readFile5(join4(options.cwd, path), "utf8"))
236926
236980
  ])
236927
236981
  );
236928
236982
  const revision = `${head}:${hash2(JSON.stringify([diff, sourceHashes]))}`;
236929
- const remote = (await exec("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
236983
+ const remote = (await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
236930
236984
  const repositoryKey = safeRepositoryKey(remote);
236931
236985
  const statePath = join4(options.cwd, ".saasfunnels", "setup-run.json");
236932
236986
  if (options.resume) {
236933
- const saved = JSON.parse(await readFile4(statePath, "utf8"));
236987
+ const saved = JSON.parse(await readFile5(statePath, "utf8"));
236934
236988
  if (saved.generation !== context.generation || saved.repositoryKey !== repositoryKey || saved.revision !== revision)
236935
236989
  throw new Error(
236936
236990
  "The app connection or revision changed. Run setup again without --resume."
@@ -236942,11 +236996,7 @@ async function runGuidedSetup(options) {
236942
236996
  )).trim()
236943
236997
  );
236944
236998
  if (!approved)
236945
- 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"
236949
- };
236999
+ return { exitCode: options.prompt ? 0 : 2, stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n", stderr: "" };
236950
237000
  run = (await request("/api/developer-tools/setup/runs", {
236951
237001
  generation: context.generation,
236952
237002
  repositoryKey,
@@ -237013,6 +237063,7 @@ async function runGuidedSetup(options) {
237013
237063
  void update(progressState, progressFailure).catch(() => {
237014
237064
  });
237015
237065
  }, 3e4);
237066
+ options.progress?.(run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026");
237016
237067
  if (!run.receipts.features) {
237017
237068
  await update("running");
237018
237069
  const scan = await runFeatureSetup({
@@ -237023,6 +237074,8 @@ async function runGuidedSetup(options) {
237023
237074
  manifestOnly: true,
237024
237075
  accept: ["all"]
237025
237076
  });
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.");
237026
237079
  if (!scan.ok)
237027
237080
  throw new Error(
237028
237081
  "Feature discovery needs attention. Run features setup to review the local findings."
@@ -237037,11 +237090,12 @@ async function runGuidedSetup(options) {
237037
237090
  );
237038
237091
  }
237039
237092
  stage = "plans";
237093
+ if (!context.catalogReady) options.progress?.("Waiting for your Stripe catalog. Setup can stay open while it imports\u2026");
237040
237094
  const catalogDeadline = Date.now() + 5 * 6e4;
237041
237095
  const generation = context.generation;
237042
237096
  while (!context.catalogReady && Date.now() < catalogDeadline) {
237043
237097
  await update("waiting", "catalog_pending");
237044
- await new Promise((resolve5) => setTimeout(resolve5, 1e4));
237098
+ await new Promise((resolve6) => setTimeout(resolve6, 1e4));
237045
237099
  context = contextSchema.parse(
237046
237100
  await request("/api/developer-tools/setup/context")
237047
237101
  );
@@ -237051,9 +237105,10 @@ async function runGuidedSetup(options) {
237051
237105
  if (!context.catalogReady)
237052
237106
  return {
237053
237107
  exitCode: 2,
237054
- stdout: "Stripe is still importing. Run setup run --resume --send when the catalog is ready.\n",
237108
+ stdout: "Stripe is still importing. Run npx --yes saasfunnels@latest setup --resume when the catalog is ready.\n",
237055
237109
  stderr: ""
237056
237110
  };
237111
+ options.progress?.(run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026");
237057
237112
  if (!run.receipts.plans) {
237058
237113
  await update("running");
237059
237114
  const candidates = await discoverPlanSourceCandidates({
@@ -237063,7 +237118,7 @@ async function runGuidedSetup(options) {
237063
237118
  if (candidates.length > 12) throw new Error("Too many sources");
237064
237119
  let manual = null;
237065
237120
  try {
237066
- manual = await readFile4(
237121
+ manual = await readFile5(
237067
237122
  join4(options.cwd, ".saasfunnels/setup-pricing.json"),
237068
237123
  "utf8"
237069
237124
  );
@@ -237073,7 +237128,7 @@ async function runGuidedSetup(options) {
237073
237128
  const discovered = manual !== null ? extractSetupPricing(manual, "json") : (await Promise.all(
237074
237129
  candidates.map(
237075
237130
  async (candidate) => extractSetupPricing(
237076
- await readFile4(join4(options.cwd, candidate.path), "utf8"),
237131
+ await readFile5(join4(options.cwd, candidate.path), "utf8"),
237077
237132
  candidate.kind
237078
237133
  )
237079
237134
  )
@@ -237092,13 +237147,14 @@ async function runGuidedSetup(options) {
237092
237147
  await update("waiting", "extraction_unsupported");
237093
237148
  return {
237094
237149
  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",
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",
237096
237151
  stderr: ""
237097
237152
  };
237098
237153
  }
237099
237154
  }
237100
237155
  stage = "branches";
237101
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");
237102
237158
  if (!run.receipts.branches) {
237103
237159
  await update("running");
237104
237160
  const branches = planValues.length ? await discoverPlanBranches({
@@ -237129,14 +237185,14 @@ async function runGuidedSetup(options) {
237129
237185
  }
237130
237186
  return {
237131
237187
  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",
237188
+ 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
237189
  stderr: ""
237134
237190
  };
237135
237191
  } catch (error51) {
237136
237192
  clearInterval(timer);
237137
237193
  await reportFailure?.().catch(() => {
237138
237194
  });
237139
- const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery)/.test(
237195
+ const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Repository setup)/.test(
237140
237196
  error51.message
237141
237197
  ) ? error51.message : "Setup could not finish. Check local repository access and developer credentials, then resume.";
237142
237198
  return { exitCode: 2, stdout: "", stderr: safe + "\n" };
@@ -237187,11 +237243,11 @@ async function submitApprovedSetupPlans(input) {
237187
237243
  }
237188
237244
 
237189
237245
  // src/cli.ts
237190
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
237246
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
237191
237247
 
237192
237248
  // src/identity.ts
237193
237249
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
237194
- var SAASFUNNELS_CLI_VERSION = "0.3.0";
237250
+ var SAASFUNNELS_CLI_VERSION = "0.3.1";
237195
237251
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
237196
237252
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
237197
237253
  var SAASFUNNELS_ENV = Object.freeze({
@@ -238979,7 +239035,8 @@ function usage() {
238979
239035
  return `${SAASFUNNELS_PRODUCT_NAME} CLI
238980
239036
 
238981
239037
  Usage:
238982
- ${SAASFUNNELS_CLI_NAME} setup run [--resume] [--send]
239038
+ ${SAASFUNNELS_CLI_NAME} setup [--resume] [--env-file <path>]
239039
+ ${SAASFUNNELS_CLI_NAME} setup run [--resume] [--send] [--non-interactive]
238983
239040
  ${SAASFUNNELS_CLI_NAME} init [--api-base-url <url>] [--force]
238984
239041
  ${SAASFUNNELS_CLI_NAME} agent install --target codex|claude-code|cursor|markdown [--endpoint <url>]
238985
239042
  ${SAASFUNNELS_CLI_NAME} events validate <file> [--source direct|posthog|segment] [--json]
@@ -239016,7 +239073,7 @@ function filesystem(options) {
239016
239073
  mkdir: async (path, mkdirOptions) => {
239017
239074
  await mkdir4(path, mkdirOptions);
239018
239075
  },
239019
- readFile: readFile5,
239076
+ readFile: readFile6,
239020
239077
  writeFile: writeFile4
239021
239078
  };
239022
239079
  }
@@ -239047,7 +239104,7 @@ function sourceFlag(flags) {
239047
239104
  }
239048
239105
  async function readJsonFile(filePath, options) {
239049
239106
  const raw = await filesystem(options).readFile(
239050
- resolve4(cwd(options), filePath),
239107
+ resolve5(cwd(options), filePath),
239051
239108
  "utf8"
239052
239109
  );
239053
239110
  return JSON.parse(raw);
@@ -239236,7 +239293,7 @@ async function commandFeatures(args, flags, options) {
239236
239293
  }
239237
239294
  const discoveryRoots = (flagString(flags, "discovery-roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean);
239238
239295
  const source = await filesystem(options).readFile(
239239
- resolve4(cwd(options), path),
239296
+ resolve5(cwd(options), path),
239240
239297
  "utf8"
239241
239298
  );
239242
239299
  const validation = parseFeatureManifestSource(source);
@@ -239252,7 +239309,7 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
239252
239309
  let sdkVersions = [];
239253
239310
  try {
239254
239311
  const packageSource = await filesystem(options).readFile(
239255
- resolve4(cwd(options), "package.json"),
239312
+ resolve5(cwd(options), "package.json"),
239256
239313
  "utf8"
239257
239314
  );
239258
239315
  sdkVersions = featureSdkVersionsFromPackageJson(
@@ -239323,7 +239380,7 @@ async function commandCatalog(args, flags, options) {
239323
239380
  if (action === "validate") {
239324
239381
  const path = args[1] ?? ".saasfunnels/catalog.yaml";
239325
239382
  const source = await filesystem(options).readFile(
239326
- resolve4(cwd(options), path),
239383
+ resolve5(cwd(options), path),
239327
239384
  "utf8"
239328
239385
  );
239329
239386
  const validation = parseFeatureManifestSource(source);
@@ -239573,7 +239630,7 @@ async function commandVerify(flags, options) {
239573
239630
  if (catalogPath) {
239574
239631
  try {
239575
239632
  const source = await filesystem(options).readFile(
239576
- resolve4(cwd(options), catalogPath),
239633
+ resolve5(cwd(options), catalogPath),
239577
239634
  "utf8"
239578
239635
  );
239579
239636
  const validation = parseFeatureManifestSource(source);
@@ -239916,7 +239973,15 @@ async function runSaaSFunnelsCli(argv2, options = {}) {
239916
239973
  if (!command || command === "help" || command === "--help")
239917
239974
  return result(0, usage());
239918
239975
  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 });
239976
+ if (command === "setup") {
239977
+ 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";
239978
+ if (hasFlag(parsed.flags, "help") || args[0] === "help") return result(0, help);
239979
+ if (args.length > 1 || args[0] && args[0] !== "run") return result(2, "", help);
239980
+ const interactive = !jsonMode(parsed.flags) && !hasFlag(parsed.flags, "non-interactive");
239981
+ 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 });
239982
+ if (!access2.key) return result(2, "", access2.error + "\n");
239983
+ 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 });
239984
+ }
239920
239985
  if (command === "agent")
239921
239986
  return await commandAgentInstall(args, parsed.flags, options);
239922
239987
  if (command === "events")
@@ -239944,13 +240009,14 @@ ${usage()}`);
239944
240009
 
239945
240010
  // src/saasfunnels.ts
239946
240011
  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] ?? ""));
240012
+ 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
240013
  var prompts = interactiveFeatureSetup ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
239949
240014
  var result2;
239950
240015
  try {
239951
240016
  result2 = await runSaaSFunnelsCli(argv, {
239952
240017
  cwd: process.cwd(),
239953
240018
  env: process.env,
240019
+ progress: (message) => process.stdout.write(message + "\n"),
239954
240020
  prompt: prompts ? (message) => prompts.question(message) : void 0
239955
240021
  });
239956
240022
  } finally {
@@ -121,6 +121,13 @@ export type FeatureSetupResult = {
121
121
  state: "failed" | "passed" | "skipped";
122
122
  };
123
123
  scannedFileCount: number;
124
+ coverage: {
125
+ complete: boolean;
126
+ matchedFiles: number;
127
+ scannedFiles: number;
128
+ byteLimitReached: boolean;
129
+ candidateLimitReached: boolean;
130
+ };
124
131
  validation: {
125
132
  errors: string[];
126
133
  ok: boolean;
@@ -1,5 +1,5 @@
1
1
  export declare const SAASFUNNELS_CLI_NAME = "saasfunnels";
2
- export declare const SAASFUNNELS_CLI_VERSION = "0.3.0";
2
+ export declare const SAASFUNNELS_CLI_VERSION = "0.3.1";
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.0";
80
+ declare const mcpServerVersion = "0.3.1";
81
81
  export declare function saasFunnelsMcpResources(): McpResourceDefinition[];
82
82
  export declare function readSaaSFunnelsMcpResource(uri: string, options?: SaaSFunnelsMcpOptions): string;
83
83
  export declare function saasFunnelsMcpToolDefinitions({ allowFunnelTests, allowFunnelWrites, allowTestWrites, includeHostedAccountTools, }?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasfunnels",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "description": "SaaSFunnels CLI and local stdio MCP server",
6
6
  "author": "SaaSFunnels",