saasfunnels 0.1.0-beta.0 → 0.2.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 +48 -3
- package/dist/library.js +25514 -0
- package/dist/saasfunnels.js +839 -83
- package/dist/types/capabilities.d.ts +14 -0
- package/dist/types/check-summary.d.ts +26 -0
- package/dist/types/feature-setup.d.ts +258 -0
- package/dist/types/identity.d.ts +14 -0
- package/dist/types/library.d.ts +3 -0
- package/dist/types/mcp.d.ts +91 -0
- package/dist/types/runtime-contracts.d.ts +73 -0
- package/package.json +12 -5
package/dist/saasfunnels.js
CHANGED
|
@@ -4001,10 +4001,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
4001
4001
|
let offset = bm.offset;
|
|
4002
4002
|
let commentEnd = null;
|
|
4003
4003
|
for (const collItem of bm.items) {
|
|
4004
|
-
const { start, key: key2, sep, value } = collItem;
|
|
4004
|
+
const { start, key: key2, sep: sep3, value } = collItem;
|
|
4005
4005
|
const keyProps = resolveProps.resolveProps(start, {
|
|
4006
4006
|
indicator: "explicit-key-ind",
|
|
4007
|
-
next: key2 ??
|
|
4007
|
+
next: key2 ?? sep3?.[0],
|
|
4008
4008
|
offset,
|
|
4009
4009
|
onError,
|
|
4010
4010
|
parentIndent: bm.indent,
|
|
@@ -4018,7 +4018,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4018
4018
|
else if ("indent" in key2 && key2.indent !== bm.indent)
|
|
4019
4019
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
4020
4020
|
}
|
|
4021
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
4021
|
+
if (!keyProps.anchor && !keyProps.tag && !sep3) {
|
|
4022
4022
|
commentEnd = keyProps.end;
|
|
4023
4023
|
if (keyProps.comment) {
|
|
4024
4024
|
if (map2.comment)
|
|
@@ -4042,7 +4042,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4042
4042
|
ctx.atKey = false;
|
|
4043
4043
|
if (utilMapIncludes.mapIncludes(ctx, map2.items, keyNode))
|
|
4044
4044
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
4045
|
-
const valueProps = resolveProps.resolveProps(
|
|
4045
|
+
const valueProps = resolveProps.resolveProps(sep3 ?? [], {
|
|
4046
4046
|
indicator: "map-value-ind",
|
|
4047
4047
|
next: value,
|
|
4048
4048
|
offset: keyNode.range[2],
|
|
@@ -4058,7 +4058,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4058
4058
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
4059
4059
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
4060
4060
|
}
|
|
4061
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
4061
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep3, null, valueProps, onError);
|
|
4062
4062
|
if (ctx.schema.compat)
|
|
4063
4063
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
4064
4064
|
offset = valueNode.range[2];
|
|
@@ -4149,7 +4149,7 @@ var require_resolve_end = __commonJS({
|
|
|
4149
4149
|
let comment = "";
|
|
4150
4150
|
if (end) {
|
|
4151
4151
|
let hasSpace = false;
|
|
4152
|
-
let
|
|
4152
|
+
let sep3 = "";
|
|
4153
4153
|
for (const token of end) {
|
|
4154
4154
|
const { source, type } = token;
|
|
4155
4155
|
switch (type) {
|
|
@@ -4163,13 +4163,13 @@ var require_resolve_end = __commonJS({
|
|
|
4163
4163
|
if (!comment)
|
|
4164
4164
|
comment = cb;
|
|
4165
4165
|
else
|
|
4166
|
-
comment +=
|
|
4167
|
-
|
|
4166
|
+
comment += sep3 + cb;
|
|
4167
|
+
sep3 = "";
|
|
4168
4168
|
break;
|
|
4169
4169
|
}
|
|
4170
4170
|
case "newline":
|
|
4171
4171
|
if (comment)
|
|
4172
|
-
|
|
4172
|
+
sep3 += source;
|
|
4173
4173
|
hasSpace = true;
|
|
4174
4174
|
break;
|
|
4175
4175
|
default:
|
|
@@ -4212,18 +4212,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4212
4212
|
let offset = fc.offset + fc.start.source.length;
|
|
4213
4213
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
4214
4214
|
const collItem = fc.items[i];
|
|
4215
|
-
const { start, key: key2, sep, value } = collItem;
|
|
4215
|
+
const { start, key: key2, sep: sep3, value } = collItem;
|
|
4216
4216
|
const props = resolveProps.resolveProps(start, {
|
|
4217
4217
|
flow: fcName,
|
|
4218
4218
|
indicator: "explicit-key-ind",
|
|
4219
|
-
next: key2 ??
|
|
4219
|
+
next: key2 ?? sep3?.[0],
|
|
4220
4220
|
offset,
|
|
4221
4221
|
onError,
|
|
4222
4222
|
parentIndent: fc.indent,
|
|
4223
4223
|
startOnNewline: false
|
|
4224
4224
|
});
|
|
4225
4225
|
if (!props.found) {
|
|
4226
|
-
if (!props.anchor && !props.tag && !
|
|
4226
|
+
if (!props.anchor && !props.tag && !sep3 && !value) {
|
|
4227
4227
|
if (i === 0 && props.comma)
|
|
4228
4228
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
4229
4229
|
else if (i < fc.items.length - 1)
|
|
@@ -4277,8 +4277,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4277
4277
|
}
|
|
4278
4278
|
}
|
|
4279
4279
|
}
|
|
4280
|
-
if (!isMap && !
|
|
4281
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
4280
|
+
if (!isMap && !sep3 && !props.found) {
|
|
4281
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep3, null, props, onError);
|
|
4282
4282
|
coll.items.push(valueNode);
|
|
4283
4283
|
offset = valueNode.range[2];
|
|
4284
4284
|
if (isBlock(value))
|
|
@@ -4290,7 +4290,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4290
4290
|
if (isBlock(key2))
|
|
4291
4291
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
4292
4292
|
ctx.atKey = false;
|
|
4293
|
-
const valueProps = resolveProps.resolveProps(
|
|
4293
|
+
const valueProps = resolveProps.resolveProps(sep3 ?? [], {
|
|
4294
4294
|
flow: fcName,
|
|
4295
4295
|
indicator: "map-value-ind",
|
|
4296
4296
|
next: value,
|
|
@@ -4301,8 +4301,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4301
4301
|
});
|
|
4302
4302
|
if (valueProps.found) {
|
|
4303
4303
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
4304
|
-
if (
|
|
4305
|
-
for (const st of
|
|
4304
|
+
if (sep3)
|
|
4305
|
+
for (const st of sep3) {
|
|
4306
4306
|
if (st === valueProps.found)
|
|
4307
4307
|
break;
|
|
4308
4308
|
if (st.type === "newline") {
|
|
@@ -4319,7 +4319,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4319
4319
|
else
|
|
4320
4320
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
4321
4321
|
}
|
|
4322
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
4322
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep3, null, valueProps, onError) : null;
|
|
4323
4323
|
if (valueNode) {
|
|
4324
4324
|
if (isBlock(value))
|
|
4325
4325
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -4499,7 +4499,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4499
4499
|
chompStart = i + 1;
|
|
4500
4500
|
}
|
|
4501
4501
|
let value = "";
|
|
4502
|
-
let
|
|
4502
|
+
let sep3 = "";
|
|
4503
4503
|
let prevMoreIndented = false;
|
|
4504
4504
|
for (let i = 0; i < contentStart; ++i)
|
|
4505
4505
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -4516,24 +4516,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4516
4516
|
indent = "";
|
|
4517
4517
|
}
|
|
4518
4518
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
4519
|
-
value +=
|
|
4520
|
-
|
|
4519
|
+
value += sep3 + indent.slice(trimIndent) + content;
|
|
4520
|
+
sep3 = "\n";
|
|
4521
4521
|
} else if (indent.length > trimIndent || content[0] === " ") {
|
|
4522
|
-
if (
|
|
4523
|
-
|
|
4524
|
-
else if (!prevMoreIndented &&
|
|
4525
|
-
|
|
4526
|
-
value +=
|
|
4527
|
-
|
|
4522
|
+
if (sep3 === " ")
|
|
4523
|
+
sep3 = "\n";
|
|
4524
|
+
else if (!prevMoreIndented && sep3 === "\n")
|
|
4525
|
+
sep3 = "\n\n";
|
|
4526
|
+
value += sep3 + indent.slice(trimIndent) + content;
|
|
4527
|
+
sep3 = "\n";
|
|
4528
4528
|
prevMoreIndented = true;
|
|
4529
4529
|
} else if (content === "") {
|
|
4530
|
-
if (
|
|
4530
|
+
if (sep3 === "\n")
|
|
4531
4531
|
value += "\n";
|
|
4532
4532
|
else
|
|
4533
|
-
|
|
4533
|
+
sep3 = "\n";
|
|
4534
4534
|
} else {
|
|
4535
|
-
value +=
|
|
4536
|
-
|
|
4535
|
+
value += sep3 + content;
|
|
4536
|
+
sep3 = " ";
|
|
4537
4537
|
prevMoreIndented = false;
|
|
4538
4538
|
}
|
|
4539
4539
|
}
|
|
@@ -4715,25 +4715,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
4715
4715
|
if (!match)
|
|
4716
4716
|
return source;
|
|
4717
4717
|
let res = match[1];
|
|
4718
|
-
let
|
|
4718
|
+
let sep3 = " ";
|
|
4719
4719
|
let pos = first.lastIndex;
|
|
4720
4720
|
line.lastIndex = pos;
|
|
4721
4721
|
while (match = line.exec(source)) {
|
|
4722
4722
|
if (match[1] === "") {
|
|
4723
|
-
if (
|
|
4724
|
-
res +=
|
|
4723
|
+
if (sep3 === "\n")
|
|
4724
|
+
res += sep3;
|
|
4725
4725
|
else
|
|
4726
|
-
|
|
4726
|
+
sep3 = "\n";
|
|
4727
4727
|
} else {
|
|
4728
|
-
res +=
|
|
4729
|
-
|
|
4728
|
+
res += sep3 + match[1];
|
|
4729
|
+
sep3 = " ";
|
|
4730
4730
|
}
|
|
4731
4731
|
pos = line.lastIndex;
|
|
4732
4732
|
}
|
|
4733
4733
|
const last = /[ \t]*(.*)/sy;
|
|
4734
4734
|
last.lastIndex = pos;
|
|
4735
4735
|
match = last.exec(source);
|
|
4736
|
-
return res +
|
|
4736
|
+
return res + sep3 + (match?.[1] ?? "");
|
|
4737
4737
|
}
|
|
4738
4738
|
function doubleQuotedValue(source, onError) {
|
|
4739
4739
|
let res = "";
|
|
@@ -5543,14 +5543,14 @@ var require_cst_stringify = __commonJS({
|
|
|
5543
5543
|
}
|
|
5544
5544
|
}
|
|
5545
5545
|
}
|
|
5546
|
-
function stringifyItem({ start, key: key2, sep, value }) {
|
|
5546
|
+
function stringifyItem({ start, key: key2, sep: sep3, value }) {
|
|
5547
5547
|
let res = "";
|
|
5548
5548
|
for (const st of start)
|
|
5549
5549
|
res += st.source;
|
|
5550
5550
|
if (key2)
|
|
5551
5551
|
res += stringifyToken(key2);
|
|
5552
|
-
if (
|
|
5553
|
-
for (const st of
|
|
5552
|
+
if (sep3)
|
|
5553
|
+
for (const st of sep3)
|
|
5554
5554
|
res += st.source;
|
|
5555
5555
|
if (value)
|
|
5556
5556
|
res += stringifyToken(value);
|
|
@@ -6717,18 +6717,18 @@ var require_parser = __commonJS({
|
|
|
6717
6717
|
if (this.type === "map-value-ind") {
|
|
6718
6718
|
const prev = getPrevProps(this.peek(2));
|
|
6719
6719
|
const start = getFirstKeyStartProps(prev);
|
|
6720
|
-
let
|
|
6720
|
+
let sep3;
|
|
6721
6721
|
if (scalar.end) {
|
|
6722
|
-
|
|
6723
|
-
|
|
6722
|
+
sep3 = scalar.end;
|
|
6723
|
+
sep3.push(this.sourceToken);
|
|
6724
6724
|
delete scalar.end;
|
|
6725
6725
|
} else
|
|
6726
|
-
|
|
6726
|
+
sep3 = [this.sourceToken];
|
|
6727
6727
|
const map2 = {
|
|
6728
6728
|
type: "block-map",
|
|
6729
6729
|
offset: scalar.offset,
|
|
6730
6730
|
indent: scalar.indent,
|
|
6731
|
-
items: [{ start, key: scalar, sep }]
|
|
6731
|
+
items: [{ start, key: scalar, sep: sep3 }]
|
|
6732
6732
|
};
|
|
6733
6733
|
this.onKeyLine = true;
|
|
6734
6734
|
this.stack[this.stack.length - 1] = map2;
|
|
@@ -6881,15 +6881,15 @@ var require_parser = __commonJS({
|
|
|
6881
6881
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
6882
6882
|
const start2 = getFirstKeyStartProps(it.start);
|
|
6883
6883
|
const key2 = it.key;
|
|
6884
|
-
const
|
|
6885
|
-
|
|
6884
|
+
const sep3 = it.sep;
|
|
6885
|
+
sep3.push(this.sourceToken);
|
|
6886
6886
|
delete it.key;
|
|
6887
6887
|
delete it.sep;
|
|
6888
6888
|
this.stack.push({
|
|
6889
6889
|
type: "block-map",
|
|
6890
6890
|
offset: this.offset,
|
|
6891
6891
|
indent: this.indent,
|
|
6892
|
-
items: [{ start: start2, key: key2, sep }]
|
|
6892
|
+
items: [{ start: start2, key: key2, sep: sep3 }]
|
|
6893
6893
|
});
|
|
6894
6894
|
} else if (start.length > 0) {
|
|
6895
6895
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -7083,13 +7083,13 @@ var require_parser = __commonJS({
|
|
|
7083
7083
|
const prev = getPrevProps(parent);
|
|
7084
7084
|
const start = getFirstKeyStartProps(prev);
|
|
7085
7085
|
fixFlowSeqItems(fc);
|
|
7086
|
-
const
|
|
7087
|
-
|
|
7086
|
+
const sep3 = fc.end.splice(1, fc.end.length);
|
|
7087
|
+
sep3.push(this.sourceToken);
|
|
7088
7088
|
const map2 = {
|
|
7089
7089
|
type: "block-map",
|
|
7090
7090
|
offset: fc.offset,
|
|
7091
7091
|
indent: fc.indent,
|
|
7092
|
-
items: [{ start, key: fc, sep }]
|
|
7092
|
+
items: [{ start, key: fc, sep: sep3 }]
|
|
7093
7093
|
};
|
|
7094
7094
|
this.onKeyLine = true;
|
|
7095
7095
|
this.stack[this.stack.length - 1] = map2;
|
|
@@ -7371,8 +7371,8 @@ var require_dist = __commonJS({
|
|
|
7371
7371
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
7372
7372
|
|
|
7373
7373
|
// src/cli.ts
|
|
7374
|
-
import { dirname as
|
|
7375
|
-
import { mkdir as
|
|
7374
|
+
import { dirname as dirname3, join as join4, resolve as resolve4 } from "node:path";
|
|
7375
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
7376
7376
|
|
|
7377
7377
|
// src/runtime-contracts.js
|
|
7378
7378
|
import { createHash } from "node:crypto";
|
|
@@ -22575,9 +22575,9 @@ function boundedTraitValue(value) {
|
|
|
22575
22575
|
}
|
|
22576
22576
|
function pickAccountTraits(records, keys = accountTraitKeys) {
|
|
22577
22577
|
const traits = {};
|
|
22578
|
-
for (const
|
|
22578
|
+
for (const record4 of records) {
|
|
22579
22579
|
for (const key2 of keys) {
|
|
22580
|
-
const value = boundedTraitValue(
|
|
22580
|
+
const value = boundedTraitValue(record4[key2]);
|
|
22581
22581
|
if (value !== void 0) traits[key2] = value;
|
|
22582
22582
|
}
|
|
22583
22583
|
}
|
|
@@ -23310,8 +23310,8 @@ function collectSemanticCandidates(payload, source) {
|
|
|
23310
23310
|
records.push({ path: "event.properties", record: objectValue2(wrappedEvent.properties) });
|
|
23311
23311
|
}
|
|
23312
23312
|
return records.flatMap(
|
|
23313
|
-
({ path, record:
|
|
23314
|
-
const value = nonEmptyString(
|
|
23313
|
+
({ path, record: record4 }) => ["semantic_type", "event_type"].flatMap((field) => {
|
|
23314
|
+
const value = nonEmptyString(record4[field]);
|
|
23315
23315
|
return value ? [{ path: path ? `${path}.${field}` : field, value }] : [];
|
|
23316
23316
|
})
|
|
23317
23317
|
);
|
|
@@ -23992,13 +23992,24 @@ var featureInstrumentationBindingSchema = external_exports.object({
|
|
|
23992
23992
|
),
|
|
23993
23993
|
symbol: external_exports.string().min(1).max(240)
|
|
23994
23994
|
}).strict();
|
|
23995
|
+
var featureInstrumentationManifestProducers = ["cli", "github_action", "github_app"];
|
|
23996
|
+
var featureInstrumentationScanRoles = ["series", "candidate"];
|
|
23995
23997
|
var featureInstrumentationManifestSchema = external_exports.object({
|
|
23996
23998
|
bindings: external_exports.array(featureInstrumentationBindingSchema).max(1e4),
|
|
23999
|
+
discoveryRoots: external_exports.array(
|
|
24000
|
+
external_exports.string().min(1).max(200).refine(
|
|
24001
|
+
(value) => !value.startsWith("/") && !value.includes(".."),
|
|
24002
|
+
"Discovery roots must be relative and contained."
|
|
24003
|
+
)
|
|
24004
|
+
).max(50),
|
|
23997
24005
|
environment: external_exports.enum(funnelFeatureEnvironments),
|
|
23998
24006
|
manifestFingerprint: fingerprint2,
|
|
23999
24007
|
manifestVersion: external_exports.string().min(1).max(80),
|
|
24008
|
+
producer: external_exports.enum(featureInstrumentationManifestProducers),
|
|
24009
|
+
repositoryKey: external_exports.string().min(1).max(200),
|
|
24000
24010
|
repositoryRevision: external_exports.string().min(1).max(160),
|
|
24001
|
-
|
|
24011
|
+
scanRole: external_exports.enum(featureInstrumentationScanRoles),
|
|
24012
|
+
schemaVersion: external_exports.literal(2),
|
|
24002
24013
|
sdkVersions: external_exports.array(external_exports.string().max(40).regex(/^\d+\.\d+\.\d+$/)).max(10).default([]),
|
|
24003
24014
|
validatedAt: timestamp2,
|
|
24004
24015
|
validationState: external_exports.enum(["valid", "failed"])
|
|
@@ -24039,6 +24050,7 @@ var featureBindingKinds = [
|
|
|
24039
24050
|
];
|
|
24040
24051
|
var defaultRoots = ["app", "src", "lib", "components", "pages"];
|
|
24041
24052
|
var ignoredSegments = /* @__PURE__ */ new Set([
|
|
24053
|
+
".claude",
|
|
24042
24054
|
".git",
|
|
24043
24055
|
".next",
|
|
24044
24056
|
".output",
|
|
@@ -24642,7 +24654,7 @@ async function runFeatureSetup(input) {
|
|
|
24642
24654
|
(edit) => changedEdits.some((changed) => changed.id === edit.id)
|
|
24643
24655
|
);
|
|
24644
24656
|
changeRecordPath = ".saasfunnels/feature-installation.json";
|
|
24645
|
-
const
|
|
24657
|
+
const record4 = {
|
|
24646
24658
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24647
24659
|
environment: input.environment,
|
|
24648
24660
|
files: changedEdits.map((edit) => ({
|
|
@@ -24659,7 +24671,7 @@ async function runFeatureSetup(input) {
|
|
|
24659
24671
|
path: change.path
|
|
24660
24672
|
})),
|
|
24661
24673
|
{
|
|
24662
|
-
contents: `${JSON.stringify(
|
|
24674
|
+
contents: `${JSON.stringify(record4, null, 2)}
|
|
24663
24675
|
`,
|
|
24664
24676
|
path: changeRecordPath
|
|
24665
24677
|
}
|
|
@@ -24739,11 +24751,15 @@ function buildFeatureInstrumentationHandoff(input) {
|
|
|
24739
24751
|
symbol: evidence.symbol
|
|
24740
24752
|
}))
|
|
24741
24753
|
),
|
|
24754
|
+
discoveryRoots: [...new Set(input.discoveryRoots ?? [])].sort(),
|
|
24742
24755
|
environment: input.manifest.environment,
|
|
24743
24756
|
manifestFingerprint,
|
|
24744
24757
|
manifestVersion: manifestFingerprint.slice(0, 16),
|
|
24758
|
+
producer: input.producer ?? "cli",
|
|
24759
|
+
repositoryKey: input.repositoryKey,
|
|
24745
24760
|
repositoryRevision: input.repositoryRevision,
|
|
24746
|
-
|
|
24761
|
+
scanRole: input.scanRole ?? "series",
|
|
24762
|
+
schemaVersion: 2,
|
|
24747
24763
|
sdkVersions: [...new Set(input.sdkVersions ?? [])].sort(),
|
|
24748
24764
|
validatedAt: input.validatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
24749
24765
|
validationState: "valid"
|
|
@@ -24786,7 +24802,7 @@ Applied reviewed edits. Change record: ${result3.changeRecordPath}` : result3.id
|
|
|
24786
24802
|
|
|
24787
24803
|
// src/identity.ts
|
|
24788
24804
|
var SAASFUNNELS_CLI_NAME = "saasfunnels";
|
|
24789
|
-
var SAASFUNNELS_CLI_VERSION = "0.1
|
|
24805
|
+
var SAASFUNNELS_CLI_VERSION = "0.2.1";
|
|
24790
24806
|
var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
|
|
24791
24807
|
var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
|
|
24792
24808
|
var SAASFUNNELS_ENV = Object.freeze({
|
|
@@ -24817,6 +24833,449 @@ function saasFunnelsPublicText(value) {
|
|
|
24817
24833
|
).replaceAll("prevenueFeatureKeys", "saasFunnelsFeatureKeys").replaceAll("PrevenueFeatureKey", "SaaSFunnelsFeatureKey").replaceAll("SaaSFunnels", SAASFUNNELS_PRODUCT_NAME);
|
|
24818
24834
|
}
|
|
24819
24835
|
|
|
24836
|
+
// src/check-summary.ts
|
|
24837
|
+
var kindHeadings = {
|
|
24838
|
+
added: "New Feature bindings",
|
|
24839
|
+
moved: "Moved bindings",
|
|
24840
|
+
removed: "Removed bindings",
|
|
24841
|
+
renamed: "Renamed bindings"
|
|
24842
|
+
};
|
|
24843
|
+
var rejectionReasons = {
|
|
24844
|
+
binding_duplicate: "duplicate evidence for one location",
|
|
24845
|
+
binding_fingerprint_invalid: "malformed fingerprint",
|
|
24846
|
+
binding_kind_unknown: "unrecognised binding kind",
|
|
24847
|
+
binding_path_unsafe: "unsafe or sensitive file path",
|
|
24848
|
+
binding_shape_invalid: "malformed binding"
|
|
24849
|
+
};
|
|
24850
|
+
function formatFeatureDriftCheckSummary(input) {
|
|
24851
|
+
const lines = [];
|
|
24852
|
+
const removed = input.drift.filter((entry) => entry.kind === "removed");
|
|
24853
|
+
const breaking = removed.filter(
|
|
24854
|
+
(entry) => entry.featureKey && (input.targetedFeatureKeys ?? []).includes(entry.featureKey)
|
|
24855
|
+
);
|
|
24856
|
+
if (breaking.length) {
|
|
24857
|
+
lines.push("### Live Funnels target Features this change removes");
|
|
24858
|
+
for (const entry of breaking) {
|
|
24859
|
+
lines.push(`- \`${entry.featureKey}\`${entry.location ? ` \u2014 ${entry.location}` : ""}`);
|
|
24860
|
+
}
|
|
24861
|
+
lines.push("");
|
|
24862
|
+
}
|
|
24863
|
+
if (!input.drift.length) {
|
|
24864
|
+
lines.push("No Feature binding changes.");
|
|
24865
|
+
} else {
|
|
24866
|
+
for (const kind of ["removed", "renamed", "moved", "added"]) {
|
|
24867
|
+
const entries = input.drift.filter((entry) => entry.kind === kind);
|
|
24868
|
+
if (!entries.length) continue;
|
|
24869
|
+
lines.push(`### ${kindHeadings[kind]}`);
|
|
24870
|
+
for (const entry of entries) {
|
|
24871
|
+
lines.push(
|
|
24872
|
+
`- \`${entry.featureKey ?? "unknown"}\`${entry.location ? ` \u2014 ${entry.location}` : ""}`
|
|
24873
|
+
);
|
|
24874
|
+
}
|
|
24875
|
+
lines.push("");
|
|
24876
|
+
}
|
|
24877
|
+
}
|
|
24878
|
+
if (input.proposedFeatureKeys?.length) {
|
|
24879
|
+
lines.push("### Found in code, not in your catalog");
|
|
24880
|
+
for (const featureKey of input.proposedFeatureKeys) {
|
|
24881
|
+
lines.push(`- \`${featureKey}\``);
|
|
24882
|
+
}
|
|
24883
|
+
lines.push("");
|
|
24884
|
+
}
|
|
24885
|
+
if (input.rejectedBindings?.length) {
|
|
24886
|
+
lines.push("### Evidence that could not be recorded");
|
|
24887
|
+
for (const rejection of input.rejectedBindings) {
|
|
24888
|
+
lines.push(
|
|
24889
|
+
`- \`${rejection.featureKey ?? "unknown"}\` \u2014 ${rejectionReasons[rejection.reason] ?? rejection.reason}`
|
|
24890
|
+
);
|
|
24891
|
+
}
|
|
24892
|
+
lines.push("");
|
|
24893
|
+
}
|
|
24894
|
+
return {
|
|
24895
|
+
// Never fail the build: a false positive blocking a merge queue is the
|
|
24896
|
+
// fastest way to get uninstalled.
|
|
24897
|
+
conclusion: "neutral",
|
|
24898
|
+
summary: lines.join("\n").trim(),
|
|
24899
|
+
title: breaking.length ? `${breaking.length} Feature${breaking.length === 1 ? "" : "s"} targeted by live Funnels removed` : input.drift.length ? `${input.drift.length} Feature binding change${input.drift.length === 1 ? "" : "s"}` : "No Feature binding changes"
|
|
24900
|
+
};
|
|
24901
|
+
}
|
|
24902
|
+
|
|
24903
|
+
// src/plan-branches.ts
|
|
24904
|
+
import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
24905
|
+
import { extname as extname2, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
24906
|
+
var maxPlanBranchFileCharacters = 2e6;
|
|
24907
|
+
var codeExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
24908
|
+
var nonDefinitionPattern = /(^|\/)(__tests__|__mocks__|tests?|specs?|fixtures?|testing|mocks?|mock-data|stories|__stories__)(\/)|\.(test|spec|d|stories)\.[cm]?[jt]sx?$/i;
|
|
24909
|
+
var comparisonPattern = /\b([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\s*(===|==|!==|!=)\s*(?:["'`]([A-Za-z][A-Za-z0-9_ -]{0,39})["'`]|([A-Za-z_$][\w$]*(?:\.[\w$]+){0,2}))/g;
|
|
24910
|
+
var planIdentifierPattern = /(plan|tier|level|subscription|package)/i;
|
|
24911
|
+
var enumOpenPattern = /\b(?:enum|const)\s+([A-Za-z_$][\w$]*)\s*(?:=\s*)?\{/;
|
|
24912
|
+
var memberPattern = /^\s*([A-Za-z_$][\w$]*)\s*[:=]\s*["'`]([A-Za-z][A-Za-z0-9_ -]{0,39})["'`]/;
|
|
24913
|
+
var blockClosePattern = /^\s*\}/;
|
|
24914
|
+
var simpleConstPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[\w<>[\]|\s]+)?=\s*["'`]([A-Za-z][A-Za-z0-9_ -]{0,39})["'`]/;
|
|
24915
|
+
var denialPattern = /\b(?:throw|redirect|notFound|forbidden)\b|\breturn\s+(?:null|false|undefined)\b|upgrade|paywall|locked|disabled|403/i;
|
|
24916
|
+
var restrictionNamePattern = /\b(?:needs?|requires?|must)[A-Z_]|\b(?:is|has)?(?:Locked|Blocked|Restricted|Gated|Disabled)|upgrade|paywall/i;
|
|
24917
|
+
var numericBranchPattern = /\?\s*[\d_]+\s*:\s*[\d_]+|:\s*[\d_]+\s*[,;)]/;
|
|
24918
|
+
var renderPattern = /<[A-Z][\w.]*|className=|return\s*\(/;
|
|
24919
|
+
var marketingPattern = /\b(?:upgrade|get started|contact sales|choose|current plan|most popular|per month|per year|\/mo\b|\/yr\b)|\$\{?\d/i;
|
|
24920
|
+
var escapePattern = /\breturn\s+(?:null|false|undefined)\b|\bthrow\b|\bredirect\b|\bnotFound\b/;
|
|
24921
|
+
function collectConstants(lines, into) {
|
|
24922
|
+
let container = null;
|
|
24923
|
+
for (const line of lines) {
|
|
24924
|
+
if (container && blockClosePattern.test(line)) {
|
|
24925
|
+
container = null;
|
|
24926
|
+
continue;
|
|
24927
|
+
}
|
|
24928
|
+
if (!container) {
|
|
24929
|
+
const open = line.match(enumOpenPattern);
|
|
24930
|
+
if (open?.[1]) {
|
|
24931
|
+
container = open[1];
|
|
24932
|
+
continue;
|
|
24933
|
+
}
|
|
24934
|
+
const simple = line.match(simpleConstPattern);
|
|
24935
|
+
if (simple?.[1] && simple[2]) record3(into, simple[1], simple[2]);
|
|
24936
|
+
continue;
|
|
24937
|
+
}
|
|
24938
|
+
const member = line.match(memberPattern);
|
|
24939
|
+
if (member?.[1] && member[2]) {
|
|
24940
|
+
record3(into, `${container}.${member[1]}`, member[2]);
|
|
24941
|
+
record3(into, member[1], member[2]);
|
|
24942
|
+
}
|
|
24943
|
+
}
|
|
24944
|
+
}
|
|
24945
|
+
function record3(into, name, value) {
|
|
24946
|
+
const normalized = value.toLowerCase();
|
|
24947
|
+
if (!into.has(name)) {
|
|
24948
|
+
into.set(name, normalized);
|
|
24949
|
+
return;
|
|
24950
|
+
}
|
|
24951
|
+
if (into.get(name) !== normalized) into.set(name, null);
|
|
24952
|
+
}
|
|
24953
|
+
function resolveConstant(index, reference) {
|
|
24954
|
+
const direct = index.get(reference);
|
|
24955
|
+
if (direct !== void 0) return direct;
|
|
24956
|
+
const segments = reference.split(".");
|
|
24957
|
+
if (segments.length > 1) {
|
|
24958
|
+
const tail = segments.slice(-2).join(".");
|
|
24959
|
+
const viaTail = index.get(tail);
|
|
24960
|
+
if (viaTail !== void 0) return viaTail;
|
|
24961
|
+
const viaMember = index.get(segments[segments.length - 1]);
|
|
24962
|
+
if (viaMember !== void 0) return viaMember;
|
|
24963
|
+
}
|
|
24964
|
+
return void 0;
|
|
24965
|
+
}
|
|
24966
|
+
function normalize(path) {
|
|
24967
|
+
return path.split(sep).join("/");
|
|
24968
|
+
}
|
|
24969
|
+
function allowed(relativePath, excludes) {
|
|
24970
|
+
if (!relativePath || relativePath.startsWith("..")) return false;
|
|
24971
|
+
const segments = relativePath.split("/");
|
|
24972
|
+
if (segments.some((segment) => ignoredSegments.has(segment))) return false;
|
|
24973
|
+
if (segments.some((segment) => /^\.next[-.]?/.test(segment))) return false;
|
|
24974
|
+
if (sensitivePathPattern.test(relativePath)) return false;
|
|
24975
|
+
if (nonDefinitionPattern.test(relativePath)) return false;
|
|
24976
|
+
return !excludes.some((exclude) => relativePath.startsWith(exclude));
|
|
24977
|
+
}
|
|
24978
|
+
function nearestSymbol2(lines, lineIndex) {
|
|
24979
|
+
for (let index = lineIndex; index >= Math.max(0, lineIndex - 20); index -= 1) {
|
|
24980
|
+
const match = lines[index]?.match(
|
|
24981
|
+
/(?:export\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/
|
|
24982
|
+
);
|
|
24983
|
+
if (match?.[1]) return match[1];
|
|
24984
|
+
}
|
|
24985
|
+
return "module";
|
|
24986
|
+
}
|
|
24987
|
+
function assignedName(line) {
|
|
24988
|
+
return line.match(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/)?.[1] ?? null;
|
|
24989
|
+
}
|
|
24990
|
+
function bindingUsages(lines, lineIndex, name) {
|
|
24991
|
+
const reference = new RegExp(`\\b${name}\\b`);
|
|
24992
|
+
const usages = [];
|
|
24993
|
+
lines.forEach((line, index) => {
|
|
24994
|
+
if (index === lineIndex || !reference.test(line)) return;
|
|
24995
|
+
usages.push(line);
|
|
24996
|
+
});
|
|
24997
|
+
return usages.join("\n");
|
|
24998
|
+
}
|
|
24999
|
+
function shapeAt(lines, lineIndex) {
|
|
25000
|
+
const line = lines[lineIndex] ?? "";
|
|
25001
|
+
const window = lines.slice(lineIndex, lineIndex + 4).join("\n");
|
|
25002
|
+
if (numericBranchPattern.test(line)) return "limit";
|
|
25003
|
+
if (renderPattern.test(window) && marketingPattern.test(window) && !escapePattern.test(window)) {
|
|
25004
|
+
return "presentation";
|
|
25005
|
+
}
|
|
25006
|
+
return "boolean";
|
|
25007
|
+
}
|
|
25008
|
+
function polarityAt(lines, lineIndex, negated) {
|
|
25009
|
+
const window = lines.slice(lineIndex, lineIndex + 3).join("\n");
|
|
25010
|
+
const bound = assignedName(lines[lineIndex] ?? "");
|
|
25011
|
+
const named = bound ? restrictionNamePattern.test(bound) : false;
|
|
25012
|
+
const usages = bound ? bindingUsages(lines, lineIndex, bound) : "";
|
|
25013
|
+
if (!named && !denialPattern.test(window) && !denialPattern.test(usages)) {
|
|
25014
|
+
return "unclear";
|
|
25015
|
+
}
|
|
25016
|
+
return negated ? "unclear" : "deny";
|
|
25017
|
+
}
|
|
25018
|
+
function clusterPlanBranches(branches) {
|
|
25019
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
25020
|
+
for (const branch of branches) {
|
|
25021
|
+
const segments = branch.repositoryPath.split("/");
|
|
25022
|
+
const location = segments.slice(0, -1).join("/") || ".";
|
|
25023
|
+
const key2 = `${branch.shape}|${location}`;
|
|
25024
|
+
const existing = clusters.get(key2);
|
|
25025
|
+
if (existing) existing.push(branch);
|
|
25026
|
+
else clusters.set(key2, [branch]);
|
|
25027
|
+
}
|
|
25028
|
+
return [...clusters.entries()].map(([key2, grouped]) => {
|
|
25029
|
+
const polarities = new Set(grouped.map((branch) => branch.polarity));
|
|
25030
|
+
const decided = polarities.has("deny") && polarities.has("grant") ? "unclear" : polarities.has("deny") ? "deny" : polarities.has("grant") ? "grant" : "unclear";
|
|
25031
|
+
return {
|
|
25032
|
+
branches: grouped,
|
|
25033
|
+
location: key2.slice(key2.indexOf("|") + 1),
|
|
25034
|
+
planValues: [...new Set(grouped.map((branch) => branch.planValue))].sort(),
|
|
25035
|
+
polarity: decided,
|
|
25036
|
+
shape: grouped[0].shape
|
|
25037
|
+
};
|
|
25038
|
+
}).sort(
|
|
25039
|
+
(left, right) => right.branches.length - left.branches.length || left.location.localeCompare(right.location)
|
|
25040
|
+
);
|
|
25041
|
+
}
|
|
25042
|
+
async function discoverPlanBranches(input) {
|
|
25043
|
+
const excludes = input.excludes ?? [];
|
|
25044
|
+
const planValues = new Set(
|
|
25045
|
+
input.planValues.map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
25046
|
+
);
|
|
25047
|
+
if (!planValues.size) return [];
|
|
25048
|
+
const roots = input.roots ?? [".", ...defaultRoots];
|
|
25049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25050
|
+
const branches = [];
|
|
25051
|
+
const constants = /* @__PURE__ */ new Map();
|
|
25052
|
+
const pending = [];
|
|
25053
|
+
const queue = roots.map((root) => resolve2(input.cwd, root));
|
|
25054
|
+
while (queue.length) {
|
|
25055
|
+
const current = queue.shift();
|
|
25056
|
+
if (seen.has(current)) continue;
|
|
25057
|
+
seen.add(current);
|
|
25058
|
+
const relativePath = normalize(relative2(input.cwd, current));
|
|
25059
|
+
if (relativePath && !allowed(relativePath, excludes)) continue;
|
|
25060
|
+
let entryStat;
|
|
25061
|
+
try {
|
|
25062
|
+
entryStat = await stat2(current);
|
|
25063
|
+
} catch {
|
|
25064
|
+
continue;
|
|
25065
|
+
}
|
|
25066
|
+
if (entryStat.isDirectory()) {
|
|
25067
|
+
const entries = await readdir2(current, { withFileTypes: true });
|
|
25068
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
25069
|
+
queue.push(join2(current, entry.name));
|
|
25070
|
+
}
|
|
25071
|
+
continue;
|
|
25072
|
+
}
|
|
25073
|
+
if (!entryStat.isFile() || !relativePath) continue;
|
|
25074
|
+
if (!codeExtensions.has(extname2(current).toLowerCase())) continue;
|
|
25075
|
+
if (entryStat.size > maxPlanBranchFileCharacters) continue;
|
|
25076
|
+
const lines = (await readFile2(current, "utf8")).split(/\r?\n/);
|
|
25077
|
+
collectConstants(lines, constants);
|
|
25078
|
+
lines.forEach((line, lineIndex) => {
|
|
25079
|
+
comparisonPattern.lastIndex = 0;
|
|
25080
|
+
let match;
|
|
25081
|
+
while (match = comparisonPattern.exec(line)) {
|
|
25082
|
+
const [, identifier, operator, literal2, reference] = match;
|
|
25083
|
+
if (!planIdentifierPattern.test(identifier ?? "")) continue;
|
|
25084
|
+
if (!literal2 && !reference) continue;
|
|
25085
|
+
pending.push({
|
|
25086
|
+
line: lineIndex + 1,
|
|
25087
|
+
literal: literal2 ?? null,
|
|
25088
|
+
negated: operator.startsWith("!"),
|
|
25089
|
+
polarity: polarityAt(lines, lineIndex, operator.startsWith("!")),
|
|
25090
|
+
reference: reference ?? null,
|
|
25091
|
+
repositoryPath: relativePath,
|
|
25092
|
+
shape: shapeAt(lines, lineIndex),
|
|
25093
|
+
symbol: nearestSymbol2(lines, lineIndex)
|
|
25094
|
+
});
|
|
25095
|
+
}
|
|
25096
|
+
});
|
|
25097
|
+
}
|
|
25098
|
+
for (const item of pending) {
|
|
25099
|
+
const value = item.literal ? item.literal.toLowerCase() : resolveConstant(constants, item.reference);
|
|
25100
|
+
if (!value || !planValues.has(value)) continue;
|
|
25101
|
+
branches.push({
|
|
25102
|
+
line: item.line,
|
|
25103
|
+
planValue: value,
|
|
25104
|
+
polarity: item.polarity,
|
|
25105
|
+
repositoryPath: item.repositoryPath,
|
|
25106
|
+
shape: item.shape,
|
|
25107
|
+
symbol: item.symbol
|
|
25108
|
+
});
|
|
25109
|
+
}
|
|
25110
|
+
return branches.sort(
|
|
25111
|
+
(left, right) => left.repositoryPath.localeCompare(right.repositoryPath) || left.line - right.line
|
|
25112
|
+
);
|
|
25113
|
+
}
|
|
25114
|
+
|
|
25115
|
+
// src/plan-sources.ts
|
|
25116
|
+
import { readFile as readFile3, readdir as readdir3, stat as stat3, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
25117
|
+
import { dirname as dirname2, extname as extname3, join as join3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
25118
|
+
var planSourceApprovalPath = ".saasfunnels/plan-sources.json";
|
|
25119
|
+
var maxPlanSourceFiles = 12;
|
|
25120
|
+
var maxPlanSourceCharacters = 512e3;
|
|
25121
|
+
var planSourceStems = [
|
|
25122
|
+
"billing",
|
|
25123
|
+
"entitlement",
|
|
25124
|
+
"entitlements",
|
|
25125
|
+
"plan",
|
|
25126
|
+
"plans",
|
|
25127
|
+
"price",
|
|
25128
|
+
"prices",
|
|
25129
|
+
"pricing",
|
|
25130
|
+
"subscription",
|
|
25131
|
+
"subscriptions",
|
|
25132
|
+
"tier",
|
|
25133
|
+
"tiers"
|
|
25134
|
+
];
|
|
25135
|
+
var planSourceKindByExtension = /* @__PURE__ */ new Map([
|
|
25136
|
+
[".cjs", "typescript"],
|
|
25137
|
+
[".js", "typescript"],
|
|
25138
|
+
[".json", "json"],
|
|
25139
|
+
[".jsonc", "json"],
|
|
25140
|
+
[".mjs", "typescript"],
|
|
25141
|
+
[".ts", "typescript"],
|
|
25142
|
+
[".tsx", "typescript"],
|
|
25143
|
+
[".yaml", "yaml"],
|
|
25144
|
+
[".yml", "yaml"]
|
|
25145
|
+
]);
|
|
25146
|
+
var billingIntervalPattern = /\b(month|monthly|year|yearly|annual|annually|week|weekly|day|daily|recurring|interval|billing_period)\b/i;
|
|
25147
|
+
var chargeAmountPattern = /\b(unit_amount|unit_amount_decimal|amount|amount_decimal|price|prices|currency|usd|eur|gbp|cents)\b/i;
|
|
25148
|
+
var stripePriceIdPattern = /["'`]price_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{14,}["'`]/;
|
|
25149
|
+
var nonDefinitionPattern2 = /(^|\/)(__tests__|__mocks__|tests?|specs?)(\/)|\.(test|spec|d)\.[cm]?[jt]sx?$/i;
|
|
25150
|
+
var maxNamedOnlyCandidates = 10;
|
|
25151
|
+
function normalize2(path) {
|
|
25152
|
+
return path.split(sep2).join("/");
|
|
25153
|
+
}
|
|
25154
|
+
function allowed2(relativePath, excludes) {
|
|
25155
|
+
if (!relativePath || relativePath.startsWith("..")) return false;
|
|
25156
|
+
const segments = relativePath.split("/");
|
|
25157
|
+
if (segments.some((segment) => ignoredSegments.has(segment))) return false;
|
|
25158
|
+
if (segments.some((segment) => /^\.next[-.]?/.test(segment))) return false;
|
|
25159
|
+
if (sensitivePathPattern.test(relativePath)) return false;
|
|
25160
|
+
if (nonDefinitionPattern2.test(relativePath)) return false;
|
|
25161
|
+
return !excludes.some((exclude) => relativePath.startsWith(exclude));
|
|
25162
|
+
}
|
|
25163
|
+
function looksLikePlanSource(relativePath) {
|
|
25164
|
+
const base = relativePath.split("/").pop() ?? "";
|
|
25165
|
+
const stem = base.slice(0, base.length - extname3(base).length).toLowerCase();
|
|
25166
|
+
const parts = stem.split(/[^a-z0-9]+/).filter(Boolean);
|
|
25167
|
+
return parts.some((part) => planSourceStems.includes(part));
|
|
25168
|
+
}
|
|
25169
|
+
async function discoverPlanSourceCandidates(input) {
|
|
25170
|
+
const excludes = input.excludes ?? [];
|
|
25171
|
+
const roots = input.roots ?? [".", "config", ...defaultRoots];
|
|
25172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25173
|
+
const candidates = [];
|
|
25174
|
+
const queue = roots.map((root) => resolve3(input.cwd, root));
|
|
25175
|
+
while (queue.length && candidates.length < 200) {
|
|
25176
|
+
const current = queue.shift();
|
|
25177
|
+
if (seen.has(current)) continue;
|
|
25178
|
+
seen.add(current);
|
|
25179
|
+
const relativePath = normalize2(relative3(input.cwd, current));
|
|
25180
|
+
if (relativePath && !allowed2(relativePath, excludes)) continue;
|
|
25181
|
+
let entryStat;
|
|
25182
|
+
try {
|
|
25183
|
+
entryStat = await stat3(current);
|
|
25184
|
+
} catch {
|
|
25185
|
+
continue;
|
|
25186
|
+
}
|
|
25187
|
+
if (entryStat.isDirectory()) {
|
|
25188
|
+
const entries = await readdir3(current, { withFileTypes: true });
|
|
25189
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
25190
|
+
queue.push(join3(current, entry.name));
|
|
25191
|
+
}
|
|
25192
|
+
continue;
|
|
25193
|
+
}
|
|
25194
|
+
if (!entryStat.isFile()) continue;
|
|
25195
|
+
const kind = planSourceKindByExtension.get(extname3(current).toLowerCase());
|
|
25196
|
+
if (!kind || !relativePath) continue;
|
|
25197
|
+
if (entryStat.size > maxPlanSourceCharacters) continue;
|
|
25198
|
+
const contents = await readFile3(current, "utf8");
|
|
25199
|
+
const hasStripePriceId = stripePriceIdPattern.test(contents);
|
|
25200
|
+
const named = looksLikePlanSource(relativePath);
|
|
25201
|
+
const looksPriced = billingIntervalPattern.test(contents) && chargeAmountPattern.test(contents);
|
|
25202
|
+
if (!hasStripePriceId && !(named && looksPriced)) continue;
|
|
25203
|
+
candidates.push({
|
|
25204
|
+
characters: contents.length,
|
|
25205
|
+
confidenceBasisPoints: hasStripePriceId ? 9e3 : 4e3,
|
|
25206
|
+
hasStripePriceId,
|
|
25207
|
+
kind,
|
|
25208
|
+
path: relativePath,
|
|
25209
|
+
rationale: hasStripePriceId ? named ? "Filename suggests commercial configuration and the file references a Stripe price." : "The file references a Stripe price." : "Filename suggests commercial configuration and the file states amounts and billing intervals."
|
|
25210
|
+
});
|
|
25211
|
+
}
|
|
25212
|
+
const sorted = candidates.sort(
|
|
25213
|
+
(left, right) => right.confidenceBasisPoints - left.confidenceBasisPoints || left.path.localeCompare(right.path)
|
|
25214
|
+
);
|
|
25215
|
+
const confirmed = sorted.filter((candidate) => candidate.hasStripePriceId);
|
|
25216
|
+
const namedOnly = sorted.filter((candidate) => !candidate.hasStripePriceId);
|
|
25217
|
+
return [...confirmed, ...namedOnly.slice(0, maxNamedOnlyCandidates)];
|
|
25218
|
+
}
|
|
25219
|
+
async function readApprovedPlanSources(cwd2) {
|
|
25220
|
+
try {
|
|
25221
|
+
const raw = await readFile3(resolve3(cwd2, planSourceApprovalPath), "utf8");
|
|
25222
|
+
const parsed = JSON.parse(raw);
|
|
25223
|
+
return Array.isArray(parsed.files) ? parsed.files.filter((value) => typeof value === "string") : [];
|
|
25224
|
+
} catch {
|
|
25225
|
+
return [];
|
|
25226
|
+
}
|
|
25227
|
+
}
|
|
25228
|
+
async function writeApprovedPlanSources(cwd2, files) {
|
|
25229
|
+
const target = resolve3(cwd2, planSourceApprovalPath);
|
|
25230
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
25231
|
+
await writeFile2(
|
|
25232
|
+
target,
|
|
25233
|
+
`${JSON.stringify(
|
|
25234
|
+
{
|
|
25235
|
+
files: [...files].sort(),
|
|
25236
|
+
note: "Only these files are uploaded for plan mapping. Review before committing.",
|
|
25237
|
+
schemaVersion: 1
|
|
25238
|
+
},
|
|
25239
|
+
null,
|
|
25240
|
+
2
|
|
25241
|
+
)}
|
|
25242
|
+
`,
|
|
25243
|
+
"utf8"
|
|
25244
|
+
);
|
|
25245
|
+
return target;
|
|
25246
|
+
}
|
|
25247
|
+
async function buildPlanMappingHandoff(input) {
|
|
25248
|
+
if (input.files.length > maxPlanSourceFiles) {
|
|
25249
|
+
throw new Error(
|
|
25250
|
+
`Plan mapping accepts at most ${maxPlanSourceFiles} files; ${input.files.length} are approved. Remove some from ${planSourceApprovalPath}.`
|
|
25251
|
+
);
|
|
25252
|
+
}
|
|
25253
|
+
const observedAt = input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
25254
|
+
const inputs = [];
|
|
25255
|
+
for (const file2 of [...input.files].sort()) {
|
|
25256
|
+
const relativePath = normalize2(file2);
|
|
25257
|
+
if (!allowed2(relativePath, [])) {
|
|
25258
|
+
throw new Error(`${relativePath} is not a readable plan source path.`);
|
|
25259
|
+
}
|
|
25260
|
+
const kind = planSourceKindByExtension.get(extname3(relativePath).toLowerCase());
|
|
25261
|
+
if (!kind) {
|
|
25262
|
+
throw new Error(`${relativePath} is not a supported plan source type.`);
|
|
25263
|
+
}
|
|
25264
|
+
const content = await readFile3(resolve3(input.cwd, relativePath), "utf8");
|
|
25265
|
+
if (content.length > maxPlanSourceCharacters) {
|
|
25266
|
+
throw new Error(
|
|
25267
|
+
`${relativePath} is ${content.length} characters; the limit is ${maxPlanSourceCharacters}.`
|
|
25268
|
+
);
|
|
25269
|
+
}
|
|
25270
|
+
inputs.push({ content, kind, label: relativePath, observedAt });
|
|
25271
|
+
}
|
|
25272
|
+
return { inputs };
|
|
25273
|
+
}
|
|
25274
|
+
function planMappingRequestKey(input) {
|
|
25275
|
+
const slug = `${input.repositoryKey}:${input.repositoryRevision}`.toLowerCase().replace(/[^a-z0-9_.:-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
25276
|
+
return `plans-${slug}`.slice(0, 160);
|
|
25277
|
+
}
|
|
25278
|
+
|
|
24820
25279
|
// src/mcp.ts
|
|
24821
25280
|
import { createInterface } from "node:readline";
|
|
24822
25281
|
|
|
@@ -26112,7 +26571,15 @@ async function serveSaaSFunnelsMcp(options = {}) {
|
|
|
26112
26571
|
var cliSchemaVersion = "2026-07-02.1";
|
|
26113
26572
|
var targetNames = /* @__PURE__ */ new Set(["codex", "claude-code", "cursor", "markdown"]);
|
|
26114
26573
|
var sourceNames2 = /* @__PURE__ */ new Set(["direct", "posthog", "segment"]);
|
|
26115
|
-
var tokenPattern2 = /\b((?:pv|sk|pk|rk|whsec|xox[baprs]|gh[pousr])_[A-Za-z0-9_=-]{8,}|[A-Za-z0-9_-]{32,})\b/g;
|
|
26574
|
+
var tokenPattern2 = /\b((?:pv|sk|pk|rk|whsec|xox[baprs]|gh[pousr])_[A-Za-z0-9_=-]{8,}|(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{32,})\b/g;
|
|
26575
|
+
var pathValuedKeys = /* @__PURE__ */ new Set([
|
|
26576
|
+
"approvalPath",
|
|
26577
|
+
"file",
|
|
26578
|
+
"files",
|
|
26579
|
+
"location",
|
|
26580
|
+
"path",
|
|
26581
|
+
"repositoryPath"
|
|
26582
|
+
]);
|
|
26116
26583
|
function parseArgs2(argv2) {
|
|
26117
26584
|
const args = [];
|
|
26118
26585
|
const flags = {};
|
|
@@ -26158,9 +26625,30 @@ function result(exitCode, stdout = "", stderr = "") {
|
|
|
26158
26625
|
stdout: redactOutput(stdout)
|
|
26159
26626
|
};
|
|
26160
26627
|
}
|
|
26628
|
+
function redactStructured(value, key2) {
|
|
26629
|
+
if (typeof value === "string") {
|
|
26630
|
+
return key2 && pathValuedKeys.has(key2) ? value : redactOutput(value);
|
|
26631
|
+
}
|
|
26632
|
+
if (Array.isArray(value)) {
|
|
26633
|
+
return value.map((item) => redactStructured(item, key2));
|
|
26634
|
+
}
|
|
26635
|
+
if (value && typeof value === "object") {
|
|
26636
|
+
return Object.fromEntries(
|
|
26637
|
+
Object.entries(value).map(([name, item]) => [
|
|
26638
|
+
name,
|
|
26639
|
+
redactStructured(item, name)
|
|
26640
|
+
])
|
|
26641
|
+
);
|
|
26642
|
+
}
|
|
26643
|
+
return value;
|
|
26644
|
+
}
|
|
26161
26645
|
function jsonResult(exitCode, data) {
|
|
26162
|
-
return
|
|
26163
|
-
|
|
26646
|
+
return {
|
|
26647
|
+
exitCode,
|
|
26648
|
+
stderr: "",
|
|
26649
|
+
stdout: `${JSON.stringify(redactStructured(data), null, 2)}
|
|
26650
|
+
`
|
|
26651
|
+
};
|
|
26164
26652
|
}
|
|
26165
26653
|
function usage() {
|
|
26166
26654
|
return `${SAASFUNNELS_PRODUCT_NAME} CLI
|
|
@@ -26173,7 +26661,11 @@ Usage:
|
|
|
26173
26661
|
${SAASFUNNELS_CLI_NAME} events send-test [--file <file>] [--json]
|
|
26174
26662
|
${SAASFUNNELS_CLI_NAME} features setup [--root <paths>] [--exclude <paths>] [--accept <ids|all>] [--reject <ids>] [--map <id=key>] [--manifest-only] [--apply] [--json]
|
|
26175
26663
|
${SAASFUNNELS_CLI_NAME} features check --feature <key> --account-id <uuid> [--environment test] [--json]
|
|
26176
|
-
${SAASFUNNELS_CLI_NAME} features handoff [--file <path>] --repository-revision <revision> [--send] [--json]
|
|
26664
|
+
${SAASFUNNELS_CLI_NAME} features handoff [--file <path>] --repository-revision <revision> --repository-key <key> [--discovery-roots <paths>] [--scan-role series|candidate] [--producer cli|github_action|github_app] [--send] [--json]
|
|
26665
|
+
${SAASFUNNELS_CLI_NAME} plans discover [--root <paths>] [--exclude <paths>] [--apply] [--json]
|
|
26666
|
+
${SAASFUNNELS_CLI_NAME} plans branches --plans <names> [--root <paths>] [--json]
|
|
26667
|
+
${SAASFUNNELS_CLI_NAME} plans branches --plans <names> --repository-key <repo> --repository-revision <sha> --send
|
|
26668
|
+
${SAASFUNNELS_CLI_NAME} plans handoff --repository-key <key> --repository-revision <rev> --integration-id <uuid> [--send] [--json]
|
|
26177
26669
|
${SAASFUNNELS_CLI_NAME} catalog discover [feature setup options]
|
|
26178
26670
|
${SAASFUNNELS_CLI_NAME} catalog validate [file] [--json]
|
|
26179
26671
|
${SAASFUNNELS_CLI_NAME} catalog diff [feature setup options]
|
|
@@ -26196,10 +26688,10 @@ function flagList(flags, name) {
|
|
|
26196
26688
|
function filesystem(options) {
|
|
26197
26689
|
return options.fs ?? {
|
|
26198
26690
|
mkdir: async (path, mkdirOptions) => {
|
|
26199
|
-
await
|
|
26691
|
+
await mkdir3(path, mkdirOptions);
|
|
26200
26692
|
},
|
|
26201
|
-
readFile:
|
|
26202
|
-
writeFile:
|
|
26693
|
+
readFile: readFile4,
|
|
26694
|
+
writeFile: writeFile3
|
|
26203
26695
|
};
|
|
26204
26696
|
}
|
|
26205
26697
|
function cwd(options) {
|
|
@@ -26229,19 +26721,19 @@ function sourceFlag(flags) {
|
|
|
26229
26721
|
}
|
|
26230
26722
|
async function readJsonFile(filePath, options) {
|
|
26231
26723
|
const raw = await filesystem(options).readFile(
|
|
26232
|
-
|
|
26724
|
+
resolve4(cwd(options), filePath),
|
|
26233
26725
|
"utf8"
|
|
26234
26726
|
);
|
|
26235
26727
|
return JSON.parse(raw);
|
|
26236
26728
|
}
|
|
26237
26729
|
async function commandInit(flags, options) {
|
|
26238
26730
|
const fs = filesystem(options);
|
|
26239
|
-
const configPath =
|
|
26731
|
+
const configPath = join4(cwd(options), ".saasfunnels", "config.json");
|
|
26240
26732
|
const config2 = {
|
|
26241
26733
|
api_base_url: apiBaseUrl2(options, flags),
|
|
26242
26734
|
schema_version: cliSchemaVersion
|
|
26243
26735
|
};
|
|
26244
|
-
await fs.mkdir(
|
|
26736
|
+
await fs.mkdir(dirname3(configPath), { recursive: true });
|
|
26245
26737
|
await fs.writeFile(
|
|
26246
26738
|
configPath,
|
|
26247
26739
|
`${JSON.stringify(config2, null, 2)}
|
|
@@ -26276,9 +26768,9 @@ async function commandAgentInstall(args, flags, options) {
|
|
|
26276
26768
|
const artifacts = getDeveloperToolAgentHandoffArtifacts({ endpoint });
|
|
26277
26769
|
const artifact = artifacts[target];
|
|
26278
26770
|
const targetPath = flagString(flags, "path") ?? saasFunnelsPublicText(artifact.target_path);
|
|
26279
|
-
const absoluteTarget =
|
|
26771
|
+
const absoluteTarget = join4(cwd(options), targetPath);
|
|
26280
26772
|
const fs = filesystem(options);
|
|
26281
|
-
await fs.mkdir(
|
|
26773
|
+
await fs.mkdir(dirname3(absoluteTarget), { recursive: true });
|
|
26282
26774
|
await fs.writeFile(
|
|
26283
26775
|
absoluteTarget,
|
|
26284
26776
|
saasFunnelsPublicText(artifact.file_contents),
|
|
@@ -26396,8 +26888,29 @@ async function commandFeatures(args, flags, options) {
|
|
|
26396
26888
|
"Missing --repository-revision for the structured Feature handoff.\n"
|
|
26397
26889
|
);
|
|
26398
26890
|
}
|
|
26891
|
+
const repositoryKey = flagString(flags, "repository-key");
|
|
26892
|
+
if (!repositoryKey) {
|
|
26893
|
+
return result(
|
|
26894
|
+
2,
|
|
26895
|
+
"",
|
|
26896
|
+
"Missing --repository-key for the structured Feature handoff.\n"
|
|
26897
|
+
);
|
|
26898
|
+
}
|
|
26899
|
+
const scanRole = flagString(flags, "scan-role") ?? "series";
|
|
26900
|
+
if (scanRole !== "series" && scanRole !== "candidate") {
|
|
26901
|
+
return result(2, "", "Use --scan-role series or --scan-role candidate.\n");
|
|
26902
|
+
}
|
|
26903
|
+
const producer = flagString(flags, "producer") ?? "cli";
|
|
26904
|
+
if (producer !== "cli" && producer !== "github_action" && producer !== "github_app") {
|
|
26905
|
+
return result(
|
|
26906
|
+
2,
|
|
26907
|
+
"",
|
|
26908
|
+
"Use --producer cli, --producer github_action, or --producer github_app.\n"
|
|
26909
|
+
);
|
|
26910
|
+
}
|
|
26911
|
+
const discoveryRoots = (flagString(flags, "discovery-roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean);
|
|
26399
26912
|
const source = await filesystem(options).readFile(
|
|
26400
|
-
|
|
26913
|
+
resolve4(cwd(options), path),
|
|
26401
26914
|
"utf8"
|
|
26402
26915
|
);
|
|
26403
26916
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -26413,7 +26926,7 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
|
|
|
26413
26926
|
let sdkVersions = [];
|
|
26414
26927
|
try {
|
|
26415
26928
|
const packageSource = await filesystem(options).readFile(
|
|
26416
|
-
|
|
26929
|
+
resolve4(cwd(options), "package.json"),
|
|
26417
26930
|
"utf8"
|
|
26418
26931
|
);
|
|
26419
26932
|
sdkVersions = featureSdkVersionsFromPackageJson(
|
|
@@ -26422,8 +26935,12 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
|
|
|
26422
26935
|
} catch {
|
|
26423
26936
|
}
|
|
26424
26937
|
const handoff = buildFeatureInstrumentationHandoff({
|
|
26938
|
+
discoveryRoots,
|
|
26425
26939
|
manifest: validation.manifest,
|
|
26940
|
+
producer,
|
|
26941
|
+
repositoryKey,
|
|
26426
26942
|
repositoryRevision,
|
|
26943
|
+
scanRole,
|
|
26427
26944
|
sdkVersions
|
|
26428
26945
|
});
|
|
26429
26946
|
if (hasFlag(flags, "send")) {
|
|
@@ -26434,8 +26951,9 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
|
|
|
26434
26951
|
"",
|
|
26435
26952
|
"Missing SAASFUNNELS_API_KEY with features:write scope for --send.\n"
|
|
26436
26953
|
);
|
|
26954
|
+
const endpoint = scanRole === "candidate" ? "/api/developer-tools/features/instrumentation/preview" : "/api/developer-tools/features/instrumentation";
|
|
26437
26955
|
const response = await (options.fetch ?? fetch)(
|
|
26438
|
-
`${apiBaseUrl2(options, flags)}
|
|
26956
|
+
`${apiBaseUrl2(options, flags)}${endpoint}`,
|
|
26439
26957
|
{
|
|
26440
26958
|
body: JSON.stringify(handoff),
|
|
26441
26959
|
headers: {
|
|
@@ -26446,13 +26964,19 @@ ${validation.errors.map((error51) => `- ${error51}`).join("\n")}
|
|
|
26446
26964
|
}
|
|
26447
26965
|
);
|
|
26448
26966
|
const body = await response.json();
|
|
26449
|
-
|
|
26967
|
+
const reported = response.ok ? {
|
|
26968
|
+
...body,
|
|
26969
|
+
checkSummary: formatFeatureDriftCheckSummary({
|
|
26970
|
+
drift: body?.data?.drift ?? [],
|
|
26971
|
+
rejectedBindings: body?.data?.rejectedBindings ?? []
|
|
26972
|
+
})
|
|
26973
|
+
} : body;
|
|
26974
|
+
return jsonMode(flags) ? jsonResult(response.ok ? 0 : 1, reported) : response.ok ? result(
|
|
26450
26975
|
0,
|
|
26451
26976
|
"Feature instrumentation handoff accepted. Open the Features review link returned by SaaSFunnels.\n"
|
|
26452
26977
|
) : result(1, "", "Feature instrumentation handoff was rejected.\n");
|
|
26453
26978
|
}
|
|
26454
|
-
return jsonMode(flags) ? jsonResult(0, handoff) :
|
|
26455
|
-
`);
|
|
26979
|
+
return jsonMode(flags) ? jsonResult(0, handoff) : jsonResult(0, handoff);
|
|
26456
26980
|
}
|
|
26457
26981
|
return result(
|
|
26458
26982
|
2,
|
|
@@ -26473,7 +26997,7 @@ async function commandCatalog(args, flags, options) {
|
|
|
26473
26997
|
if (action === "validate") {
|
|
26474
26998
|
const path = args[1] ?? ".saasfunnels/catalog.yaml";
|
|
26475
26999
|
const source = await filesystem(options).readFile(
|
|
26476
|
-
|
|
27000
|
+
resolve4(cwd(options), path),
|
|
26477
27001
|
"utf8"
|
|
26478
27002
|
);
|
|
26479
27003
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -26723,7 +27247,7 @@ async function commandVerify(flags, options) {
|
|
|
26723
27247
|
if (catalogPath) {
|
|
26724
27248
|
try {
|
|
26725
27249
|
const source = await filesystem(options).readFile(
|
|
26726
|
-
|
|
27250
|
+
resolve4(cwd(options), catalogPath),
|
|
26727
27251
|
"utf8"
|
|
26728
27252
|
);
|
|
26729
27253
|
const validation = parseFeatureManifestSource(source);
|
|
@@ -26831,6 +27355,236 @@ async function commandMcp(args, flags, options) {
|
|
|
26831
27355
|
return result(2, "", `Usage: ${SAASFUNNELS_CLI_NAME} mcp serve
|
|
26832
27356
|
`);
|
|
26833
27357
|
}
|
|
27358
|
+
async function commandPlans(args, flags, options) {
|
|
27359
|
+
if (args[0] === "branches") {
|
|
27360
|
+
const planValues = flagList(flags, "plans");
|
|
27361
|
+
if (!planValues.length)
|
|
27362
|
+
return result(
|
|
27363
|
+
2,
|
|
27364
|
+
"",
|
|
27365
|
+
"Missing --plans with the plan names from your Stripe catalog.\n"
|
|
27366
|
+
);
|
|
27367
|
+
const branches = await discoverPlanBranches({
|
|
27368
|
+
cwd: cwd(options),
|
|
27369
|
+
excludes: flagList(flags, "exclude"),
|
|
27370
|
+
planValues,
|
|
27371
|
+
roots: flagList(flags, "root").length ? flagList(flags, "root") : void 0
|
|
27372
|
+
});
|
|
27373
|
+
const clusters = clusterPlanBranches(branches);
|
|
27374
|
+
if (flags.json) {
|
|
27375
|
+
return jsonResult(0, { branches, clusters });
|
|
27376
|
+
}
|
|
27377
|
+
if (!branches.length) {
|
|
27378
|
+
return result(
|
|
27379
|
+
0,
|
|
27380
|
+
"No plan branches found. This codebase may read entitlements rather than comparing plan names.\n",
|
|
27381
|
+
""
|
|
27382
|
+
);
|
|
27383
|
+
}
|
|
27384
|
+
const lines = [
|
|
27385
|
+
`${branches.length} plan branch(es) in ${clusters.length} group(s):`,
|
|
27386
|
+
""
|
|
27387
|
+
];
|
|
27388
|
+
for (const cluster of clusters) {
|
|
27389
|
+
const polarity = cluster.polarity === "deny" ? "restricts" : cluster.polarity === "grant" ? "grants" : "unclear";
|
|
27390
|
+
lines.push(
|
|
27391
|
+
` ${cluster.location} \u2014 ${cluster.planValues.join(", ")} (${cluster.shape}, ${polarity}, ${cluster.branches.length} site(s))`
|
|
27392
|
+
);
|
|
27393
|
+
}
|
|
27394
|
+
lines.push("");
|
|
27395
|
+
lines.push("Each group is one place your product differs by plan. Naming what");
|
|
27396
|
+
lines.push("each one gates is a review step; nothing is guessed here.");
|
|
27397
|
+
if (!hasFlag(flags, "send")) {
|
|
27398
|
+
lines.push("");
|
|
27399
|
+
lines.push("Add --send to hand these to your workspace for review.");
|
|
27400
|
+
return result(0, `${lines.join("\n")}
|
|
27401
|
+
`, "");
|
|
27402
|
+
}
|
|
27403
|
+
const repositoryKey = flagString(flags, "repository-key");
|
|
27404
|
+
if (!repositoryKey)
|
|
27405
|
+
return result(2, "", "Missing --repository-key for the plan branch handoff.\n");
|
|
27406
|
+
const repositoryRevision = flagString(flags, "repository-revision");
|
|
27407
|
+
if (!repositoryRevision)
|
|
27408
|
+
return result(2, "", "Missing --repository-revision for the plan branch handoff.\n");
|
|
27409
|
+
const key2 = envValue2(options, SAASFUNNELS_ENV.apiKey);
|
|
27410
|
+
if (!key2)
|
|
27411
|
+
return result(2, "", "Missing SAASFUNNELS_API_KEY with features:write for --send.\n");
|
|
27412
|
+
const payload = {
|
|
27413
|
+
clusters: clusters.map((cluster) => ({
|
|
27414
|
+
branchCount: cluster.branches.length,
|
|
27415
|
+
branches: cluster.branches.map((branch) => ({
|
|
27416
|
+
line: branch.line,
|
|
27417
|
+
planValue: branch.planValue,
|
|
27418
|
+
polarity: branch.polarity,
|
|
27419
|
+
repositoryPath: branch.repositoryPath,
|
|
27420
|
+
shape: branch.shape,
|
|
27421
|
+
symbol: branch.symbol
|
|
27422
|
+
})),
|
|
27423
|
+
location: cluster.location,
|
|
27424
|
+
planValues: cluster.planValues,
|
|
27425
|
+
polarity: cluster.polarity,
|
|
27426
|
+
shape: cluster.shape
|
|
27427
|
+
})),
|
|
27428
|
+
environment: flagString(flags, "environment") ?? "test",
|
|
27429
|
+
planValues,
|
|
27430
|
+
producer: flagString(flags, "producer") ?? "cli",
|
|
27431
|
+
repositoryKey,
|
|
27432
|
+
repositoryRevision,
|
|
27433
|
+
schemaVersion: 1
|
|
27434
|
+
};
|
|
27435
|
+
const response = await (options.fetch ?? fetch)(
|
|
27436
|
+
`${apiBaseUrl2(options, flags)}/api/developer-tools/plans/branches`,
|
|
27437
|
+
{
|
|
27438
|
+
body: JSON.stringify(payload),
|
|
27439
|
+
headers: {
|
|
27440
|
+
authorization: `Bearer ${key2}`,
|
|
27441
|
+
"content-type": "application/json"
|
|
27442
|
+
},
|
|
27443
|
+
method: "POST"
|
|
27444
|
+
}
|
|
27445
|
+
);
|
|
27446
|
+
const body = await response.json();
|
|
27447
|
+
if (!response.ok)
|
|
27448
|
+
return result(1, "", `${body.error ?? "The plan branch handoff failed."}
|
|
27449
|
+
`);
|
|
27450
|
+
if (jsonMode(flags)) return jsonResult(0, body.data ?? {});
|
|
27451
|
+
lines.push("");
|
|
27452
|
+
lines.push(`Sent ${body.data?.clusterCount ?? clusters.length} group(s) for review.`);
|
|
27453
|
+
if (body.data?.retainedReviews)
|
|
27454
|
+
lines.push(
|
|
27455
|
+
`${body.data.retainedReviews} already reviewed and left as answered.`
|
|
27456
|
+
);
|
|
27457
|
+
if (body.data?.reviewUrl) lines.push(body.data.reviewUrl);
|
|
27458
|
+
return result(0, `${lines.join("\n")}
|
|
27459
|
+
`, "");
|
|
27460
|
+
}
|
|
27461
|
+
if (args[0] === "discover") {
|
|
27462
|
+
const candidates = await discoverPlanSourceCandidates({
|
|
27463
|
+
cwd: cwd(options),
|
|
27464
|
+
excludes: flagList(flags, "exclude"),
|
|
27465
|
+
roots: flagList(flags, "root").length ? flagList(flags, "root") : void 0
|
|
27466
|
+
});
|
|
27467
|
+
if (!candidates.length) {
|
|
27468
|
+
return jsonMode(flags) ? jsonResult(0, { candidates: [], ok: true }) : result(
|
|
27469
|
+
0,
|
|
27470
|
+
"No plan or pricing definition files found. Map plans to Features in SaaSFunnels instead.\n"
|
|
27471
|
+
);
|
|
27472
|
+
}
|
|
27473
|
+
const approved = candidates.slice(0, maxPlanSourceFiles);
|
|
27474
|
+
if (hasFlag(flags, "apply")) {
|
|
27475
|
+
if (options.prompt) {
|
|
27476
|
+
const confirmation = await options.prompt(
|
|
27477
|
+
`Approve these files for upload to SaaSFunnels: ${approved.map((candidate) => candidate.path).join(", ")}? [y/N] `
|
|
27478
|
+
);
|
|
27479
|
+
if (!/^y(?:es)?\$/i.test(confirmation.trim())) {
|
|
27480
|
+
return result(2, "", "Plan source approval was cancelled.\n");
|
|
27481
|
+
}
|
|
27482
|
+
}
|
|
27483
|
+
await writeApprovedPlanSources(
|
|
27484
|
+
cwd(options),
|
|
27485
|
+
approved.map((candidate) => candidate.path)
|
|
27486
|
+
);
|
|
27487
|
+
}
|
|
27488
|
+
if (jsonMode(flags))
|
|
27489
|
+
return jsonResult(0, {
|
|
27490
|
+
approvalPath: planSourceApprovalPath,
|
|
27491
|
+
candidates,
|
|
27492
|
+
ok: true,
|
|
27493
|
+
written: hasFlag(flags, "apply")
|
|
27494
|
+
});
|
|
27495
|
+
const confirmed = candidates.filter((candidate) => candidate.hasStripePriceId);
|
|
27496
|
+
const namedOnly = candidates.filter((candidate) => !candidate.hasStripePriceId);
|
|
27497
|
+
const lines = [];
|
|
27498
|
+
if (confirmed.length) {
|
|
27499
|
+
lines.push("Defines Stripe prices:");
|
|
27500
|
+
confirmed.forEach(
|
|
27501
|
+
(candidate) => lines.push(` ${candidate.path} (${candidate.kind})`)
|
|
27502
|
+
);
|
|
27503
|
+
}
|
|
27504
|
+
if (namedOnly.length) {
|
|
27505
|
+
if (confirmed.length) lines.push("");
|
|
27506
|
+
lines.push(
|
|
27507
|
+
confirmed.length ? "Named like pricing configuration, no Stripe price found:" : "No file defines a Stripe price. These matched by name only:"
|
|
27508
|
+
);
|
|
27509
|
+
namedOnly.forEach(
|
|
27510
|
+
(candidate) => lines.push(` ${candidate.path} (${candidate.kind})`)
|
|
27511
|
+
);
|
|
27512
|
+
}
|
|
27513
|
+
return result(
|
|
27514
|
+
0,
|
|
27515
|
+
`${lines.join("\n")}
|
|
27516
|
+
|
|
27517
|
+
${hasFlag(flags, "apply") ? `Approved ${approved.length} file(s) in ${planSourceApprovalPath}. Review and commit it.` : `Re-run with --apply to record these in ${planSourceApprovalPath}.`}
|
|
27518
|
+
`
|
|
27519
|
+
);
|
|
27520
|
+
}
|
|
27521
|
+
if (args[0] === "handoff") {
|
|
27522
|
+
const repositoryKey = flagString(flags, "repository-key");
|
|
27523
|
+
if (!repositoryKey)
|
|
27524
|
+
return result(2, "", "Missing --repository-key for the plan mapping handoff.\n");
|
|
27525
|
+
const repositoryRevision = flagString(flags, "repository-revision");
|
|
27526
|
+
if (!repositoryRevision)
|
|
27527
|
+
return result(
|
|
27528
|
+
2,
|
|
27529
|
+
"",
|
|
27530
|
+
"Missing --repository-revision for the plan mapping handoff.\n"
|
|
27531
|
+
);
|
|
27532
|
+
const integrationId = flagString(flags, "integration-id");
|
|
27533
|
+
if (!integrationId)
|
|
27534
|
+
return result(2, "", "Missing --integration-id for the plan mapping handoff.\n");
|
|
27535
|
+
const files = await readApprovedPlanSources(cwd(options));
|
|
27536
|
+
if (!files.length)
|
|
27537
|
+
return result(
|
|
27538
|
+
2,
|
|
27539
|
+
"",
|
|
27540
|
+
`No approved plan sources. Run "${SAASFUNNELS_CLI_NAME} plans discover --apply" and commit ${planSourceApprovalPath}.
|
|
27541
|
+
`
|
|
27542
|
+
);
|
|
27543
|
+
const { inputs } = await buildPlanMappingHandoff({ cwd: cwd(options), files });
|
|
27544
|
+
const requestKey = planMappingRequestKey({ repositoryKey, repositoryRevision });
|
|
27545
|
+
if (!hasFlag(flags, "send")) {
|
|
27546
|
+
return jsonMode(flags) ? jsonResult(0, {
|
|
27547
|
+
files: inputs.map((entry) => entry.label),
|
|
27548
|
+
ok: true,
|
|
27549
|
+
requestKey
|
|
27550
|
+
}) : result(
|
|
27551
|
+
0,
|
|
27552
|
+
`Would upload for plan mapping:
|
|
27553
|
+
${inputs.map((entry) => `- ${entry.label}`).join("\n")}
|
|
27554
|
+
|
|
27555
|
+
Add --send to upload.
|
|
27556
|
+
`
|
|
27557
|
+
);
|
|
27558
|
+
}
|
|
27559
|
+
const key2 = envValue2(options, SAASFUNNELS_ENV.apiKey);
|
|
27560
|
+
if (!key2)
|
|
27561
|
+
return result(
|
|
27562
|
+
2,
|
|
27563
|
+
"",
|
|
27564
|
+
"Missing SAASFUNNELS_API_KEY with catalog access for --send.\n"
|
|
27565
|
+
);
|
|
27566
|
+
const response = await (options.fetch ?? fetch)(
|
|
27567
|
+
`${apiBaseUrl2(options, flags)}/api/funnels/catalog/plan-mapping/imports`,
|
|
27568
|
+
{
|
|
27569
|
+
body: JSON.stringify({ inputs, integrationId, requestKey }),
|
|
27570
|
+
headers: {
|
|
27571
|
+
authorization: `Bearer ${key2}`,
|
|
27572
|
+
"content-type": "application/json"
|
|
27573
|
+
},
|
|
27574
|
+
method: "POST"
|
|
27575
|
+
}
|
|
27576
|
+
);
|
|
27577
|
+
const body = await response.json();
|
|
27578
|
+
return jsonMode(flags) ? jsonResult(response.ok ? 0 : 1, body) : response.ok ? result(
|
|
27579
|
+
0,
|
|
27580
|
+
`Staged ${inputs.length} plan source(s) for review in SaaSFunnels.
|
|
27581
|
+
`
|
|
27582
|
+
) : result(1, "", "The plan mapping handoff was rejected.\n");
|
|
27583
|
+
}
|
|
27584
|
+
return result(2, "", `Unknown plans command.
|
|
27585
|
+
|
|
27586
|
+
${usage()}`);
|
|
27587
|
+
}
|
|
26834
27588
|
async function runSaaSFunnelsCli(argv2, options = {}) {
|
|
26835
27589
|
const parsed = parseArgs2(argv2);
|
|
26836
27590
|
const [command, ...args] = parsed.args;
|
|
@@ -26846,6 +27600,8 @@ async function runSaaSFunnelsCli(argv2, options = {}) {
|
|
|
26846
27600
|
return await commandFeatures(args, parsed.flags, options);
|
|
26847
27601
|
if (command === "catalog")
|
|
26848
27602
|
return await commandCatalog(args, parsed.flags, options);
|
|
27603
|
+
if (command === "plans")
|
|
27604
|
+
return await commandPlans(args, parsed.flags, options);
|
|
26849
27605
|
if (command === "doctor") return await commandDoctor(parsed.flags, options);
|
|
26850
27606
|
if (command === "readiness")
|
|
26851
27607
|
return await commandReadiness(parsed.flags, options);
|