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.
- package/README.md +12 -4
- package/dist/library.js +1 -1
- package/dist/saasfunnels.js +479 -183
- package/dist/types/feature-setup.d.ts +9 -0
- package/dist/types/identity.d.ts +1 -1
- package/dist/types/mcp.d.ts +1 -1
- package/package.json +1 -1
package/dist/saasfunnels.js
CHANGED
|
@@ -8649,7 +8649,7 @@ ${lanes.join("\n")}
|
|
|
8649
8649
|
writeOutputIsTTY() {
|
|
8650
8650
|
return process.stdout.isTTY;
|
|
8651
8651
|
},
|
|
8652
|
-
readFile:
|
|
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
|
|
8694
|
-
if (
|
|
8695
|
-
return
|
|
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
|
|
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
|
|
8913
|
+
let stat;
|
|
8914
8914
|
if (typeof dirent === "string" || dirent.isSymbolicLink()) {
|
|
8915
8915
|
const name = combinePaths(path, entry);
|
|
8916
|
-
|
|
8917
|
-
if (!
|
|
8916
|
+
stat = statSync(name);
|
|
8917
|
+
if (!stat) {
|
|
8918
8918
|
continue;
|
|
8919
8919
|
}
|
|
8920
8920
|
} else {
|
|
8921
|
-
|
|
8921
|
+
stat = dirent;
|
|
8922
8922
|
}
|
|
8923
|
-
if (
|
|
8923
|
+
if (stat.isFile()) {
|
|
8924
8924
|
files.push(entry);
|
|
8925
|
-
} else if (
|
|
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
|
|
8941
|
-
if (!
|
|
8940
|
+
const stat = statSync(path);
|
|
8941
|
+
if (!stat) {
|
|
8942
8942
|
return false;
|
|
8943
8943
|
}
|
|
8944
8944
|
switch (entryKind) {
|
|
8945
8945
|
case 0:
|
|
8946
|
-
return
|
|
8946
|
+
return stat.isFile();
|
|
8947
8947
|
case 1:
|
|
8948
|
-
return
|
|
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,
|
|
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,
|
|
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,
|
|
45693
|
-
return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine,
|
|
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,
|
|
45776
|
-
const textOrDiagnostic = tryReadFile(fileName,
|
|
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,
|
|
45792
|
-
const textOrDiagnostic = tryReadFile(fileName,
|
|
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,
|
|
45795
|
+
function tryReadFile(fileName, readFile7) {
|
|
45796
45796
|
let text;
|
|
45797
45797
|
try {
|
|
45798
|
-
text =
|
|
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
|
|
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
|
-
|
|
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()]),
|
|
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(
|
|
135902
|
+
function createGetSourceFile(readFile7, setParentNodes) {
|
|
135903
135903
|
return (fileName, languageVersionOrOptions, onError) => {
|
|
135904
135904
|
let text;
|
|
135905
135905
|
try {
|
|
135906
135906
|
mark("beforeIORead");
|
|
135907
|
-
text =
|
|
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
|
|
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:
|
|
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((
|
|
211293
|
-
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve:
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 >
|
|
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
|
|
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)
|
|
218943
|
-
|
|
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.
|
|
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
|
|
218951
|
-
|
|
218952
|
-
|
|
218953
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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 (!
|
|
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
|
|
218987
|
-
|
|
218988
|
-
|
|
218989
|
-
|
|
218990
|
-
|
|
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
|
-
|
|
218994
|
-
|
|
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
|
|
219001
|
-
|
|
219002
|
-
|
|
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
|
|
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
|
|
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)
|
|
219020
|
-
|
|
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
|
|
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
|
-
|
|
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 = [
|
|
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(
|
|
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) =>
|
|
235820
|
-
|
|
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
|
|
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() &&
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
236232
|
+
const absolute = resolve2(cwd2, change.path);
|
|
236063
236233
|
const relativePath = safeRelative(cwd2, absolute);
|
|
236064
236234
|
if (!relativePath)
|
|
236065
|
-
throw new Error(
|
|
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, {
|
|
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(
|
|
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
|
|
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))
|
|
236385
|
+
if (!uniqueCandidates.has(identity))
|
|
236386
|
+
uniqueCandidates.set(identity, candidate);
|
|
236207
236387
|
}
|
|
236208
|
-
const candidates = [...uniqueCandidates.values()]
|
|
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
|
-
|
|
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))
|
|
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 = {
|
|
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(
|
|
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
|
|
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
|
|
236459
|
-
import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as
|
|
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) =>
|
|
236517
|
-
while (queue.length
|
|
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
|
|
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(
|
|
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 >
|
|
236540
|
-
|
|
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
|
|
236764
|
+
return [...confirmed, ...namedOnly];
|
|
236560
236765
|
}
|
|
236561
236766
|
async function readApprovedPlanSources(cwd2) {
|
|
236562
236767
|
try {
|
|
236563
|
-
const raw = await
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
236623
|
-
import { extname as extname3, join as join3, relative as relative3, resolve as
|
|
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: [
|
|
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) =>
|
|
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
|
|
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(
|
|
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)
|
|
236794
|
-
|
|
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
|
|
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
|
-
|
|
236900
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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")
|
|
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((
|
|
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
|
|
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
|
-
|
|
237064
|
-
|
|
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
|
-
|
|
237067
|
-
join4(options.cwd,
|
|
237068
|
-
|
|
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
|
|
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
|
-
|
|
237074
|
-
|
|
237075
|
-
|
|
237076
|
-
|
|
237077
|
-
|
|
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
|
-
|
|
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:
|
|
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 = [
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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 {
|