saasfunnels 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/library.js +1 -1
- package/dist/saasfunnels.js +145 -79
- package/dist/types/feature-setup.d.ts +7 -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
|
@@ -10,12 +10,16 @@ This repository contains only the MIT-licensed CLI and its bounded runtime contr
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Start in your app repository:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
16
|
+
npx --yes saasfunnels@latest setup
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
The explicit version tag avoids running an older project-local CLI. Setup asks where developer access is saved (Doppler or `.env.local`) when needed, requests approval before uploading findings, and shows each discovery stage. It never prints keys, rewrites env files, deploys your app, or approves inferred plans. Connect Stripe and create app credentials in the web Setup first. `setup run` remains a compatible entry point.
|
|
20
|
+
|
|
21
|
+
For scripts, use `--env-file .env.local --send --non-interactive`, or inject `SAASFUNNELS_API_KEY` from your secret manager. Without explicit approval, non-interactive runs stop without uploading. Run `setup --help` for setup-only guidance. Runtime implementation and payment verification remain in the web Setup.
|
|
22
|
+
|
|
19
23
|
## Use
|
|
20
24
|
|
|
21
25
|
```bash
|
|
@@ -51,7 +55,7 @@ approved set is reviewable.
|
|
|
51
55
|
discovery-roots: app,lib
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
On a pull request the
|
|
58
|
+
The Action uploads an existing feature manifest; it does not freshly scan application source. On a pull request the upload is a `candidate`: the result is compared
|
|
55
59
|
against the branch's baseline and discarded. Only the default branch advances
|
|
56
60
|
the lineage. Changing `discovery-roots` starts a new lineage, so drift is
|
|
57
61
|
measured against a comparable scan rather than a wider or narrower one.
|
package/dist/library.js
CHANGED
|
@@ -24264,7 +24264,7 @@ function validateFeatureCatalogManifest(value) {
|
|
|
24264
24264
|
|
|
24265
24265
|
// src/identity.ts
|
|
24266
24266
|
var SAASFUNNELS_CLI_NAME = "saasfunnels";
|
|
24267
|
-
var SAASFUNNELS_CLI_VERSION = "0.3.
|
|
24267
|
+
var SAASFUNNELS_CLI_VERSION = "0.3.1";
|
|
24268
24268
|
var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
|
|
24269
24269
|
var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
|
|
24270
24270
|
var SAASFUNNELS_ENV = Object.freeze({
|
package/dist/saasfunnels.js
CHANGED
|
@@ -8649,7 +8649,7 @@ ${lanes.join("\n")}
|
|
|
8649
8649
|
writeOutputIsTTY() {
|
|
8650
8650
|
return process.stdout.isTTY;
|
|
8651
8651
|
},
|
|
8652
|
-
readFile:
|
|
8652
|
+
readFile: readFile7,
|
|
8653
8653
|
writeFile: writeFile22,
|
|
8654
8654
|
watchFile: watchFile2,
|
|
8655
8655
|
watchDirectory,
|
|
@@ -8855,7 +8855,7 @@ ${lanes.join("\n")}
|
|
|
8855
8855
|
callback
|
|
8856
8856
|
);
|
|
8857
8857
|
}
|
|
8858
|
-
function
|
|
8858
|
+
function readFile7(fileName, _encoding) {
|
|
8859
8859
|
let buffer;
|
|
8860
8860
|
try {
|
|
8861
8861
|
buffer = _fs.readFileSync(fileName);
|
|
@@ -45538,7 +45538,7 @@ ${lanes.join("\n")}
|
|
|
45538
45538
|
const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName);
|
|
45539
45539
|
return possibleOption ? createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption);
|
|
45540
45540
|
}
|
|
45541
|
-
function parseCommandLineWorker(diagnostics, commandLine,
|
|
45541
|
+
function parseCommandLineWorker(diagnostics, commandLine, readFile7) {
|
|
45542
45542
|
const options = {};
|
|
45543
45543
|
let watchOptions;
|
|
45544
45544
|
const fileNames = [];
|
|
@@ -45586,7 +45586,7 @@ ${lanes.join("\n")}
|
|
|
45586
45586
|
}
|
|
45587
45587
|
}
|
|
45588
45588
|
function parseResponseFile(fileName) {
|
|
45589
|
-
const text = tryReadFile(fileName,
|
|
45589
|
+
const text = tryReadFile(fileName, readFile7 || ((fileName2) => sys.readFile(fileName2)));
|
|
45590
45590
|
if (!isString(text)) {
|
|
45591
45591
|
errors.push(text);
|
|
45592
45592
|
return;
|
|
@@ -45689,8 +45689,8 @@ ${lanes.join("\n")}
|
|
|
45689
45689
|
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
|
|
45690
45690
|
optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument
|
|
45691
45691
|
};
|
|
45692
|
-
function parseCommandLine(commandLine,
|
|
45693
|
-
return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine,
|
|
45692
|
+
function parseCommandLine(commandLine, readFile7) {
|
|
45693
|
+
return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile7);
|
|
45694
45694
|
}
|
|
45695
45695
|
function getOptionFromName(optionName, allowShort) {
|
|
45696
45696
|
return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort);
|
|
@@ -45772,8 +45772,8 @@ ${lanes.join("\n")}
|
|
|
45772
45772
|
watchOptionsToExtend
|
|
45773
45773
|
);
|
|
45774
45774
|
}
|
|
45775
|
-
function readConfigFile(fileName,
|
|
45776
|
-
const textOrDiagnostic = tryReadFile(fileName,
|
|
45775
|
+
function readConfigFile(fileName, readFile7) {
|
|
45776
|
+
const textOrDiagnostic = tryReadFile(fileName, readFile7);
|
|
45777
45777
|
return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
|
|
45778
45778
|
}
|
|
45779
45779
|
function parseConfigFileTextToJson(fileName, jsonText) {
|
|
@@ -45788,14 +45788,14 @@ ${lanes.join("\n")}
|
|
|
45788
45788
|
error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : void 0
|
|
45789
45789
|
};
|
|
45790
45790
|
}
|
|
45791
|
-
function readJsonConfigFile(fileName,
|
|
45792
|
-
const textOrDiagnostic = tryReadFile(fileName,
|
|
45791
|
+
function readJsonConfigFile(fileName, readFile7) {
|
|
45792
|
+
const textOrDiagnostic = tryReadFile(fileName, readFile7);
|
|
45793
45793
|
return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] };
|
|
45794
45794
|
}
|
|
45795
|
-
function tryReadFile(fileName,
|
|
45795
|
+
function tryReadFile(fileName, readFile7) {
|
|
45796
45796
|
let text;
|
|
45797
45797
|
try {
|
|
45798
|
-
text =
|
|
45798
|
+
text = readFile7(fileName);
|
|
45799
45799
|
} catch (e) {
|
|
45800
45800
|
return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message);
|
|
45801
45801
|
}
|
|
@@ -124332,7 +124332,7 @@ ${lanes.join("\n")}
|
|
|
124332
124332
|
}
|
|
124333
124333
|
}
|
|
124334
124334
|
function createImportCallExpressionAMD(arg, containsLexicalThis) {
|
|
124335
|
-
const
|
|
124335
|
+
const resolve6 = factory2.createUniqueName("resolve");
|
|
124336
124336
|
const reject = factory2.createUniqueName("reject");
|
|
124337
124337
|
const parameters = [
|
|
124338
124338
|
factory2.createParameterDeclaration(
|
|
@@ -124341,7 +124341,7 @@ ${lanes.join("\n")}
|
|
|
124341
124341
|
/*dotDotDotToken*/
|
|
124342
124342
|
void 0,
|
|
124343
124343
|
/*name*/
|
|
124344
|
-
|
|
124344
|
+
resolve6
|
|
124345
124345
|
),
|
|
124346
124346
|
factory2.createParameterDeclaration(
|
|
124347
124347
|
/*modifiers*/
|
|
@@ -124358,7 +124358,7 @@ ${lanes.join("\n")}
|
|
|
124358
124358
|
factory2.createIdentifier("require"),
|
|
124359
124359
|
/*typeArguments*/
|
|
124360
124360
|
void 0,
|
|
124361
|
-
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]),
|
|
124361
|
+
[factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve6, reject]
|
|
124362
124362
|
)
|
|
124363
124363
|
)
|
|
124364
124364
|
]);
|
|
@@ -135899,12 +135899,12 @@ ${lanes.join("\n")}
|
|
|
135899
135899
|
function createCompilerHost(options, setParentNodes) {
|
|
135900
135900
|
return createCompilerHostWorker(options, setParentNodes);
|
|
135901
135901
|
}
|
|
135902
|
-
function createGetSourceFile(
|
|
135902
|
+
function createGetSourceFile(readFile7, setParentNodes) {
|
|
135903
135903
|
return (fileName, languageVersionOrOptions, onError) => {
|
|
135904
135904
|
let text;
|
|
135905
135905
|
try {
|
|
135906
135906
|
mark("beforeIORead");
|
|
135907
|
-
text =
|
|
135907
|
+
text = readFile7(fileName);
|
|
135908
135908
|
mark("afterIORead");
|
|
135909
135909
|
measure("I/O Read", "beforeIORead", "afterIORead");
|
|
135910
135910
|
} catch (e) {
|
|
@@ -136808,7 +136808,7 @@ ${lanes.join("\n")}
|
|
|
136808
136808
|
getRedirectFromOutput,
|
|
136809
136809
|
forEachResolvedProjectReference: forEachResolvedProjectReference2
|
|
136810
136810
|
});
|
|
136811
|
-
const
|
|
136811
|
+
const readFile7 = host.readFile.bind(host);
|
|
136812
136812
|
(_e = tracing) == null ? void 0 : _e.push(tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram });
|
|
136813
136813
|
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
|
|
136814
136814
|
(_f = tracing) == null ? void 0 : _f.pop();
|
|
@@ -137029,7 +137029,7 @@ ${lanes.join("\n")}
|
|
|
137029
137029
|
shouldTransformImportCall,
|
|
137030
137030
|
emitBuildInfo,
|
|
137031
137031
|
fileExists,
|
|
137032
|
-
readFile:
|
|
137032
|
+
readFile: readFile7,
|
|
137033
137033
|
directoryExists,
|
|
137034
137034
|
getSymlinkCache,
|
|
137035
137035
|
realpath: (_o = host.realpath) == null ? void 0 : _o.bind(host),
|
|
@@ -211289,8 +211289,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|
|
211289
211289
|
installPackage(options) {
|
|
211290
211290
|
this.packageInstallId++;
|
|
211291
211291
|
const request = { kind: "installPackage", ...options, id: this.packageInstallId };
|
|
211292
|
-
const promise2 = new Promise((
|
|
211293
|
-
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve:
|
|
211292
|
+
const promise2 = new Promise((resolve6, reject) => {
|
|
211293
|
+
(this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve6, reject });
|
|
211294
211294
|
});
|
|
211295
211295
|
this.installer.send(request);
|
|
211296
211296
|
return promise2;
|
|
@@ -218902,8 +218902,48 @@ var require_dist = __commonJS({
|
|
|
218902
218902
|
// src/saasfunnels.ts
|
|
218903
218903
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
218904
218904
|
|
|
218905
|
+
// src/setup-access.ts
|
|
218906
|
+
import { execFile } from "node:child_process";
|
|
218907
|
+
import { readFile } from "node:fs/promises";
|
|
218908
|
+
import { resolve } from "node:path";
|
|
218909
|
+
import { parseEnv, promisify } from "node:util";
|
|
218910
|
+
var exec = promisify(execFile);
|
|
218911
|
+
async function resolveSetupAccess(options) {
|
|
218912
|
+
if (options.key?.trim()) return { key: options.key.trim() };
|
|
218913
|
+
let file2 = options.envFile;
|
|
218914
|
+
if (!file2 && options.prompt) {
|
|
218915
|
+
const choice = (await options.prompt(
|
|
218916
|
+
"Developer access is not loaded. Where did you save SAASFUNNELS_API_KEY?\n 1. Doppler (this folder\u2019s configured project)\n 2. .env.local in this app\n 3. I haven\u2019t saved it yet\nChoose [1/2/3]: "
|
|
218917
|
+
)).trim();
|
|
218918
|
+
if (choice === "1") {
|
|
218919
|
+
try {
|
|
218920
|
+
const key2 = options.loadDoppler ? await options.loadDoppler() : (await exec("doppler", ["secrets", "get", "SAASFUNNELS_API_KEY", "--plain"], {
|
|
218921
|
+
cwd: options.cwd,
|
|
218922
|
+
timeout: 3e4,
|
|
218923
|
+
maxBuffer: 64e3
|
|
218924
|
+
})).stdout;
|
|
218925
|
+
if (key2.trim()) return { key: key2.trim() };
|
|
218926
|
+
} catch {
|
|
218927
|
+
}
|
|
218928
|
+
return { error: "Couldn\u2019t load developer access from Doppler. Check this folder\u2019s Doppler project/config and save SAASFUNNELS_API_KEY there, then rerun this command." };
|
|
218929
|
+
}
|
|
218930
|
+
if (choice === "2") file2 = ".env.local";
|
|
218931
|
+
}
|
|
218932
|
+
if (file2) {
|
|
218933
|
+
try {
|
|
218934
|
+
const source = await readFile(resolve(options.cwd, file2), "utf8");
|
|
218935
|
+
if (source.length > 64e3) throw new Error("Oversized env file");
|
|
218936
|
+
const key2 = parseEnv(source).SAASFUNNELS_API_KEY?.trim();
|
|
218937
|
+
if (key2) return { key: key2 };
|
|
218938
|
+
} catch {
|
|
218939
|
+
}
|
|
218940
|
+
return { error: "Couldn\u2019t find SAASFUNNELS_API_KEY in the selected env file. Save the developer access key there, then rerun this command. No file was changed." };
|
|
218941
|
+
}
|
|
218942
|
+
return { error: "Open SaaSFunnels \u2192 Setup \u2192 Environment values \u2192 Developer setup access. Save SAASFUNNELS_API_KEY in your secret manager or .env.local, then run npx --yes saasfunnels@latest setup again. Never paste the key into a command. For automation, load it into the process or use --env-file .env.local." };
|
|
218943
|
+
}
|
|
218944
|
+
|
|
218905
218945
|
// src/cli.ts
|
|
218906
|
-
import { dirname as dirname3, join as join5, resolve as
|
|
218946
|
+
import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
|
|
218907
218947
|
|
|
218908
218948
|
// src/setup-pricing.ts
|
|
218909
218949
|
var import_typescript = __toESM(require_typescript(), 1);
|
|
@@ -219023,9 +219063,9 @@ function extractSetupPricing(source, kind) {
|
|
|
219023
219063
|
|
|
219024
219064
|
// src/setup-run.ts
|
|
219025
219065
|
import { createHash as createHash3 } from "node:crypto";
|
|
219026
|
-
import { execFile } from "node:child_process";
|
|
219027
|
-
import { promisify } from "node:util";
|
|
219028
|
-
import { readFile as
|
|
219066
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
219067
|
+
import { promisify as promisify2 } from "node:util";
|
|
219068
|
+
import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
|
|
219029
219069
|
import { join as join4 } from "node:path";
|
|
219030
219070
|
|
|
219031
219071
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -233548,14 +233588,14 @@ import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
|
233548
233588
|
import {
|
|
233549
233589
|
access,
|
|
233550
233590
|
mkdir,
|
|
233551
|
-
readFile,
|
|
233591
|
+
readFile as readFile2,
|
|
233552
233592
|
readdir,
|
|
233553
233593
|
rename,
|
|
233554
233594
|
rm,
|
|
233555
233595
|
stat,
|
|
233556
233596
|
writeFile
|
|
233557
233597
|
} from "node:fs/promises";
|
|
233558
|
-
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
233598
|
+
import { dirname, extname, join, relative, resolve as resolve2 } from "node:path";
|
|
233559
233599
|
|
|
233560
233600
|
// src/runtime-contracts.js
|
|
233561
233601
|
import { createHash } from "node:crypto";
|
|
@@ -235816,7 +235856,7 @@ function allowedPath(path, excludes) {
|
|
|
235816
235856
|
}
|
|
235817
235857
|
async function walkFiles(input) {
|
|
235818
235858
|
const files = [];
|
|
235819
|
-
const queue = input.roots.map((root) =>
|
|
235859
|
+
const queue = input.roots.map((root) => resolve2(input.cwd, root));
|
|
235820
235860
|
while (queue.length && files.length < 500) {
|
|
235821
235861
|
const current = queue.shift();
|
|
235822
235862
|
const relativePath = safeRelative(input.cwd, current);
|
|
@@ -235938,7 +235978,7 @@ function candidatesFromFile(path, source) {
|
|
|
235938
235978
|
async function detectFramework(cwd2, requestedRoots) {
|
|
235939
235979
|
let packageJson = {};
|
|
235940
235980
|
try {
|
|
235941
|
-
packageJson = JSON.parse(await
|
|
235981
|
+
packageJson = JSON.parse(await readFile2(join(cwd2, "package.json"), "utf8"));
|
|
235942
235982
|
} catch {
|
|
235943
235983
|
}
|
|
235944
235984
|
const dependencies = {
|
|
@@ -235950,7 +235990,7 @@ async function detectFramework(cwd2, requestedRoots) {
|
|
|
235950
235990
|
const roots = (requestedRoots?.length ? requestedRoots : defaultRoots).filter((root) => !root.startsWith("../") && !root.startsWith("/"));
|
|
235951
235991
|
const existingRoots = [];
|
|
235952
235992
|
for (const root of roots) {
|
|
235953
|
-
if (await exists(
|
|
235993
|
+
if (await exists(resolve2(cwd2, root))) existingRoots.push(root);
|
|
235954
235994
|
}
|
|
235955
235995
|
return {
|
|
235956
235996
|
framework,
|
|
@@ -236042,7 +236082,7 @@ export type SaaSFunnelsFeatureKey =
|
|
|
236042
236082
|
}
|
|
236043
236083
|
async function readable(path) {
|
|
236044
236084
|
try {
|
|
236045
|
-
return await
|
|
236085
|
+
return await readFile2(path, "utf8");
|
|
236046
236086
|
} catch {
|
|
236047
236087
|
return null;
|
|
236048
236088
|
}
|
|
@@ -236059,7 +236099,7 @@ async function applyFeatureSetupChangesAtomically(cwd2, changes, io = {
|
|
|
236059
236099
|
const staged = [];
|
|
236060
236100
|
try {
|
|
236061
236101
|
for (const change of changes) {
|
|
236062
|
-
const absolute =
|
|
236102
|
+
const absolute = resolve2(cwd2, change.path);
|
|
236063
236103
|
const relativePath = safeRelative(cwd2, absolute);
|
|
236064
236104
|
if (!relativePath)
|
|
236065
236105
|
throw new Error(`Refusing to write outside the repository: ${change.path}`);
|
|
@@ -236169,7 +236209,7 @@ async function runFeatureRuntimeCheck(input) {
|
|
|
236169
236209
|
}
|
|
236170
236210
|
}
|
|
236171
236211
|
async function runFeatureSetup(input) {
|
|
236172
|
-
if (input.environment !== "test") {
|
|
236212
|
+
if (input.environment !== "test" && !(input.manifestOnly && !input.apply && !input.prompt)) {
|
|
236173
236213
|
throw new Error(
|
|
236174
236214
|
"Feature setup is Test-only. Review and promote Feature catalogs from Stripe Products."
|
|
236175
236215
|
);
|
|
@@ -236190,12 +236230,18 @@ async function runFeatureSetup(input) {
|
|
|
236190
236230
|
});
|
|
236191
236231
|
const discovered = [];
|
|
236192
236232
|
let scannedBytes = 0;
|
|
236233
|
+
let scannedFileCount = 0;
|
|
236234
|
+
let byteLimitReached = false;
|
|
236193
236235
|
for (const file2 of files) {
|
|
236194
|
-
const source = await
|
|
236236
|
+
const source = await readFile2(file2, "utf8");
|
|
236195
236237
|
scannedBytes += Buffer.byteLength(source);
|
|
236196
|
-
if (scannedBytes > 2e6)
|
|
236238
|
+
if (scannedBytes > 2e6) {
|
|
236239
|
+
byteLimitReached = true;
|
|
236240
|
+
break;
|
|
236241
|
+
}
|
|
236197
236242
|
const path = safeRelative(input.cwd, file2);
|
|
236198
236243
|
if (!path) continue;
|
|
236244
|
+
scannedFileCount++;
|
|
236199
236245
|
discovered.push(...candidatesFromFile(path, source));
|
|
236200
236246
|
}
|
|
236201
236247
|
const uniqueCandidates = /* @__PURE__ */ new Map();
|
|
@@ -236208,7 +236254,7 @@ async function runFeatureSetup(input) {
|
|
|
236208
236254
|
const candidates = [...uniqueCandidates.values()].slice(0, 200);
|
|
236209
236255
|
const manifestPath = ".saasfunnels/catalog.yaml";
|
|
236210
236256
|
const existingManifestSource = await readable(
|
|
236211
|
-
|
|
236257
|
+
resolve2(input.cwd, manifestPath)
|
|
236212
236258
|
);
|
|
236213
236259
|
const existingManifestValidation = existingManifestSource ? parseFeatureManifestSource(existingManifestSource) : null;
|
|
236214
236260
|
if (existingManifestValidation && !existingManifestValidation.ok) {
|
|
@@ -236263,7 +236309,7 @@ async function runFeatureSetup(input) {
|
|
|
236263
236309
|
const existingManifest = existingManifestSource;
|
|
236264
236310
|
const constantsPath = ".saasfunnels/features.ts";
|
|
236265
236311
|
const constantsContents = generatedConstants(manifest);
|
|
236266
|
-
const existingConstants = await readable(
|
|
236312
|
+
const existingConstants = await readable(resolve2(input.cwd, constantsPath));
|
|
236267
236313
|
const constantsSafe = existingConstants === null || existingConstants.startsWith(generatedMarker);
|
|
236268
236314
|
const editInputs = [
|
|
236269
236315
|
{
|
|
@@ -236373,7 +236419,8 @@ async function runFeatureSetup(input) {
|
|
|
236373
236419
|
ok: validation.ok,
|
|
236374
236420
|
reviewUrl,
|
|
236375
236421
|
runtimeCheck: check2,
|
|
236376
|
-
scannedFileCount
|
|
236422
|
+
scannedFileCount,
|
|
236423
|
+
coverage: { complete: scannedFileCount > 0 && !byteLimitReached && uniqueCandidates.size <= 200, matchedFiles: files.length, scannedFiles: scannedFileCount, byteLimitReached, candidateLimitReached: uniqueCandidates.size > 200 },
|
|
236377
236424
|
validation: { errors: validation.errors, ok: validation.ok }
|
|
236378
236425
|
};
|
|
236379
236426
|
}
|
|
@@ -236455,8 +236502,8 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
|
|
|
236455
236502
|
}
|
|
236456
236503
|
|
|
236457
236504
|
// src/plan-sources.ts
|
|
236458
|
-
import { readFile as
|
|
236459
|
-
import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as
|
|
236505
|
+
import { readFile as readFile3, readdir as readdir2, stat as stat2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
236506
|
+
import { dirname as dirname2, extname as extname2, join as join2, relative as relative2, resolve as resolve3, sep } from "node:path";
|
|
236460
236507
|
var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
|
|
236461
236508
|
var maxPlanSourceFiles = 12;
|
|
236462
236509
|
var maxPlanSourceCharacters = 512e3;
|
|
@@ -236513,7 +236560,7 @@ async function discoverPlanSourceCandidates(input) {
|
|
|
236513
236560
|
const roots = input.roots ?? [".", "config", ...defaultRoots];
|
|
236514
236561
|
const seen = /* @__PURE__ */ new Set();
|
|
236515
236562
|
const candidates = [];
|
|
236516
|
-
const queue = roots.map((root) =>
|
|
236563
|
+
const queue = roots.map((root) => resolve3(input.cwd, root));
|
|
236517
236564
|
while (queue.length && candidates.length < 200) {
|
|
236518
236565
|
const current = queue.shift();
|
|
236519
236566
|
if (seen.has(current)) continue;
|
|
@@ -236537,7 +236584,7 @@ async function discoverPlanSourceCandidates(input) {
|
|
|
236537
236584
|
const kind = planSourceKindByExtension.get(extname2(current).toLowerCase());
|
|
236538
236585
|
if (!kind || !relativePath) continue;
|
|
236539
236586
|
if (entryStat.size > maxPlanSourceCharacters) continue;
|
|
236540
|
-
const contents = await
|
|
236587
|
+
const contents = await readFile3(current, "utf8");
|
|
236541
236588
|
const hasStripePriceId = stripePriceIdPattern.test(contents);
|
|
236542
236589
|
const named = looksLikePlanSource(relativePath);
|
|
236543
236590
|
const looksPriced = billingIntervalPattern.test(contents) && chargeAmountPattern.test(contents);
|
|
@@ -236560,7 +236607,7 @@ async function discoverPlanSourceCandidates(input) {
|
|
|
236560
236607
|
}
|
|
236561
236608
|
async function readApprovedPlanSources(cwd2) {
|
|
236562
236609
|
try {
|
|
236563
|
-
const raw = await
|
|
236610
|
+
const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
|
|
236564
236611
|
const parsed = JSON.parse(raw);
|
|
236565
236612
|
return Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [];
|
|
236566
236613
|
} catch {
|
|
@@ -236568,7 +236615,7 @@ async function readApprovedPlanSources(cwd2) {
|
|
|
236568
236615
|
}
|
|
236569
236616
|
}
|
|
236570
236617
|
async function writeApprovedPlanSources(cwd2, files) {
|
|
236571
|
-
const target =
|
|
236618
|
+
const target = resolve3(cwd2, planSourceApprovalPath);
|
|
236572
236619
|
await mkdir2(dirname2(target), { recursive: true });
|
|
236573
236620
|
await writeFile2(
|
|
236574
236621
|
target,
|
|
@@ -236603,7 +236650,7 @@ async function buildPlanMappingHandoff(input) {
|
|
|
236603
236650
|
if (!kind) {
|
|
236604
236651
|
throw new Error(`${relativePath} is not a supported plan source type.`);
|
|
236605
236652
|
}
|
|
236606
|
-
const content = await
|
|
236653
|
+
const content = await readFile3(resolve3(input.cwd, relativePath), "utf8");
|
|
236607
236654
|
if (content.length > maxPlanSourceCharacters) {
|
|
236608
236655
|
throw new Error(
|
|
236609
236656
|
`${relativePath} is ${content.length} characters; the limit is ${maxPlanSourceCharacters}.`
|
|
@@ -236619,8 +236666,8 @@ function planMappingRequestKey(input) {
|
|
|
236619
236666
|
}
|
|
236620
236667
|
|
|
236621
236668
|
// src/plan-branches.ts
|
|
236622
|
-
import { readdir as readdir3, readFile as
|
|
236623
|
-
import { extname as extname3, join as join3, relative as relative3, resolve as
|
|
236669
|
+
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "node:fs/promises";
|
|
236670
|
+
import { extname as extname3, join as join3, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
236624
236671
|
var maxPlanBranchFileCharacters = 2e6;
|
|
236625
236672
|
var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
236626
236673
|
var nonDefinitionPattern2 = /(^|\/)(__tests__|__mocks__|tests?|specs?|fixtures?|testing|mocks?|mock-data|stories|__stories__)(\/)|\.(test|spec|d|stories)\.[cm]?[jt]sx?$/i;
|
|
@@ -236768,7 +236815,7 @@ async function discoverPlanBranches(input) {
|
|
|
236768
236815
|
const branches = [];
|
|
236769
236816
|
const constants = /* @__PURE__ */ new Map();
|
|
236770
236817
|
const pending = [];
|
|
236771
|
-
const queue = roots.map((root) =>
|
|
236818
|
+
const queue = roots.map((root) => resolve4(input.cwd, root));
|
|
236772
236819
|
while (queue.length) {
|
|
236773
236820
|
const current = queue.shift();
|
|
236774
236821
|
if (seen.has(current)) continue;
|
|
@@ -236791,7 +236838,7 @@ async function discoverPlanBranches(input) {
|
|
|
236791
236838
|
if (!entryStat.isFile() || !relativePath) continue;
|
|
236792
236839
|
if (!codeExtensions.has(extname3(current).toLowerCase())) continue;
|
|
236793
236840
|
if (entryStat.size > maxPlanBranchFileCharacters) continue;
|
|
236794
|
-
const lines = (await
|
|
236841
|
+
const lines = (await readFile4(current, "utf8")).split(/\r?\n/);
|
|
236795
236842
|
collectConstants(lines, constants);
|
|
236796
236843
|
lines.forEach((line, lineIndex) => {
|
|
236797
236844
|
comparisonPattern.lastIndex = 0;
|
|
@@ -236831,7 +236878,7 @@ async function discoverPlanBranches(input) {
|
|
|
236831
236878
|
}
|
|
236832
236879
|
|
|
236833
236880
|
// src/setup-run.ts
|
|
236834
|
-
var
|
|
236881
|
+
var exec2 = promisify2(execFile2);
|
|
236835
236882
|
var hash2 = (text) => createHash3("sha256").update(text).digest("hex");
|
|
236836
236883
|
var contextSchema = external_exports.object({
|
|
236837
236884
|
workspaceId: external_exports.string(),
|
|
@@ -236889,6 +236936,7 @@ async function runGuidedSetup(options) {
|
|
|
236889
236936
|
let progressState = "running";
|
|
236890
236937
|
let progressFailure = null;
|
|
236891
236938
|
try {
|
|
236939
|
+
options.progress?.("1/4 Checking app connection\u2026");
|
|
236892
236940
|
let context = contextSchema.parse(
|
|
236893
236941
|
await request("/api/developer-tools/setup/context")
|
|
236894
236942
|
);
|
|
@@ -236896,8 +236944,14 @@ async function runGuidedSetup(options) {
|
|
|
236896
236944
|
throw new Error(
|
|
236897
236945
|
"Connect Stripe and create app credentials in Setup first."
|
|
236898
236946
|
);
|
|
236899
|
-
|
|
236900
|
-
|
|
236947
|
+
try {
|
|
236948
|
+
await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd });
|
|
236949
|
+
await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd });
|
|
236950
|
+
} catch {
|
|
236951
|
+
throw new Error("Repository setup needed: run this command inside your app\u2019s Git repository with an initial commit and an origin remote. No push or production deploy is required for discovery.");
|
|
236952
|
+
}
|
|
236953
|
+
const head = (await exec2("git", ["rev-parse", "HEAD"], { cwd: options.cwd })).stdout.trim();
|
|
236954
|
+
const diff = (await exec2(
|
|
236901
236955
|
"git",
|
|
236902
236956
|
[
|
|
236903
236957
|
"diff",
|
|
@@ -236910,7 +236964,7 @@ async function runGuidedSetup(options) {
|
|
|
236910
236964
|
],
|
|
236911
236965
|
{ cwd: options.cwd, maxBuffer: 8e6 }
|
|
236912
236966
|
)).stdout;
|
|
236913
|
-
const untracked = (await
|
|
236967
|
+
const untracked = (await exec2("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
236914
236968
|
cwd: options.cwd
|
|
236915
236969
|
})).stdout.split("\0").filter(
|
|
236916
236970
|
(path) => path && !path.startsWith(".saasfunnels/") && !/(^|\/)(\.env|node_modules|dist|\.next)(\/|$|\.)/i.test(path)
|
|
@@ -236922,15 +236976,15 @@ async function runGuidedSetup(options) {
|
|
|
236922
236976
|
const sourceHashes = await Promise.all(
|
|
236923
236977
|
untracked.map(async (path) => [
|
|
236924
236978
|
path,
|
|
236925
|
-
hash2(await
|
|
236979
|
+
hash2(await readFile5(join4(options.cwd, path), "utf8"))
|
|
236926
236980
|
])
|
|
236927
236981
|
);
|
|
236928
236982
|
const revision = `${head}:${hash2(JSON.stringify([diff, sourceHashes]))}`;
|
|
236929
|
-
const remote = (await
|
|
236983
|
+
const remote = (await exec2("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
|
|
236930
236984
|
const repositoryKey = safeRepositoryKey(remote);
|
|
236931
236985
|
const statePath = join4(options.cwd, ".saasfunnels", "setup-run.json");
|
|
236932
236986
|
if (options.resume) {
|
|
236933
|
-
const saved = JSON.parse(await
|
|
236987
|
+
const saved = JSON.parse(await readFile5(statePath, "utf8"));
|
|
236934
236988
|
if (saved.generation !== context.generation || saved.repositoryKey !== repositoryKey || saved.revision !== revision)
|
|
236935
236989
|
throw new Error(
|
|
236936
236990
|
"The app connection or revision changed. Run setup again without --resume."
|
|
@@ -236942,11 +236996,7 @@ async function runGuidedSetup(options) {
|
|
|
236942
236996
|
)).trim()
|
|
236943
236997
|
);
|
|
236944
236998
|
if (!approved)
|
|
236945
|
-
return {
|
|
236946
|
-
exitCode: 2,
|
|
236947
|
-
stdout: "",
|
|
236948
|
-
stderr: "No evidence uploaded. Review the installation instructions, then run setup run --send to approve discovery uploads.\n"
|
|
236949
|
-
};
|
|
236999
|
+
return { exitCode: options.prompt ? 0 : 2, stdout: "Discovery cancelled. Nothing was uploaded. Run npx --yes saasfunnels@latest setup when ready.\n", stderr: "" };
|
|
236950
237000
|
run = (await request("/api/developer-tools/setup/runs", {
|
|
236951
237001
|
generation: context.generation,
|
|
236952
237002
|
repositoryKey,
|
|
@@ -237013,6 +237063,7 @@ async function runGuidedSetup(options) {
|
|
|
237013
237063
|
void update(progressState, progressFailure).catch(() => {
|
|
237014
237064
|
});
|
|
237015
237065
|
}, 3e4);
|
|
237066
|
+
options.progress?.(run.receipts.features ? "2/4 Features already submitted." : "2/4 Discovering features\u2026");
|
|
237016
237067
|
if (!run.receipts.features) {
|
|
237017
237068
|
await update("running");
|
|
237018
237069
|
const scan = await runFeatureSetup({
|
|
@@ -237023,6 +237074,8 @@ async function runGuidedSetup(options) {
|
|
|
237023
237074
|
manifestOnly: true,
|
|
237024
237075
|
accept: ["all"]
|
|
237025
237076
|
});
|
|
237077
|
+
if (!scan.coverage.complete)
|
|
237078
|
+
throw new Error("Feature discovery needs attention: no supported source was scanned or the scan limit was reached. No feature evidence was submitted. Ask your developer to review scanner coverage before retrying.");
|
|
237026
237079
|
if (!scan.ok)
|
|
237027
237080
|
throw new Error(
|
|
237028
237081
|
"Feature discovery needs attention. Run features setup to review the local findings."
|
|
@@ -237037,11 +237090,12 @@ async function runGuidedSetup(options) {
|
|
|
237037
237090
|
);
|
|
237038
237091
|
}
|
|
237039
237092
|
stage = "plans";
|
|
237093
|
+
if (!context.catalogReady) options.progress?.("Waiting for your Stripe catalog. Setup can stay open while it imports\u2026");
|
|
237040
237094
|
const catalogDeadline = Date.now() + 5 * 6e4;
|
|
237041
237095
|
const generation = context.generation;
|
|
237042
237096
|
while (!context.catalogReady && Date.now() < catalogDeadline) {
|
|
237043
237097
|
await update("waiting", "catalog_pending");
|
|
237044
|
-
await new Promise((
|
|
237098
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e4));
|
|
237045
237099
|
context = contextSchema.parse(
|
|
237046
237100
|
await request("/api/developer-tools/setup/context")
|
|
237047
237101
|
);
|
|
@@ -237051,9 +237105,10 @@ async function runGuidedSetup(options) {
|
|
|
237051
237105
|
if (!context.catalogReady)
|
|
237052
237106
|
return {
|
|
237053
237107
|
exitCode: 2,
|
|
237054
|
-
stdout: "Stripe is still importing. Run setup
|
|
237108
|
+
stdout: "Stripe is still importing. Run npx --yes saasfunnels@latest setup --resume when the catalog is ready.\n",
|
|
237055
237109
|
stderr: ""
|
|
237056
237110
|
};
|
|
237111
|
+
options.progress?.(run.receipts.plans ? "3/4 Plans already submitted." : "3/4 Discovering plans and prices\u2026");
|
|
237057
237112
|
if (!run.receipts.plans) {
|
|
237058
237113
|
await update("running");
|
|
237059
237114
|
const candidates = await discoverPlanSourceCandidates({
|
|
@@ -237063,7 +237118,7 @@ async function runGuidedSetup(options) {
|
|
|
237063
237118
|
if (candidates.length > 12) throw new Error("Too many sources");
|
|
237064
237119
|
let manual = null;
|
|
237065
237120
|
try {
|
|
237066
|
-
manual = await
|
|
237121
|
+
manual = await readFile5(
|
|
237067
237122
|
join4(options.cwd, ".saasfunnels/setup-pricing.json"),
|
|
237068
237123
|
"utf8"
|
|
237069
237124
|
);
|
|
@@ -237073,7 +237128,7 @@ async function runGuidedSetup(options) {
|
|
|
237073
237128
|
const discovered = manual !== null ? extractSetupPricing(manual, "json") : (await Promise.all(
|
|
237074
237129
|
candidates.map(
|
|
237075
237130
|
async (candidate) => extractSetupPricing(
|
|
237076
|
-
await
|
|
237131
|
+
await readFile5(join4(options.cwd, candidate.path), "utf8"),
|
|
237077
237132
|
candidate.kind
|
|
237078
237133
|
)
|
|
237079
237134
|
)
|
|
@@ -237092,13 +237147,14 @@ async function runGuidedSetup(options) {
|
|
|
237092
237147
|
await update("waiting", "extraction_unsupported");
|
|
237093
237148
|
return {
|
|
237094
237149
|
exitCode: 2,
|
|
237095
|
-
stdout: "Pricing discovery needs manual review. Create .saasfunnels/setup-pricing.json with a plans array containing key, name, features (boolean or numeric access), and prices (Stripe price IDs). Review it, then run setup
|
|
237150
|
+
stdout: "Pricing discovery needs manual review. Create .saasfunnels/setup-pricing.json with a plans array containing key, name, features (boolean or numeric access), and prices (Stripe price IDs). Review it, then run npx --yes saasfunnels@latest setup --resume. No source files were uploaded.\n",
|
|
237096
237151
|
stderr: ""
|
|
237097
237152
|
};
|
|
237098
237153
|
}
|
|
237099
237154
|
}
|
|
237100
237155
|
stage = "branches";
|
|
237101
237156
|
const planValues = [.../* @__PURE__ */ new Set([...context.planNames, ...run.planNames ?? []])];
|
|
237157
|
+
options.progress?.(run.receipts.branches ? "4/4 Plan checks already submitted." : "4/4 Checking plan conditions\u2026");
|
|
237102
237158
|
if (!run.receipts.branches) {
|
|
237103
237159
|
await update("running");
|
|
237104
237160
|
const branches = planValues.length ? await discoverPlanBranches({
|
|
@@ -237129,14 +237185,14 @@ async function runGuidedSetup(options) {
|
|
|
237129
237185
|
}
|
|
237130
237186
|
return {
|
|
237131
237187
|
exitCode: 0,
|
|
237132
|
-
stdout: "Discovery submitted.
|
|
237188
|
+
stdout: "Discovery submitted. Open https://app.saasfunnels.ai/app/setup to review findings and verify installation. Payment transactions have not been tested by this command.\n",
|
|
237133
237189
|
stderr: ""
|
|
237134
237190
|
};
|
|
237135
237191
|
} catch (error51) {
|
|
237136
237192
|
clearInterval(timer);
|
|
237137
237193
|
await reportFailure?.().catch(() => {
|
|
237138
237194
|
});
|
|
237139
|
-
const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery)/.test(
|
|
237195
|
+
const safe = error51 instanceof Error && /^(Setup request failed|Connect Stripe|The app connection|Use a repository|Feature discovery|Repository setup)/.test(
|
|
237140
237196
|
error51.message
|
|
237141
237197
|
) ? error51.message : "Setup could not finish. Check local repository access and developer credentials, then resume.";
|
|
237142
237198
|
return { exitCode: 2, stdout: "", stderr: safe + "\n" };
|
|
@@ -237187,11 +237243,11 @@ async function submitApprovedSetupPlans(input) {
|
|
|
237187
237243
|
}
|
|
237188
237244
|
|
|
237189
237245
|
// src/cli.ts
|
|
237190
|
-
import { mkdir as mkdir4, readFile as
|
|
237246
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
237191
237247
|
|
|
237192
237248
|
// src/identity.ts
|
|
237193
237249
|
var SAASFUNNELS_CLI_NAME = "saasfunnels";
|
|
237194
|
-
var SAASFUNNELS_CLI_VERSION = "0.3.
|
|
237250
|
+
var SAASFUNNELS_CLI_VERSION = "0.3.1";
|
|
237195
237251
|
var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
|
|
237196
237252
|
var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
|
|
237197
237253
|
var SAASFUNNELS_ENV = Object.freeze({
|
|
@@ -238979,7 +239035,8 @@ function usage() {
|
|
|
238979
239035
|
return `${SAASFUNNELS_PRODUCT_NAME} CLI
|
|
238980
239036
|
|
|
238981
239037
|
Usage:
|
|
238982
|
-
${SAASFUNNELS_CLI_NAME} setup
|
|
239038
|
+
${SAASFUNNELS_CLI_NAME} setup [--resume] [--env-file <path>]
|
|
239039
|
+
${SAASFUNNELS_CLI_NAME} setup run [--resume] [--send] [--non-interactive]
|
|
238983
239040
|
${SAASFUNNELS_CLI_NAME} init [--api-base-url <url>] [--force]
|
|
238984
239041
|
${SAASFUNNELS_CLI_NAME} agent install --target codex|claude-code|cursor|markdown [--endpoint <url>]
|
|
238985
239042
|
${SAASFUNNELS_CLI_NAME} events validate <file> [--source direct|posthog|segment] [--json]
|
|
@@ -239016,7 +239073,7 @@ function filesystem(options) {
|
|
|
239016
239073
|
mkdir: async (path, mkdirOptions) => {
|
|
239017
239074
|
await mkdir4(path, mkdirOptions);
|
|
239018
239075
|
},
|
|
239019
|
-
readFile:
|
|
239076
|
+
readFile: readFile6,
|
|
239020
239077
|
writeFile: writeFile4
|
|
239021
239078
|
};
|
|
239022
239079
|
}
|
|
@@ -239047,7 +239104,7 @@ function sourceFlag(flags) {
|
|
|
239047
239104
|
}
|
|
239048
239105
|
async function readJsonFile(filePath, options) {
|
|
239049
239106
|
const raw = await filesystem(options).readFile(
|
|
239050
|
-
|
|
239107
|
+
resolve5(cwd(options), filePath),
|
|
239051
239108
|
"utf8"
|
|
239052
239109
|
);
|
|
239053
239110
|
return JSON.parse(raw);
|
|
@@ -239236,7 +239293,7 @@ async function commandFeatures(args, flags, options) {
|
|
|
239236
239293
|
}
|
|
239237
239294
|
const discoveryRoots = (flagString(flags, "discovery-roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean);
|
|
239238
239295
|
const source = await filesystem(options).readFile(
|
|
239239
|
-
|
|
239296
|
+
resolve5(cwd(options), path),
|
|
239240
239297
|
"utf8"
|
|
239241
239298
|
);
|
|
239242
239299
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -239252,7 +239309,7 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
|
|
|
239252
239309
|
let sdkVersions = [];
|
|
239253
239310
|
try {
|
|
239254
239311
|
const packageSource = await filesystem(options).readFile(
|
|
239255
|
-
|
|
239312
|
+
resolve5(cwd(options), "package.json"),
|
|
239256
239313
|
"utf8"
|
|
239257
239314
|
);
|
|
239258
239315
|
sdkVersions = featureSdkVersionsFromPackageJson(
|
|
@@ -239323,7 +239380,7 @@ async function commandCatalog(args, flags, options) {
|
|
|
239323
239380
|
if (action === "validate") {
|
|
239324
239381
|
const path = args[1] ?? ".saasfunnels/catalog.yaml";
|
|
239325
239382
|
const source = await filesystem(options).readFile(
|
|
239326
|
-
|
|
239383
|
+
resolve5(cwd(options), path),
|
|
239327
239384
|
"utf8"
|
|
239328
239385
|
);
|
|
239329
239386
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -239573,7 +239630,7 @@ async function commandVerify(flags, options) {
|
|
|
239573
239630
|
if (catalogPath) {
|
|
239574
239631
|
try {
|
|
239575
239632
|
const source = await filesystem(options).readFile(
|
|
239576
|
-
|
|
239633
|
+
resolve5(cwd(options), catalogPath),
|
|
239577
239634
|
"utf8"
|
|
239578
239635
|
);
|
|
239579
239636
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -239916,7 +239973,15 @@ async function runSaaSFunnelsCli(argv2, options = {}) {
|
|
|
239916
239973
|
if (!command || command === "help" || command === "--help")
|
|
239917
239974
|
return result(0, usage());
|
|
239918
239975
|
if (command === "init") return await commandInit(parsed.flags, options);
|
|
239919
|
-
if (command === "setup"
|
|
239976
|
+
if (command === "setup") {
|
|
239977
|
+
const help = "SaaSFunnels setup\n\nRun npx --yes saasfunnels@latest setup in your app repository.\nIt checks access, asks before sending findings, and discovers features and plans for review.\nUse --resume after an interruption, or --env-file .env.local to load developer access.\nFor automation, supply SAASFUNNELS_API_KEY and explicit upload approval with --send --non-interactive.\nApp implementation, deployment and payment verification remain in https://app.saasfunnels.ai/app/setup.\n";
|
|
239978
|
+
if (hasFlag(parsed.flags, "help") || args[0] === "help") return result(0, help);
|
|
239979
|
+
if (args.length > 1 || args[0] && args[0] !== "run") return result(2, "", help);
|
|
239980
|
+
const interactive = !jsonMode(parsed.flags) && !hasFlag(parsed.flags, "non-interactive");
|
|
239981
|
+
const access2 = await resolveSetupAccess({ cwd: cwd(options), key: envValue2(options, SAASFUNNELS_ENV.apiKey), envFile: flagString(parsed.flags, "env-file"), prompt: interactive ? options.prompt : void 0 });
|
|
239982
|
+
if (!access2.key) return result(2, "", access2.error + "\n");
|
|
239983
|
+
return runGuidedSetup({ cwd: cwd(options), apiBaseUrl: apiBaseUrl2(options, parsed.flags), key: access2.key, send: hasFlag(parsed.flags, "send"), resume: hasFlag(parsed.flags, "resume"), fetch: options.fetch, prompt: interactive ? options.prompt : void 0, progress: jsonMode(parsed.flags) ? void 0 : options.progress });
|
|
239984
|
+
}
|
|
239920
239985
|
if (command === "agent")
|
|
239921
239986
|
return await commandAgentInstall(args, parsed.flags, options);
|
|
239922
239987
|
if (command === "events")
|
|
@@ -239944,13 +240009,14 @@ ${usage()}`);
|
|
|
239944
240009
|
|
|
239945
240010
|
// src/saasfunnels.ts
|
|
239946
240011
|
var argv = process.argv.slice(2);
|
|
239947
|
-
var interactiveFeatureSetup = !argv.includes("--json") && !argv.includes("--non-interactive") && (argv[0] === "setup"
|
|
240012
|
+
var interactiveFeatureSetup = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !argv.includes("--json") && !argv.includes("--non-interactive") && (argv[0] === "setup" || argv[0] === "features" && argv[1] === "setup" || argv[0] === "catalog" && ["discover", "diff"].includes(argv[1] ?? ""));
|
|
239948
240013
|
var prompts = interactiveFeatureSetup ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
|
|
239949
240014
|
var result2;
|
|
239950
240015
|
try {
|
|
239951
240016
|
result2 = await runSaaSFunnelsCli(argv, {
|
|
239952
240017
|
cwd: process.cwd(),
|
|
239953
240018
|
env: process.env,
|
|
240019
|
+
progress: (message) => process.stdout.write(message + "\n"),
|
|
239954
240020
|
prompt: prompts ? (message) => prompts.question(message) : void 0
|
|
239955
240021
|
});
|
|
239956
240022
|
} finally {
|
|
@@ -121,6 +121,13 @@ export type FeatureSetupResult = {
|
|
|
121
121
|
state: "failed" | "passed" | "skipped";
|
|
122
122
|
};
|
|
123
123
|
scannedFileCount: number;
|
|
124
|
+
coverage: {
|
|
125
|
+
complete: boolean;
|
|
126
|
+
matchedFiles: number;
|
|
127
|
+
scannedFiles: number;
|
|
128
|
+
byteLimitReached: boolean;
|
|
129
|
+
candidateLimitReached: boolean;
|
|
130
|
+
};
|
|
124
131
|
validation: {
|
|
125
132
|
errors: string[];
|
|
126
133
|
ok: boolean;
|
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.1";
|
|
3
3
|
export declare const SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
|
|
4
4
|
export declare const SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
|
|
5
5
|
export declare const SAASFUNNELS_ENV: Readonly<{
|
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.1";
|
|
81
81
|
export declare function saasFunnelsMcpResources(): McpResourceDefinition[];
|
|
82
82
|
export declare function readSaaSFunnelsMcpResource(uri: string, options?: SaaSFunnelsMcpOptions): string;
|
|
83
83
|
export declare function saasFunnelsMcpToolDefinitions({ allowFunnelTests, allowFunnelWrites, allowTestWrites, includeHostedAccountTools, }?: {
|