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