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