Files
zCode-CLI-X/~/.npm-cache/@growthbook/growthbook@1.6.5@@@1/src/util.ts
admin 875c7f9b91 feat: Complete zCode CLI X with Telegram bot integration
- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation

🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
2026-05-05 09:01:26 +00:00

424 lines
11 KiB
TypeScript

import {
AutoExperiment,
AutoExperimentChangeType,
Polyfills,
UrlTarget,
UrlTargetType,
VariationRange,
} from "./types/growthbook";
const polyfills: Polyfills = {
fetch: globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined,
SubtleCrypto: globalThis.crypto ? globalThis.crypto.subtle : undefined,
EventSource: globalThis.EventSource,
};
export function getPolyfills(): Polyfills {
return polyfills;
}
function hashFnv32a(str: string): number {
let hval = 0x811c9dc5;
const l = str.length;
for (let i = 0; i < l; i++) {
hval ^= str.charCodeAt(i);
hval +=
(hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
return hval >>> 0;
}
export function hash(
seed: string,
value: string,
version: number,
): number | null {
// New unbiased hashing algorithm
if (version === 2) {
return (hashFnv32a(hashFnv32a(seed + value) + "") % 10000) / 10000;
}
// Original biased hashing algorithm (keep for backwards compatibility)
if (version === 1) {
return (hashFnv32a(value + seed) % 1000) / 1000;
}
// Unknown hash version
return null;
}
export function getEqualWeights(n: number): number[] {
if (n <= 0) return [];
return new Array(n).fill(1 / n);
}
export function inRange(n: number, range: VariationRange): boolean {
return n >= range[0] && n < range[1];
}
export function inNamespace(
hashValue: string,
namespace: [string, number, number],
): boolean {
const n = hash("__" + namespace[0], hashValue, 1);
if (n === null) return false;
return n >= namespace[1] && n < namespace[2];
}
export function chooseVariation(n: number, ranges: VariationRange[]): number {
for (let i = 0; i < ranges.length; i++) {
if (inRange(n, ranges[i])) {
return i;
}
}
return -1;
}
export function getUrlRegExp(regexString: string): RegExp | undefined {
try {
const escaped = regexString.replace(/([^\\])\//g, "$1\\/");
return new RegExp(escaped);
} catch (e) {
console.error(e);
return undefined;
}
}
export function isURLTargeted(url: string, targets: UrlTarget[]) {
if (!targets.length) return false;
let hasIncludeRules = false;
let isIncluded = false;
for (let i = 0; i < targets.length; i++) {
const match = _evalURLTarget(url, targets[i].type, targets[i].pattern);
if (targets[i].include === false) {
if (match) return false;
} else {
hasIncludeRules = true;
if (match) isIncluded = true;
}
}
return isIncluded || !hasIncludeRules;
}
function _evalSimpleUrlPart(
actual: string,
pattern: string,
isPath: boolean,
): boolean {
try {
// Escape special regex characters and change wildcard `_____` to `.*`
let escaped = pattern
.replace(/[*.+?^${}()|[\]\\]/g, "\\$&")
.replace(/_____/g, ".*");
if (isPath) {
// When matching pathname, make leading/trailing slashes optional
escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
}
const regex = new RegExp("^" + escaped + "$", "i");
return regex.test(actual);
} catch (e) {
return false;
}
}
function _evalSimpleUrlTarget(actual: URL, pattern: string) {
try {
// If a protocol is missing, but a host is specified, add `https://` to the front
// Use "_____" as the wildcard since `*` is not a valid hostname in some browsers
const expected = new URL(
pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"),
"https://_____",
);
// Compare each part of the URL separately
const comps: Array<[string, string, boolean]> = [
[actual.host, expected.host, false],
[actual.pathname, expected.pathname, true],
];
// We only want to compare hashes if it's explicitly being targeted
if (expected.hash) {
comps.push([actual.hash, expected.hash, false]);
}
expected.searchParams.forEach((v, k) => {
comps.push([actual.searchParams.get(k) || "", v, false]);
});
// If any comparisons fail, the whole thing fails
return !comps.some(
(data) => !_evalSimpleUrlPart(data[0], data[1], data[2]),
);
} catch (e) {
return false;
}
}
function _evalURLTarget(
url: string,
type: UrlTargetType,
pattern: string,
): boolean {
try {
const parsed = new URL(url, "https://_");
if (type === "regex") {
const regex = getUrlRegExp(pattern);
if (!regex) return false;
return (
regex.test(parsed.href) ||
regex.test(parsed.href.substring(parsed.origin.length))
);
} else if (type === "simple") {
return _evalSimpleUrlTarget(parsed, pattern);
}
return false;
} catch (e) {
return false;
}
}
export function getBucketRanges(
numVariations: number,
coverage: number | undefined,
weights?: number[],
): VariationRange[] {
coverage = coverage === undefined ? 1 : coverage;
// Make sure coverage is within bounds
if (coverage < 0) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be greater than or equal to 0");
}
coverage = 0;
} else if (coverage > 1) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be less than or equal to 1");
}
coverage = 1;
}
// Default to equal weights if missing or invalid
const equal = getEqualWeights(numVariations);
weights = weights || equal;
if (weights.length !== numVariations) {
if (process.env.NODE_ENV !== "production") {
console.error(
"Experiment.weights array must be the same length as Experiment.variations",
);
}
weights = equal;
}
// If weights don't add up to 1 (or close to it), default to equal weights
const totalWeight = weights.reduce((w, sum) => sum + w, 0);
if (totalWeight < 0.99 || totalWeight > 1.01) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.weights must add up to 1");
}
weights = equal;
}
// Covert weights to ranges
let cumulative = 0;
return weights.map((w) => {
const start = cumulative;
cumulative += w;
return [start, start + (coverage as number) * w];
}) as VariationRange[];
}
export function getQueryStringOverride(
id: string,
url: string,
numVariations: number,
) {
if (!url) {
return null;
}
const search = url.split("?")[1];
if (!search) {
return null;
}
const match = search
.replace(/#.*/, "") // Get rid of anchor
.split("&") // Split into key/value pairs
.map((kv) => kv.split("=", 2))
.filter(([k]) => k === id) // Look for key that matches the experiment id
.map(([, v]) => parseInt(v)); // Parse the value into an integer
if (match.length > 0 && match[0] >= 0 && match[0] < numVariations)
return match[0];
return null;
}
export function isIncluded(include: () => boolean) {
try {
return include();
} catch (e) {
console.error(e);
return false;
}
}
const base64ToBuf = (b: string) =>
Uint8Array.from(atob(b), (c) => c.charCodeAt(0));
export async function decrypt(
encryptedString: string,
decryptionKey?: string,
subtle?: SubtleCrypto,
): Promise<string> {
decryptionKey = decryptionKey || "";
subtle =
subtle ||
(globalThis.crypto && globalThis.crypto.subtle) ||
polyfills.SubtleCrypto;
if (!subtle) {
throw new Error("No SubtleCrypto implementation found");
}
try {
const key = await subtle.importKey(
"raw",
base64ToBuf(decryptionKey),
{ name: "AES-CBC", length: 128 },
true,
["encrypt", "decrypt"],
);
const [iv, cipherText] = encryptedString.split(".");
const plainTextBuffer = await subtle.decrypt(
{ name: "AES-CBC", iv: base64ToBuf(iv) },
key,
base64ToBuf(cipherText),
);
return new TextDecoder().decode(plainTextBuffer);
} catch (e) {
throw new Error("Failed to decrypt");
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toString(input: any): string {
if (typeof input === "string") return input;
return JSON.stringify(input);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function paddedVersionString(input: any): string {
if (typeof input === "number") {
input = input + "";
}
if (!input || typeof input !== "string") {
input = "0";
}
// Remove build info and leading `v` if any
// Split version into parts (both core version numbers and pre-release tags)
// "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
const parts = (input as string).replace(/(^v|\+.*$)/g, "").split(/[-.]/);
// If it's SemVer without a pre-release, add `~` to the end
// ["1","0","0"] -> ["1","0","0","~"]
// "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
if (parts.length === 3) {
parts.push("~");
}
// Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
// Then, join back together into a single string
return parts
.map((v) => (v.match(/^[0-9]+$/) ? v.padStart(5, " ") : v))
.join("-");
}
export function loadSDKVersion(): string {
let version: string;
try {
// @ts-expect-error right-hand value to be replaced by build with string literal
version = __SDK_VERSION__;
} catch (e) {
version = "";
}
return version;
}
export function mergeQueryStrings(oldUrl: string, newUrl: string): string {
let currUrl: URL;
let redirectUrl: URL;
try {
currUrl = new URL(oldUrl);
redirectUrl = new URL(newUrl);
} catch (e) {
console.error(`Unable to merge query strings: ${e}`);
return newUrl;
}
currUrl.searchParams.forEach((value, key) => {
// skip if search param already exists in redirectUrl
if (redirectUrl.searchParams.has(key)) {
return;
}
redirectUrl.searchParams.set(key, value);
});
return redirectUrl.toString();
}
function isObj(x: unknown): x is Record<string, unknown> {
return typeof x === "object" && x !== null;
}
export function getAutoExperimentChangeType(
exp: AutoExperiment,
): AutoExperimentChangeType {
if (
exp.urlPatterns &&
exp.variations.some(
(variation) => isObj(variation) && "urlRedirect" in variation,
)
) {
return "redirect";
} else if (
exp.variations.some(
(variation) =>
isObj(variation) &&
(variation.domMutations || "js" in variation || "css" in variation),
)
) {
return "visual";
}
return "unknown";
}
// Guarantee the promise always resolves within {timeout} ms
// Resolved value will be `null` when there's an error or it takes too long
// Note: The promise will continue running in the background, even if the timeout is hit
export async function promiseTimeout<T>(
promise: Promise<T>,
timeout?: number,
): Promise<T | null> {
return new Promise((resolve) => {
let resolved = false;
let timer: NodeJS.Timeout | undefined;
const finish = (data?: T) => {
if (resolved) return;
resolved = true;
timer && clearTimeout(timer);
resolve(data || null);
};
if (timeout) {
timer = setTimeout(() => finish(), timeout);
}
promise.then((data) => finish(data)).catch(() => finish());
});
}