@drip-apex/apex-cli 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/README.md +11 -1
- package/dist/cli.js +1402 -384
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +8 -0
package/dist/cli.js
CHANGED
|
@@ -1159,8 +1159,8 @@ var require_command = __commonJS({
|
|
|
1159
1159
|
"node_modules/commander/lib/command.js"(exports2) {
|
|
1160
1160
|
var EventEmitter = require("node:events").EventEmitter;
|
|
1161
1161
|
var childProcess2 = require("node:child_process");
|
|
1162
|
-
var
|
|
1163
|
-
var
|
|
1162
|
+
var path24 = require("node:path");
|
|
1163
|
+
var fs28 = require("node:fs");
|
|
1164
1164
|
var process16 = require("node:process");
|
|
1165
1165
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1166
1166
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2141,7 +2141,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2141
2141
|
* @param {string} subcommandName
|
|
2142
2142
|
*/
|
|
2143
2143
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2144
|
-
if (
|
|
2144
|
+
if (fs28.existsSync(executableFile)) return;
|
|
2145
2145
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2146
2146
|
const executableMissing = `'${executableFile}' does not exist
|
|
2147
2147
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2159,11 +2159,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2159
2159
|
let launchWithNode = false;
|
|
2160
2160
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2161
2161
|
function findFile(baseDir, baseName) {
|
|
2162
|
-
const localBin =
|
|
2163
|
-
if (
|
|
2164
|
-
if (sourceExt.includes(
|
|
2162
|
+
const localBin = path24.resolve(baseDir, baseName);
|
|
2163
|
+
if (fs28.existsSync(localBin)) return localBin;
|
|
2164
|
+
if (sourceExt.includes(path24.extname(baseName))) return void 0;
|
|
2165
2165
|
const foundExt = sourceExt.find(
|
|
2166
|
-
(ext) =>
|
|
2166
|
+
(ext) => fs28.existsSync(`${localBin}${ext}`)
|
|
2167
2167
|
);
|
|
2168
2168
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2169
2169
|
return void 0;
|
|
@@ -2175,21 +2175,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2175
2175
|
if (this._scriptPath) {
|
|
2176
2176
|
let resolvedScriptPath;
|
|
2177
2177
|
try {
|
|
2178
|
-
resolvedScriptPath =
|
|
2178
|
+
resolvedScriptPath = fs28.realpathSync(this._scriptPath);
|
|
2179
2179
|
} catch {
|
|
2180
2180
|
resolvedScriptPath = this._scriptPath;
|
|
2181
2181
|
}
|
|
2182
|
-
executableDir =
|
|
2183
|
-
|
|
2182
|
+
executableDir = path24.resolve(
|
|
2183
|
+
path24.dirname(resolvedScriptPath),
|
|
2184
2184
|
executableDir
|
|
2185
2185
|
);
|
|
2186
2186
|
}
|
|
2187
2187
|
if (executableDir) {
|
|
2188
2188
|
let localFile = findFile(executableDir, executableFile);
|
|
2189
2189
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2190
|
-
const legacyName =
|
|
2190
|
+
const legacyName = path24.basename(
|
|
2191
2191
|
this._scriptPath,
|
|
2192
|
-
|
|
2192
|
+
path24.extname(this._scriptPath)
|
|
2193
2193
|
);
|
|
2194
2194
|
if (legacyName !== this._name) {
|
|
2195
2195
|
localFile = findFile(
|
|
@@ -2200,7 +2200,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2200
2200
|
}
|
|
2201
2201
|
executableFile = localFile || executableFile;
|
|
2202
2202
|
}
|
|
2203
|
-
launchWithNode = sourceExt.includes(
|
|
2203
|
+
launchWithNode = sourceExt.includes(path24.extname(executableFile));
|
|
2204
2204
|
let proc;
|
|
2205
2205
|
if (process16.platform !== "win32") {
|
|
2206
2206
|
if (launchWithNode) {
|
|
@@ -3047,7 +3047,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3047
3047
|
* @return {Command}
|
|
3048
3048
|
*/
|
|
3049
3049
|
nameFromFilename(filename) {
|
|
3050
|
-
this._name =
|
|
3050
|
+
this._name = path24.basename(filename, path24.extname(filename));
|
|
3051
3051
|
return this;
|
|
3052
3052
|
}
|
|
3053
3053
|
/**
|
|
@@ -3061,9 +3061,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3061
3061
|
* @param {string} [path]
|
|
3062
3062
|
* @return {(string|null|Command)}
|
|
3063
3063
|
*/
|
|
3064
|
-
executableDir(
|
|
3065
|
-
if (
|
|
3066
|
-
this._executableDir =
|
|
3064
|
+
executableDir(path25) {
|
|
3065
|
+
if (path25 === void 0) return this._executableDir;
|
|
3066
|
+
this._executableDir = path25;
|
|
3067
3067
|
return this;
|
|
3068
3068
|
}
|
|
3069
3069
|
/**
|
|
@@ -6158,16 +6158,16 @@ __export(ora_exports, {
|
|
|
6158
6158
|
function ora(options) {
|
|
6159
6159
|
return new Ora(options);
|
|
6160
6160
|
}
|
|
6161
|
-
async function oraPromise(
|
|
6162
|
-
const actionIsFunction = typeof
|
|
6163
|
-
const actionIsPromise = typeof
|
|
6161
|
+
async function oraPromise(action71, options) {
|
|
6162
|
+
const actionIsFunction = typeof action71 === "function";
|
|
6163
|
+
const actionIsPromise = typeof action71.then === "function";
|
|
6164
6164
|
if (!actionIsFunction && !actionIsPromise) {
|
|
6165
6165
|
throw new TypeError("Parameter `action` must be a Function or a Promise");
|
|
6166
6166
|
}
|
|
6167
6167
|
const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
|
|
6168
6168
|
const spinner = ora(options).start();
|
|
6169
6169
|
try {
|
|
6170
|
-
const promise = actionIsFunction ?
|
|
6170
|
+
const promise = actionIsFunction ? action71(spinner) : action71;
|
|
6171
6171
|
const result = await promise;
|
|
6172
6172
|
spinner.succeed(
|
|
6173
6173
|
successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
|
|
@@ -7099,7 +7099,7 @@ init_source();
|
|
|
7099
7099
|
|
|
7100
7100
|
// src/commands/login.ts
|
|
7101
7101
|
init_source();
|
|
7102
|
-
var
|
|
7102
|
+
var import_node_crypto2 = __toESM(require("node:crypto"));
|
|
7103
7103
|
var import_node_http = __toESM(require("node:http"));
|
|
7104
7104
|
var import_node_os5 = __toESM(require("node:os"));
|
|
7105
7105
|
var import_node_readline2 = __toESM(require("node:readline"));
|
|
@@ -7112,7 +7112,7 @@ var import_node_os3 = __toESM(require("node:os"));
|
|
|
7112
7112
|
// package.json
|
|
7113
7113
|
var package_default = {
|
|
7114
7114
|
name: "@drip-apex/apex-cli",
|
|
7115
|
-
version: "0.6.
|
|
7115
|
+
version: "0.6.2",
|
|
7116
7116
|
license: "MIT",
|
|
7117
7117
|
description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
|
|
7118
7118
|
private: false,
|
|
@@ -7586,6 +7586,9 @@ function findConfigPath() {
|
|
|
7586
7586
|
return null;
|
|
7587
7587
|
}
|
|
7588
7588
|
|
|
7589
|
+
// src/api.ts
|
|
7590
|
+
var import_node_crypto = require("node:crypto");
|
|
7591
|
+
|
|
7589
7592
|
// ../api-client/src/http.ts
|
|
7590
7593
|
var APEX_API_VERSION_HEADER = "x-apex-api-version";
|
|
7591
7594
|
var SUPPORTED_APEX_API_VERSION = "1";
|
|
@@ -7619,8 +7622,8 @@ function createHttpClient(creds, options) {
|
|
|
7619
7622
|
warnedAboutVersionSkew = true;
|
|
7620
7623
|
warn3(version > 1 ? "CLI is behind the server \u2014 update.\n" : "Server API is older than this CLI supports.\n");
|
|
7621
7624
|
}
|
|
7622
|
-
async function request2(method,
|
|
7623
|
-
const mock = options.transport?.(method,
|
|
7625
|
+
async function request2(method, path24, body5, requestTimeoutMs, form = false) {
|
|
7626
|
+
const mock = options.transport?.(method, path24, body5);
|
|
7624
7627
|
if (mock !== void 0) {
|
|
7625
7628
|
const version = options.testTransportVersion?.();
|
|
7626
7629
|
if (version) inspectVersion(new Headers({ [APEX_API_VERSION_HEADER]: version }));
|
|
@@ -7638,7 +7641,7 @@ function createHttpClient(creds, options) {
|
|
|
7638
7641
|
}
|
|
7639
7642
|
let response;
|
|
7640
7643
|
try {
|
|
7641
|
-
response = await fetch(`${creds.baseUrl}${
|
|
7644
|
+
response = await fetch(`${creds.baseUrl}${path24}`, { method, headers, body: form ? body5 : body5 ? JSON.stringify(body5) : void 0, signal: AbortSignal.timeout(timeoutMs) });
|
|
7642
7645
|
} catch (error) {
|
|
7643
7646
|
return options.onNetworkError({ baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) });
|
|
7644
7647
|
}
|
|
@@ -7647,7 +7650,7 @@ function createHttpClient(creds, options) {
|
|
|
7647
7650
|
if (!response.ok) return { ok: false, status: response.status, data: form ? void 0 : json, error: extractErrorMessage(json, response.status), errorDetails: extractErrorDetails(json), requestId: response.headers.get("x-request-id"), rateLimit: { limit: response.headers.get("x-ratelimit-limit"), remaining: response.headers.get("x-ratelimit-remaining"), reset: response.headers.get("x-ratelimit-reset") } };
|
|
7648
7651
|
return { ok: true, status: response.status, data: json };
|
|
7649
7652
|
}
|
|
7650
|
-
return { request: (method,
|
|
7653
|
+
return { request: (method, path24, body5, timeoutMs) => request2(method, path24, body5, timeoutMs), requestForm: (method, path24, body5) => request2(method, path24, body5, void 0, true) };
|
|
7651
7654
|
}
|
|
7652
7655
|
|
|
7653
7656
|
// src/api.ts
|
|
@@ -7674,11 +7677,11 @@ function getUserKeyIdentity(creds) {
|
|
|
7674
7677
|
function getAccountContext(creds) {
|
|
7675
7678
|
return userRequest(creds, "GET", "/api/v1/me/context");
|
|
7676
7679
|
}
|
|
7677
|
-
function userRequest(creds, method,
|
|
7680
|
+
function userRequest(creds, method, path24, body5) {
|
|
7678
7681
|
return request(
|
|
7679
7682
|
{ ...creds, apiKey: creds.userApiKey ?? "", baseUrl: creds.userBaseUrl ?? creds.baseUrl },
|
|
7680
7683
|
method,
|
|
7681
|
-
|
|
7684
|
+
path24,
|
|
7682
7685
|
body5
|
|
7683
7686
|
);
|
|
7684
7687
|
}
|
|
@@ -7724,11 +7727,11 @@ function sharedClient(creds) {
|
|
|
7724
7727
|
}
|
|
7725
7728
|
return client;
|
|
7726
7729
|
}
|
|
7727
|
-
async function request(creds, method,
|
|
7728
|
-
return sharedClient(creds).request(method,
|
|
7730
|
+
async function request(creds, method, path24, body5, requestTimeoutMs) {
|
|
7731
|
+
return sharedClient(creds).request(method, path24, body5, requestTimeoutMs);
|
|
7729
7732
|
}
|
|
7730
|
-
async function requestForm(creds, method,
|
|
7731
|
-
return sharedClient(creds).requestForm(method,
|
|
7733
|
+
async function requestForm(creds, method, path24, body5) {
|
|
7734
|
+
return sharedClient(creds).requestForm(method, path24, body5);
|
|
7732
7735
|
}
|
|
7733
7736
|
function listExperiments(creds, opts) {
|
|
7734
7737
|
const params = new URLSearchParams();
|
|
@@ -7802,6 +7805,12 @@ function getExperimentQaRun(creds, id) {
|
|
|
7802
7805
|
function updateExperimentQaRun(creds, id, body5) {
|
|
7803
7806
|
return request(creds, "PATCH", `/api/v1/experiments/${id}/qa/run`, body5);
|
|
7804
7807
|
}
|
|
7808
|
+
function attachExperimentQaEvidence(creds, id, body5) {
|
|
7809
|
+
return request(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, body5);
|
|
7810
|
+
}
|
|
7811
|
+
function uploadExperimentQaEvidence(creds, id, form) {
|
|
7812
|
+
return requestForm(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, form);
|
|
7813
|
+
}
|
|
7805
7814
|
function approveExperimentQa(creds, id) {
|
|
7806
7815
|
return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
|
|
7807
7816
|
}
|
|
@@ -7959,12 +7968,17 @@ function testTargeting(creds, url, opts) {
|
|
|
7959
7968
|
return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
|
|
7960
7969
|
}
|
|
7961
7970
|
function createSos(creds, body5) {
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7971
|
+
const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto.createHash)("sha256").update(
|
|
7972
|
+
[
|
|
7973
|
+
body5.type,
|
|
7974
|
+
body5.experiment_id ?? "",
|
|
7975
|
+
body5.description,
|
|
7976
|
+
body5.sub_class ?? "",
|
|
7977
|
+
body5.loom_url ?? "",
|
|
7978
|
+
String(Math.floor(Date.now() / 36e5))
|
|
7979
|
+
].join("\0")
|
|
7980
|
+
).digest("hex");
|
|
7981
|
+
return request(creds, "POST", "/api/v1/sos", { ...body5, idempotency_key });
|
|
7968
7982
|
}
|
|
7969
7983
|
function listMySos(creds) {
|
|
7970
7984
|
return request(creds, "GET", "/api/v1/sos/mine");
|
|
@@ -8086,16 +8100,30 @@ function getAdminQuarantine(creds) {
|
|
|
8086
8100
|
"/api/v1/admin/tracking/quarantine"
|
|
8087
8101
|
);
|
|
8088
8102
|
}
|
|
8103
|
+
function archiveAdminShop(creds, shopId) {
|
|
8104
|
+
return request(
|
|
8105
|
+
userCredentials(creds),
|
|
8106
|
+
"POST",
|
|
8107
|
+
`/api/v1/admin/shops/${encodeURIComponent(shopId)}/archive`
|
|
8108
|
+
);
|
|
8109
|
+
}
|
|
8110
|
+
function restoreAdminShop(creds, shopId) {
|
|
8111
|
+
return request(
|
|
8112
|
+
userCredentials(creds),
|
|
8113
|
+
"POST",
|
|
8114
|
+
`/api/v1/admin/shops/${encodeURIComponent(shopId)}/restore`
|
|
8115
|
+
);
|
|
8116
|
+
}
|
|
8089
8117
|
function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor) {
|
|
8090
8118
|
const params = new URLSearchParams();
|
|
8091
8119
|
if (limit) params.set("limit", String(limit));
|
|
8092
8120
|
if (cursor) params.set("cursor", cursor);
|
|
8093
8121
|
const query = params.toString();
|
|
8094
|
-
const
|
|
8095
|
-
return request(creds, method,
|
|
8122
|
+
const path24 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
|
|
8123
|
+
return request(creds, method, path24, body5);
|
|
8096
8124
|
}
|
|
8097
|
-
function managedPath(kind5, id,
|
|
8098
|
-
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${
|
|
8125
|
+
function managedPath(kind5, id, action71) {
|
|
8126
|
+
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action71 ? `/${action71}` : ""}`;
|
|
8099
8127
|
}
|
|
8100
8128
|
function listManagedDeliveries(creds, kind5, limit) {
|
|
8101
8129
|
const q = limit ? `?limit=${limit}` : "";
|
|
@@ -8113,8 +8141,8 @@ function createManagedDelivery(creds, kind5, body5) {
|
|
|
8113
8141
|
function updateManagedDelivery(creds, kind5, id, body5) {
|
|
8114
8142
|
return request(creds, "PATCH", managedPath(kind5, id), body5);
|
|
8115
8143
|
}
|
|
8116
|
-
function transitionManagedDelivery(creds, kind5, id,
|
|
8117
|
-
return request(creds, "POST", managedPath(kind5, id,
|
|
8144
|
+
function transitionManagedDelivery(creds, kind5, id, action71) {
|
|
8145
|
+
return request(creds, "POST", managedPath(kind5, id, action71));
|
|
8118
8146
|
}
|
|
8119
8147
|
function listConfigResources(creds, family, opts) {
|
|
8120
8148
|
const params = new URLSearchParams();
|
|
@@ -8471,7 +8499,7 @@ function parseFormBody(body5) {
|
|
|
8471
8499
|
return new URLSearchParams(body5);
|
|
8472
8500
|
}
|
|
8473
8501
|
async function waitForBrowserLogin(baseUrl, userMode = false) {
|
|
8474
|
-
const state =
|
|
8502
|
+
const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
|
|
8475
8503
|
const clientName = import_node_os5.default.hostname() || "Local machine";
|
|
8476
8504
|
return new Promise((resolve, reject) => {
|
|
8477
8505
|
let settled = false;
|
|
@@ -8803,21 +8831,23 @@ var useCommand = new Command("use").description("Set the current Apex organizati
|
|
|
8803
8831
|
|
|
8804
8832
|
// src/commands/experiments.ts
|
|
8805
8833
|
init_source();
|
|
8806
|
-
var
|
|
8807
|
-
var
|
|
8808
|
-
var
|
|
8834
|
+
var import_node_crypto4 = __toESM(require("node:crypto"));
|
|
8835
|
+
var import_node_fs7 = __toESM(require("node:fs"));
|
|
8836
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
8809
8837
|
|
|
8810
8838
|
// ../api-client/src/qa-preview-url.ts
|
|
8839
|
+
var FORCE_ALIAS_PARAM_NAMES = ["drip_experiment_id", "drip_experiment", "drip_variation_id", "drip_variation"];
|
|
8811
8840
|
function applyQaPreviewParams(url, params) {
|
|
8812
|
-
url.searchParams.
|
|
8813
|
-
url.searchParams.
|
|
8814
|
-
url.searchParams.set("drip_variation_id", params.variationId);
|
|
8841
|
+
url.searchParams.delete("drip_force");
|
|
8842
|
+
for (const name of FORCE_ALIAS_PARAM_NAMES) url.searchParams.delete(name);
|
|
8815
8843
|
url.searchParams.set("drip_qa", "1");
|
|
8816
8844
|
if (params.debug !== false) url.searchParams.set("drip_debug", "1");
|
|
8817
8845
|
if (params.devtools) url.searchParams.set("drip_devtools", "1");
|
|
8818
8846
|
if (params.screenshot) url.searchParams.set("drip_screenshot", "1");
|
|
8819
8847
|
if (params.qaSessionId) url.searchParams.set("drip_qa_session", params.qaSessionId);
|
|
8820
8848
|
if (params.harnessToken) url.searchParams.set("drip_harness", params.harnessToken);
|
|
8849
|
+
const force = `${params.experimentPublicId ?? params.experimentId}:${params.variationPublicId ?? params.variationId}`;
|
|
8850
|
+
url.search += `${url.search ? "&" : "?"}drip_force=${force}`;
|
|
8821
8851
|
return url;
|
|
8822
8852
|
}
|
|
8823
8853
|
function buildQaPreviewUrl(experiment, options = {}) {
|
|
@@ -8825,7 +8855,7 @@ function buildQaPreviewUrl(experiment, options = {}) {
|
|
|
8825
8855
|
const target = options.storeUrl ?? experiment.store_url;
|
|
8826
8856
|
if (!target || !variation) return null;
|
|
8827
8857
|
try {
|
|
8828
|
-
return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, ...options });
|
|
8858
|
+
return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, experimentPublicId: experiment.public_id, variationPublicId: variation.public_id, ...options });
|
|
8829
8859
|
} catch {
|
|
8830
8860
|
return null;
|
|
8831
8861
|
}
|
|
@@ -8842,6 +8872,192 @@ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
|
|
|
8842
8872
|
return url;
|
|
8843
8873
|
}
|
|
8844
8874
|
|
|
8875
|
+
// src/lib/asset-upload.ts
|
|
8876
|
+
var import_node_crypto3 = __toESM(require("node:crypto"));
|
|
8877
|
+
var import_node_fs6 = __toESM(require("node:fs"));
|
|
8878
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
8879
|
+
var INLINE_DATA_URI_MAX_CHARS = 4096;
|
|
8880
|
+
var DATA_IMAGE_URI_PATTERN = /data:image\/[a-z0-9.+-]+(?:(?:;[^;,\s"'`]*)*;base64,[A-Za-z0-9+/=\s]+|(?:;[^,\s"'`]*)?,[^\s"'`]+)/gi;
|
|
8881
|
+
var QUOTED_LOCAL_IMAGE_PATTERN = /(["'])([^"'\n]+?\.(?:png|jpe?g|gif|webp|svg|avif))\1/gi;
|
|
8882
|
+
var LOCAL_IMAGE_PATTERN = /(?:\.{1,2}\/)?[a-z0-9@_%+.,~/-]+\.(?:png|jpe?g|gif|webp|svg|avif)/gi;
|
|
8883
|
+
var MIME_BY_EXTENSION = {
|
|
8884
|
+
".png": "image/png",
|
|
8885
|
+
".jpg": "image/jpeg",
|
|
8886
|
+
".jpeg": "image/jpeg",
|
|
8887
|
+
".gif": "image/gif",
|
|
8888
|
+
".webp": "image/webp",
|
|
8889
|
+
".svg": "image/svg+xml",
|
|
8890
|
+
".avif": "image/avif"
|
|
8891
|
+
};
|
|
8892
|
+
var EXTENSION_BY_MIME = {
|
|
8893
|
+
"image/png": ".png",
|
|
8894
|
+
"image/jpeg": ".jpg",
|
|
8895
|
+
"image/gif": ".gif",
|
|
8896
|
+
"image/webp": ".webp",
|
|
8897
|
+
"image/svg+xml": ".svg",
|
|
8898
|
+
"image/avif": ".avif"
|
|
8899
|
+
};
|
|
8900
|
+
function contentHash(bytes) {
|
|
8901
|
+
return import_node_crypto3.default.createHash("sha256").update(bytes).digest("hex");
|
|
8902
|
+
}
|
|
8903
|
+
function deliveryUrl(baseUrl, asset) {
|
|
8904
|
+
const returnedUrl = asset.deliveryUrl ?? asset.delivery_url ?? asset.url;
|
|
8905
|
+
if (returnedUrl) return returnedUrl;
|
|
8906
|
+
const origin = new URL(baseUrl).origin;
|
|
8907
|
+
return `${origin}/api/assets/${encodeURIComponent(asset.id)}/file`;
|
|
8908
|
+
}
|
|
8909
|
+
function extensionForMimeType(mimeType) {
|
|
8910
|
+
const known = EXTENSION_BY_MIME[mimeType];
|
|
8911
|
+
if (known) return known;
|
|
8912
|
+
const subtype = mimeType.split("/", 2)[1]?.toLowerCase();
|
|
8913
|
+
const safeSubtype = subtype?.replace(/[^a-z0-9.+-]/g, "");
|
|
8914
|
+
return safeSubtype ? `.${safeSubtype}` : ".bin";
|
|
8915
|
+
}
|
|
8916
|
+
function dataUriFromMatch(value, match2) {
|
|
8917
|
+
let uri = match2[0];
|
|
8918
|
+
const start = match2.index ?? 0;
|
|
8919
|
+
if (/url\(\s*$/i.test(value.slice(0, start))) {
|
|
8920
|
+
const closingParenthesis = uri.indexOf(")");
|
|
8921
|
+
if (closingParenthesis >= 0) uri = uri.slice(0, closingParenthesis);
|
|
8922
|
+
}
|
|
8923
|
+
return uri;
|
|
8924
|
+
}
|
|
8925
|
+
function surroundingNonWhitespaceNonQuoteRun(value, start, end) {
|
|
8926
|
+
const before = value.slice(0, start).match(/[^\s"']*$/)?.[0] ?? "";
|
|
8927
|
+
const after = value.slice(end).match(/^[^\s"']*/)?.[0] ?? "";
|
|
8928
|
+
return `${before}${value.slice(start, end)}${after}`;
|
|
8929
|
+
}
|
|
8930
|
+
function isInsideRange(start, end, ranges) {
|
|
8931
|
+
return ranges.some((range) => start >= range.start && end <= range.end);
|
|
8932
|
+
}
|
|
8933
|
+
function applyReplacements(value, replacements) {
|
|
8934
|
+
let rewritten = value;
|
|
8935
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
8936
|
+
rewritten = rewritten.slice(0, replacement.start) + replacement.value + rewritten.slice(replacement.end);
|
|
8937
|
+
}
|
|
8938
|
+
return rewritten;
|
|
8939
|
+
}
|
|
8940
|
+
function shouldRewriteExperimentFilesystemAssetReferences(filePath) {
|
|
8941
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
8942
|
+
if (/^variations\/[^/]+\/.+\.(?:js|ts|css|scss|html|json)$/i.test(normalized)) {
|
|
8943
|
+
return true;
|
|
8944
|
+
}
|
|
8945
|
+
return /^(?:experiment\/index\.(?:js|css)|experiment\/reset\.js)$/i.test(normalized);
|
|
8946
|
+
}
|
|
8947
|
+
function createAssetReferenceRewriter(options) {
|
|
8948
|
+
const uploadsByHash = /* @__PURE__ */ new Map();
|
|
8949
|
+
const uploadOnce = async (file, source) => {
|
|
8950
|
+
const hash = contentHash(file.bytes);
|
|
8951
|
+
let pending = uploadsByHash.get(hash);
|
|
8952
|
+
if (!pending) {
|
|
8953
|
+
pending = options.upload(file).then((asset) => {
|
|
8954
|
+
if (!asset.id) throw new Error(`Asset upload for ${file.name} returned no ID.`);
|
|
8955
|
+
const url = deliveryUrl(options.baseUrl, asset);
|
|
8956
|
+
options.onUpload?.({ name: file.name, url, source });
|
|
8957
|
+
return url;
|
|
8958
|
+
});
|
|
8959
|
+
uploadsByHash.set(hash, pending);
|
|
8960
|
+
pending.catch(() => uploadsByHash.delete(hash));
|
|
8961
|
+
}
|
|
8962
|
+
return pending;
|
|
8963
|
+
};
|
|
8964
|
+
const rewrite = async (value, baseDir = options.baseDir) => {
|
|
8965
|
+
if (options.enabled === false || !value) return value;
|
|
8966
|
+
let rewritten = value;
|
|
8967
|
+
const inlineMatches = [...rewritten.matchAll(DATA_IMAGE_URI_PATTERN)].map((match2) => ({ match: match2, uri: dataUriFromMatch(rewritten, match2) })).filter(({ uri }) => uri.length > INLINE_DATA_URI_MAX_CHARS);
|
|
8968
|
+
for (const { uri } of inlineMatches) {
|
|
8969
|
+
const commaIndex = uri.indexOf(",");
|
|
8970
|
+
const metadata = uri.slice("data:".length, commaIndex);
|
|
8971
|
+
const mimeType = metadata.split(";", 1)[0].toLowerCase();
|
|
8972
|
+
const payload = uri.slice(commaIndex + 1);
|
|
8973
|
+
let bytes;
|
|
8974
|
+
try {
|
|
8975
|
+
bytes = /;base64$/i.test(metadata) ? Buffer.from(payload.replace(/\s/g, ""), "base64") : Buffer.from(decodeURIComponent(payload), "utf8");
|
|
8976
|
+
} catch {
|
|
8977
|
+
continue;
|
|
8978
|
+
}
|
|
8979
|
+
const hash = contentHash(bytes);
|
|
8980
|
+
const name = `inline-${hash.slice(0, 12)}${extensionForMimeType(mimeType)}`;
|
|
8981
|
+
const url = await uploadOnce({ name, bytes, type: mimeType }, "inline");
|
|
8982
|
+
rewritten = rewritten.split(uri).join(url);
|
|
8983
|
+
}
|
|
8984
|
+
const localAssetUrl = async (reference) => {
|
|
8985
|
+
if (reference.startsWith("/") || reference.startsWith("data:") || reference.includes("://")) {
|
|
8986
|
+
return null;
|
|
8987
|
+
}
|
|
8988
|
+
const absolutePath = import_node_path4.default.resolve(baseDir, reference);
|
|
8989
|
+
let realBaseDir;
|
|
8990
|
+
let realPath;
|
|
8991
|
+
try {
|
|
8992
|
+
realBaseDir = import_node_fs6.default.realpathSync(baseDir);
|
|
8993
|
+
realPath = import_node_fs6.default.realpathSync(absolutePath);
|
|
8994
|
+
} catch {
|
|
8995
|
+
return null;
|
|
8996
|
+
}
|
|
8997
|
+
const relativeRealPath = import_node_path4.default.relative(realBaseDir, realPath);
|
|
8998
|
+
if (relativeRealPath.startsWith("..") || import_node_path4.default.isAbsolute(relativeRealPath)) return null;
|
|
8999
|
+
if (!import_node_fs6.default.statSync(realPath).isFile()) return null;
|
|
9000
|
+
const bytes = import_node_fs6.default.readFileSync(realPath);
|
|
9001
|
+
const extension = import_node_path4.default.extname(realPath).toLowerCase();
|
|
9002
|
+
return uploadOnce(
|
|
9003
|
+
{
|
|
9004
|
+
name: import_node_path4.default.basename(absolutePath),
|
|
9005
|
+
bytes,
|
|
9006
|
+
type: MIME_BY_EXTENSION[extension]
|
|
9007
|
+
},
|
|
9008
|
+
"local"
|
|
9009
|
+
);
|
|
9010
|
+
};
|
|
9011
|
+
const quotedReplacements = [];
|
|
9012
|
+
for (const match2 of rewritten.matchAll(QUOTED_LOCAL_IMAGE_PATTERN)) {
|
|
9013
|
+
const reference = match2[2];
|
|
9014
|
+
const matchStart = match2.index ?? 0;
|
|
9015
|
+
const url = await localAssetUrl(reference);
|
|
9016
|
+
if (!url) continue;
|
|
9017
|
+
const start = matchStart + match2[1].length;
|
|
9018
|
+
quotedReplacements.push({ start, end: start + reference.length, value: url });
|
|
9019
|
+
}
|
|
9020
|
+
rewritten = applyReplacements(rewritten, quotedReplacements);
|
|
9021
|
+
const quotedRanges = [...rewritten.matchAll(QUOTED_LOCAL_IMAGE_PATTERN)].map((match2) => ({
|
|
9022
|
+
start: match2.index ?? 0,
|
|
9023
|
+
end: (match2.index ?? 0) + match2[0].length
|
|
9024
|
+
}));
|
|
9025
|
+
const localMatches = [...rewritten.matchAll(LOCAL_IMAGE_PATTERN)];
|
|
9026
|
+
const bareReplacements = [];
|
|
9027
|
+
for (const match2 of localMatches) {
|
|
9028
|
+
const reference = match2[0];
|
|
9029
|
+
const start = match2.index ?? 0;
|
|
9030
|
+
const end = start + reference.length;
|
|
9031
|
+
const surroundingRun = surroundingNonWhitespaceNonQuoteRun(rewritten, start, end);
|
|
9032
|
+
if (reference.startsWith("/") || reference.startsWith("data:") || isInsideRange(start, end, quotedRanges) || surroundingRun.includes("://") || surroundingRun.startsWith("//")) {
|
|
9033
|
+
continue;
|
|
9034
|
+
}
|
|
9035
|
+
const url = await localAssetUrl(reference);
|
|
9036
|
+
if (!url) continue;
|
|
9037
|
+
bareReplacements.push({ start, end, value: url });
|
|
9038
|
+
}
|
|
9039
|
+
return applyReplacements(rewritten, bareReplacements);
|
|
9040
|
+
};
|
|
9041
|
+
const rewriteUnknown = async (value, baseDir = options.baseDir) => {
|
|
9042
|
+
if (typeof value === "string") return rewrite(value, baseDir);
|
|
9043
|
+
if (Array.isArray(value)) {
|
|
9044
|
+
return Promise.all(value.map((entry) => rewriteUnknown(entry, baseDir)));
|
|
9045
|
+
}
|
|
9046
|
+
if (value !== null && typeof value === "object") {
|
|
9047
|
+
return Object.fromEntries(
|
|
9048
|
+
await Promise.all(
|
|
9049
|
+
Object.entries(value).map(async ([key, entry]) => [
|
|
9050
|
+
key,
|
|
9051
|
+
await rewriteUnknown(entry, baseDir)
|
|
9052
|
+
])
|
|
9053
|
+
)
|
|
9054
|
+
);
|
|
9055
|
+
}
|
|
9056
|
+
return value;
|
|
9057
|
+
};
|
|
9058
|
+
return { rewrite, rewriteUnknown };
|
|
9059
|
+
}
|
|
9060
|
+
|
|
8845
9061
|
// src/commands/experiments.ts
|
|
8846
9062
|
function formatDate(iso) {
|
|
8847
9063
|
return new Date(iso).toLocaleDateString("en-US", {
|
|
@@ -8878,7 +9094,19 @@ function confidenceBasisLabel(value) {
|
|
|
8878
9094
|
function statisticsStateLabel(value) {
|
|
8879
9095
|
return value === "untrustworthy" ? "verdict withheld" : String(value ?? "unknown");
|
|
8880
9096
|
}
|
|
8881
|
-
function
|
|
9097
|
+
function formatRevenue(value, currency) {
|
|
9098
|
+
if (value === null || value === void 0) return "\u2014";
|
|
9099
|
+
if (!currency || typeof value !== "number" || !Number.isFinite(value)) return String(value);
|
|
9100
|
+
try {
|
|
9101
|
+
return new Intl.NumberFormat("en-US", {
|
|
9102
|
+
style: "currency",
|
|
9103
|
+
currency: currency.trim().toUpperCase()
|
|
9104
|
+
}).format(value);
|
|
9105
|
+
} catch {
|
|
9106
|
+
return String(value);
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
function printExperimentResults(results2, currency) {
|
|
8882
9110
|
const experiment = results2.experiment ?? {};
|
|
8883
9111
|
const totals = results2.totals ?? {};
|
|
8884
9112
|
const decision = results2.decision ?? {};
|
|
@@ -8892,7 +9120,7 @@ function printExperimentResults(results2) {
|
|
|
8892
9120
|
console.log(` Status: ${statusBadge(String(experiment.status ?? "unknown"))}`);
|
|
8893
9121
|
console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
|
|
8894
9122
|
console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
|
|
8895
|
-
console.log(` Revenue: ${
|
|
9123
|
+
console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
|
|
8896
9124
|
console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
|
|
8897
9125
|
console.log(` Statistics: ${statisticsStateLabel(statisticsState)}`);
|
|
8898
9126
|
if (decision.treatmentName) {
|
|
@@ -8915,7 +9143,7 @@ function printExperimentResults(results2) {
|
|
|
8915
9143
|
String(arm.visitors ?? 0),
|
|
8916
9144
|
String(arm.primaryConversions ?? 0),
|
|
8917
9145
|
typeof arm.primaryRate === "number" ? `${(arm.primaryRate * 100).toFixed(2)}%` : "-",
|
|
8918
|
-
|
|
9146
|
+
formatRevenue(arm.revenue, currency)
|
|
8919
9147
|
]),
|
|
8920
9148
|
[32, 8, 6, 8, 10]
|
|
8921
9149
|
);
|
|
@@ -8941,45 +9169,46 @@ var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
|
|
|
8941
9169
|
"anti-flicker.json"
|
|
8942
9170
|
]
|
|
8943
9171
|
};
|
|
9172
|
+
var SERVER_OWNED_EXPERIMENT_FILES = /* @__PURE__ */ new Set(["qa.json", "launch.json"]);
|
|
8944
9173
|
var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8945
9174
|
var pathAlternation = (paths) => paths.map(escapeRegex).join("|");
|
|
8946
9175
|
var FILESYSTEM_FILE_RE = new RegExp(
|
|
8947
9176
|
`^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
|
|
8948
9177
|
);
|
|
8949
9178
|
function sha256Text(value) {
|
|
8950
|
-
return
|
|
9179
|
+
return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
|
|
8951
9180
|
}
|
|
8952
9181
|
function folderPath(folder) {
|
|
8953
|
-
return
|
|
9182
|
+
return import_node_path5.default.resolve(process.cwd(), folder);
|
|
8954
9183
|
}
|
|
8955
9184
|
function filesystemApexDir(folder) {
|
|
8956
|
-
return
|
|
9185
|
+
return import_node_path5.default.join(folder, ".apex");
|
|
8957
9186
|
}
|
|
8958
9187
|
function filesystemSessionPath(folder) {
|
|
8959
|
-
return
|
|
9188
|
+
return import_node_path5.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
|
|
8960
9189
|
}
|
|
8961
9190
|
function writeFilesystemSession(folder, session) {
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
9191
|
+
import_node_fs7.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
|
|
9192
|
+
import_node_fs7.default.writeFileSync(
|
|
9193
|
+
import_node_path5.default.join(filesystemApexDir(folder), ".gitignore"),
|
|
8965
9194
|
"experiment-filesystem.json\n",
|
|
8966
9195
|
"utf8"
|
|
8967
9196
|
);
|
|
8968
|
-
|
|
9197
|
+
import_node_fs7.default.writeFileSync(filesystemSessionPath(folder), `${JSON.stringify(session, null, 2)}
|
|
8969
9198
|
`, "utf8");
|
|
8970
9199
|
}
|
|
8971
9200
|
function readFilesystemSession(folder) {
|
|
8972
9201
|
const filePath = filesystemSessionPath(folder);
|
|
8973
|
-
if (!
|
|
8974
|
-
const workspaceMarker =
|
|
8975
|
-
if (
|
|
9202
|
+
if (!import_node_fs7.default.existsSync(filePath)) {
|
|
9203
|
+
const workspaceMarker = import_node_path5.default.join(filesystemApexDir(folder), "session.json");
|
|
9204
|
+
if (import_node_fs7.default.existsSync(workspaceMarker)) {
|
|
8976
9205
|
throw new Error(
|
|
8977
9206
|
"This folder is an operator workspace (apex workspace \u2026), not an experiment filesystem \u2014 run `apex workspace status` here, or `apex experiments pull` into a different folder."
|
|
8978
9207
|
);
|
|
8979
9208
|
}
|
|
8980
9209
|
throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
|
|
8981
9210
|
}
|
|
8982
|
-
return JSON.parse(
|
|
9211
|
+
return JSON.parse(import_node_fs7.default.readFileSync(filePath, "utf8"));
|
|
8983
9212
|
}
|
|
8984
9213
|
function validateFilesystemPath(relativePath) {
|
|
8985
9214
|
const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
@@ -8995,42 +9224,87 @@ function writeExperimentFilesystemFiles(folder, files) {
|
|
|
8995
9224
|
}));
|
|
8996
9225
|
for (const file of validatedFiles) {
|
|
8997
9226
|
const relativePath = file.relativePath;
|
|
8998
|
-
const absPath =
|
|
8999
|
-
|
|
9000
|
-
|
|
9227
|
+
const absPath = import_node_path5.default.join(folder, relativePath);
|
|
9228
|
+
import_node_fs7.default.mkdirSync(import_node_path5.default.dirname(absPath), { recursive: true });
|
|
9229
|
+
import_node_fs7.default.writeFileSync(absPath, file.content, "utf8");
|
|
9001
9230
|
}
|
|
9002
9231
|
}
|
|
9003
|
-
function listExperimentFilesystemFiles(folder) {
|
|
9232
|
+
function listExperimentFilesystemFiles(folder, session) {
|
|
9004
9233
|
const files = [];
|
|
9005
9234
|
for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel) {
|
|
9006
|
-
const absPath =
|
|
9007
|
-
if (!
|
|
9008
|
-
const content =
|
|
9009
|
-
|
|
9235
|
+
const absPath = import_node_path5.default.join(folder, fileName);
|
|
9236
|
+
if (!import_node_fs7.default.existsSync(absPath)) continue;
|
|
9237
|
+
const content = import_node_fs7.default.readFileSync(absPath, "utf8");
|
|
9238
|
+
const sha256 = sha256Text(content);
|
|
9239
|
+
if (SERVER_OWNED_EXPERIMENT_FILES.has(fileName)) {
|
|
9240
|
+
if (session.files[fileName]?.sha256 !== sha256) {
|
|
9241
|
+
console.error(
|
|
9242
|
+
`${fileName} is server-owned; local edits are ignored (dashboard flow owns QA state)`
|
|
9243
|
+
);
|
|
9244
|
+
}
|
|
9245
|
+
continue;
|
|
9246
|
+
}
|
|
9247
|
+
files.push({ path: fileName, content, sha256 });
|
|
9010
9248
|
}
|
|
9011
9249
|
for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
|
|
9012
9250
|
const relativePath = `experiment/${fileName}`;
|
|
9013
|
-
const absPath =
|
|
9014
|
-
if (!
|
|
9015
|
-
const content =
|
|
9251
|
+
const absPath = import_node_path5.default.join(folder, relativePath);
|
|
9252
|
+
if (!import_node_fs7.default.existsSync(absPath)) continue;
|
|
9253
|
+
const content = import_node_fs7.default.readFileSync(absPath, "utf8");
|
|
9016
9254
|
files.push({ path: relativePath, content, sha256: sha256Text(content) });
|
|
9017
9255
|
}
|
|
9018
|
-
const variationsRoot =
|
|
9019
|
-
if (
|
|
9020
|
-
for (const variationDir of
|
|
9256
|
+
const variationsRoot = import_node_path5.default.join(folder, "variations");
|
|
9257
|
+
if (import_node_fs7.default.existsSync(variationsRoot)) {
|
|
9258
|
+
for (const variationDir of import_node_fs7.default.readdirSync(variationsRoot, { withFileTypes: true })) {
|
|
9021
9259
|
if (!variationDir.isDirectory()) continue;
|
|
9022
|
-
const base =
|
|
9260
|
+
const base = import_node_path5.default.join(variationsRoot, variationDir.name);
|
|
9023
9261
|
for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation) {
|
|
9024
|
-
const absPath =
|
|
9025
|
-
if (!
|
|
9026
|
-
const relativePath = validateFilesystemPath(
|
|
9027
|
-
const content =
|
|
9262
|
+
const absPath = import_node_path5.default.join(base, fileName);
|
|
9263
|
+
if (!import_node_fs7.default.existsSync(absPath)) continue;
|
|
9264
|
+
const relativePath = validateFilesystemPath(import_node_path5.default.relative(folder, absPath));
|
|
9265
|
+
const content = import_node_fs7.default.readFileSync(absPath, "utf8");
|
|
9028
9266
|
files.push({ path: relativePath, content, sha256: sha256Text(content) });
|
|
9029
9267
|
}
|
|
9030
9268
|
}
|
|
9031
9269
|
}
|
|
9032
9270
|
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
9033
9271
|
}
|
|
9272
|
+
async function rewriteExperimentFilesystemAssets(input) {
|
|
9273
|
+
const rewriter = createAssetReferenceRewriter({
|
|
9274
|
+
baseDir: input.folder,
|
|
9275
|
+
baseUrl: input.baseUrl,
|
|
9276
|
+
enabled: input.enabled,
|
|
9277
|
+
upload: async (file) => {
|
|
9278
|
+
const response = await input.upload(input.credentials, [file]);
|
|
9279
|
+
const asset = response.data?.assets?.[0];
|
|
9280
|
+
if (!response.ok || !asset?.id) {
|
|
9281
|
+
throw new Error(response.error ?? `Failed to upload ${file.name}.`);
|
|
9282
|
+
}
|
|
9283
|
+
return {
|
|
9284
|
+
id: asset.id,
|
|
9285
|
+
url: typeof asset.url === "string" ? asset.url : void 0,
|
|
9286
|
+
deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : void 0,
|
|
9287
|
+
delivery_url: typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
|
|
9288
|
+
};
|
|
9289
|
+
},
|
|
9290
|
+
onUpload: ({ name, url }) => {
|
|
9291
|
+
process.stderr.write(`Uploaded asset ${name} -> ${url}
|
|
9292
|
+
`);
|
|
9293
|
+
}
|
|
9294
|
+
});
|
|
9295
|
+
return Promise.all(
|
|
9296
|
+
input.files.map(async (file) => {
|
|
9297
|
+
if (!shouldRewriteExperimentFilesystemAssetReferences(file.path)) {
|
|
9298
|
+
return file;
|
|
9299
|
+
}
|
|
9300
|
+
const content = await rewriter.rewrite(
|
|
9301
|
+
file.content,
|
|
9302
|
+
import_node_path5.default.dirname(import_node_path5.default.join(input.folder, file.path))
|
|
9303
|
+
);
|
|
9304
|
+
return { ...file, content, sha256: sha256Text(content) };
|
|
9305
|
+
})
|
|
9306
|
+
);
|
|
9307
|
+
}
|
|
9034
9308
|
function printFilesystemPlan(plan) {
|
|
9035
9309
|
if (plan.errors.length > 0) {
|
|
9036
9310
|
console.log(source_default.red("\nErrors"));
|
|
@@ -9123,6 +9397,15 @@ function printQaRun(run3) {
|
|
|
9123
9397
|
console.log(` - ${url.label}: ${source_default.cyan(url.url)}`);
|
|
9124
9398
|
});
|
|
9125
9399
|
}
|
|
9400
|
+
if ((run3.evidenceRecords ?? []).length > 0) {
|
|
9401
|
+
console.log(source_default.bold("\nEvidence records"));
|
|
9402
|
+
for (const record2 of run3.evidenceRecords ?? []) {
|
|
9403
|
+
const device = record2.device ? ` \xB7 ${record2.device}` : "";
|
|
9404
|
+
console.log(
|
|
9405
|
+
` - ${record2.kind} \xB7 ${record2.label} \xB7 ${record2.status}${device} \xB7 ${record2.capturedAt}`
|
|
9406
|
+
);
|
|
9407
|
+
}
|
|
9408
|
+
}
|
|
9126
9409
|
console.log();
|
|
9127
9410
|
}
|
|
9128
9411
|
async function confirmApply(message) {
|
|
@@ -9140,7 +9423,7 @@ async function confirmApply(message) {
|
|
|
9140
9423
|
function parseEvidenceInput(value) {
|
|
9141
9424
|
if (!value) return void 0;
|
|
9142
9425
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
9143
|
-
const text = value.startsWith("@") ||
|
|
9426
|
+
const text = value.startsWith("@") || import_node_fs7.default.existsSync(source) ? import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), source), "utf8") : value;
|
|
9144
9427
|
const parsed = JSON.parse(text);
|
|
9145
9428
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9146
9429
|
throw new Error("QA evidence must be a JSON object");
|
|
@@ -9164,7 +9447,7 @@ async function resolveExperimentForFilesystemOrId(value) {
|
|
|
9164
9447
|
return {
|
|
9165
9448
|
experimentId: session.experimentId,
|
|
9166
9449
|
folder: candidate,
|
|
9167
|
-
label:
|
|
9450
|
+
label: import_node_path5.default.basename(candidate)
|
|
9168
9451
|
};
|
|
9169
9452
|
}
|
|
9170
9453
|
const exp = await resolveExperiment(value);
|
|
@@ -9190,6 +9473,27 @@ async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experime
|
|
|
9190
9473
|
} catch {
|
|
9191
9474
|
}
|
|
9192
9475
|
}
|
|
9476
|
+
async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
|
|
9477
|
+
if (!folder) return;
|
|
9478
|
+
try {
|
|
9479
|
+
const refreshed = await getExperimentFilesystem(creds, experimentId);
|
|
9480
|
+
if (!refreshed.ok || !refreshed.data) return;
|
|
9481
|
+
const serverOwned = refreshed.data.files.filter(
|
|
9482
|
+
(file) => SERVER_OWNED_EXPERIMENT_FILES.has(file.path)
|
|
9483
|
+
);
|
|
9484
|
+
if (serverOwned.length === 0) return;
|
|
9485
|
+
const session = readFilesystemSession(folder);
|
|
9486
|
+
writeExperimentFilesystemFiles(folder, serverOwned);
|
|
9487
|
+
writeFilesystemSession(folder, {
|
|
9488
|
+
...session,
|
|
9489
|
+
files: {
|
|
9490
|
+
...session.files,
|
|
9491
|
+
...Object.fromEntries(serverOwned.map((file) => [file.path, { sha256: file.sha256 }]))
|
|
9492
|
+
}
|
|
9493
|
+
});
|
|
9494
|
+
} catch {
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9193
9497
|
async function resolveExperiment(idOrSlug) {
|
|
9194
9498
|
const creds = requireCredentials();
|
|
9195
9499
|
if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
|
|
@@ -9282,7 +9586,7 @@ var results = new Command("results").description("Read experiment results and ti
|
|
|
9282
9586
|
output(
|
|
9283
9587
|
this,
|
|
9284
9588
|
{ experiment: exp, results: res.data },
|
|
9285
|
-
() => printExperimentResults(res.data)
|
|
9589
|
+
() => printExperimentResults(res.data, opts.currency)
|
|
9286
9590
|
);
|
|
9287
9591
|
} catch (error) {
|
|
9288
9592
|
spinner?.fail("Failed to fetch experiment results");
|
|
@@ -9329,7 +9633,7 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9329
9633
|
"Redirect treatment destination (repeatable; creates one treatment per URL)",
|
|
9330
9634
|
collectCreateOption,
|
|
9331
9635
|
[]
|
|
9332
|
-
).option("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async function action6(opts) {
|
|
9636
|
+
).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--backlog-item <id>", "Backlog item UUID or Apex backlog key").action(async function action6(opts) {
|
|
9333
9637
|
if (opts.type !== "mutation" && opts.type !== "redirect") {
|
|
9334
9638
|
fail(this, "--type must be mutation or redirect", EXIT_CODES.validation);
|
|
9335
9639
|
}
|
|
@@ -9361,6 +9665,53 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9361
9665
|
const destinations = opts.destination.map(parseHttpDestination);
|
|
9362
9666
|
const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
|
|
9363
9667
|
const creds = requireCredentials();
|
|
9668
|
+
let backlogItemId;
|
|
9669
|
+
if (opts.backlogItem !== void 0) {
|
|
9670
|
+
const ref = opts.backlogItem.trim();
|
|
9671
|
+
if (!ref) {
|
|
9672
|
+
fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
|
|
9673
|
+
}
|
|
9674
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(ref)) {
|
|
9675
|
+
backlogItemId = ref;
|
|
9676
|
+
} else if (/^[A-Za-z0-9]{2,8}-\d+$/i.test(ref)) {
|
|
9677
|
+
let cursor;
|
|
9678
|
+
let resolvedId;
|
|
9679
|
+
do {
|
|
9680
|
+
const page = await listInsights(creds, "backlog", 100, cursor);
|
|
9681
|
+
if (!page.ok || !page.data) {
|
|
9682
|
+
fail(
|
|
9683
|
+
this,
|
|
9684
|
+
page.error ?? `Unable to resolve backlog item "${ref}"`,
|
|
9685
|
+
classifyStatus(page.status),
|
|
9686
|
+
page.errorDetails
|
|
9687
|
+
);
|
|
9688
|
+
}
|
|
9689
|
+
const match2 = page.data.data.find((item) => {
|
|
9690
|
+
const metadata = item.metadata && typeof item.metadata === "object" && !Array.isArray(item.metadata) ? item.metadata : {};
|
|
9691
|
+
return typeof metadata.backlog_key === "string" && metadata.backlog_key.toUpperCase() === ref.toUpperCase();
|
|
9692
|
+
});
|
|
9693
|
+
resolvedId = match2?.id;
|
|
9694
|
+
cursor = page.data.pagination.has_more ? page.data.pagination.next_cursor ?? void 0 : void 0;
|
|
9695
|
+
} while (!resolvedId && cursor);
|
|
9696
|
+
const backlog = resolvedId ? { ok: true, status: 200, data: { id: resolvedId } } : await getInsight(creds, "backlog", ref);
|
|
9697
|
+
if (!backlog.ok || !backlog.data?.id) {
|
|
9698
|
+
fail(
|
|
9699
|
+
this,
|
|
9700
|
+
backlog.error ?? `Backlog item "${ref}" not found`,
|
|
9701
|
+
classifyStatus(backlog.status),
|
|
9702
|
+
backlog.errorDetails
|
|
9703
|
+
);
|
|
9704
|
+
}
|
|
9705
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
9706
|
+
backlog.data.id
|
|
9707
|
+
)) {
|
|
9708
|
+
fail(this, `Backlog item "${ref}" not found`, EXIT_CODES.validation);
|
|
9709
|
+
}
|
|
9710
|
+
backlogItemId = backlog.data.id;
|
|
9711
|
+
} else {
|
|
9712
|
+
fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9364
9715
|
const spinner = await createSpinner("Creating experiment...");
|
|
9365
9716
|
const res = await createExperiment(creds, {
|
|
9366
9717
|
name: opts.name,
|
|
@@ -9368,6 +9719,7 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9368
9719
|
description: opts.description ?? null,
|
|
9369
9720
|
delivery_type: opts.type,
|
|
9370
9721
|
traffic_allocation: traffic / 100,
|
|
9722
|
+
...backlogItemId ? { backlog_item_id: backlogItemId } : {},
|
|
9371
9723
|
...sourceRules.length > 0 ? {
|
|
9372
9724
|
store_url: sourceStoreUrl(sourceRules),
|
|
9373
9725
|
targeting: {
|
|
@@ -9403,8 +9755,15 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9403
9755
|
res.errorDetails
|
|
9404
9756
|
);
|
|
9405
9757
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9758
|
+
const outcome = res.status === 200 ? "already_linked" : "created";
|
|
9759
|
+
spinner?.succeed(
|
|
9760
|
+
outcome === "created" ? "Experiment created" : `Already linked \u2014 returning existing experiment ${res.data.test_key ?? res.data.slug ?? res.data.id}`
|
|
9761
|
+
);
|
|
9762
|
+
output(
|
|
9763
|
+
this,
|
|
9764
|
+
{ outcome, backlog_item_id: backlogItemId ?? null, experiment: res.data },
|
|
9765
|
+
() => printExperiment(res.data)
|
|
9766
|
+
);
|
|
9408
9767
|
});
|
|
9409
9768
|
var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
|
|
9410
9769
|
const creds = requireCredentials();
|
|
@@ -9524,7 +9883,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
|
|
|
9524
9883
|
{
|
|
9525
9884
|
folder,
|
|
9526
9885
|
experimentId: exp.id,
|
|
9527
|
-
files: res.data.files.map((file) =>
|
|
9886
|
+
files: res.data.files.map((file) => import_node_path5.default.join(folder, file.path))
|
|
9528
9887
|
},
|
|
9529
9888
|
() => {
|
|
9530
9889
|
spinner.succeed(`Pulled ${exp.name}`);
|
|
@@ -9541,7 +9900,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9541
9900
|
try {
|
|
9542
9901
|
const folder = folderPath(folderArg);
|
|
9543
9902
|
const session = readFilesystemSession(folder);
|
|
9544
|
-
const files = listExperimentFilesystemFiles(folder);
|
|
9903
|
+
const files = listExperimentFilesystemFiles(folder, session);
|
|
9545
9904
|
const res = await planExperimentFilesystem(creds, session.experimentId, { files });
|
|
9546
9905
|
if (!res.ok || !res.data) {
|
|
9547
9906
|
fail(
|
|
@@ -9558,6 +9917,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9558
9917
|
plan: res.data
|
|
9559
9918
|
});
|
|
9560
9919
|
}
|
|
9920
|
+
await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId);
|
|
9561
9921
|
output(
|
|
9562
9922
|
this,
|
|
9563
9923
|
{ folder, experimentId: session.experimentId, plan: res.data },
|
|
@@ -9568,12 +9928,19 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9568
9928
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
9569
9929
|
}
|
|
9570
9930
|
});
|
|
9571
|
-
var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").action(async function action12(folderArg, opts) {
|
|
9931
|
+
var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action12(folderArg, opts) {
|
|
9572
9932
|
const creds = requireCredentials();
|
|
9573
9933
|
try {
|
|
9574
9934
|
const folder = folderPath(folderArg);
|
|
9575
9935
|
const session = readFilesystemSession(folder);
|
|
9576
|
-
const files =
|
|
9936
|
+
const files = await rewriteExperimentFilesystemAssets({
|
|
9937
|
+
files: listExperimentFilesystemFiles(folder, session),
|
|
9938
|
+
folder,
|
|
9939
|
+
baseUrl: creds.baseUrl,
|
|
9940
|
+
enabled: opts.assetUpload !== false,
|
|
9941
|
+
upload: uploadAssets,
|
|
9942
|
+
credentials: creds
|
|
9943
|
+
});
|
|
9577
9944
|
const planRes = await planExperimentFilesystem(creds, session.experimentId, { files });
|
|
9578
9945
|
if (!planRes.ok || !planRes.data) {
|
|
9579
9946
|
fail(
|
|
@@ -9585,6 +9952,12 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
9585
9952
|
}
|
|
9586
9953
|
if (!planRes.data.ok) {
|
|
9587
9954
|
if (!shouldUseJson(this)) printFilesystemPlan(planRes.data);
|
|
9955
|
+
if (opts.assetUpload === false && planRes.data.blockingIssues?.some((issue) => /inline|hosted asset/i.test(issue.message))) {
|
|
9956
|
+
process.stderr.write(
|
|
9957
|
+
`Retry with hosted asset upload: apex experiments apply ${folderArg}
|
|
9958
|
+
`
|
|
9959
|
+
);
|
|
9960
|
+
}
|
|
9588
9961
|
fail(this, blockingIssueSummary(planRes.data.blockingIssues), EXIT_CODES.validation, {
|
|
9589
9962
|
errors: planRes.data.errors,
|
|
9590
9963
|
plan: planRes.data
|
|
@@ -9810,7 +10183,93 @@ var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled
|
|
|
9810
10183
|
throw error;
|
|
9811
10184
|
}
|
|
9812
10185
|
});
|
|
9813
|
-
|
|
10186
|
+
function parseJsonValue(value) {
|
|
10187
|
+
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
10188
|
+
const text = value.startsWith("@") || import_node_fs7.default.existsSync(source) ? import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), source), "utf8") : value;
|
|
10189
|
+
return JSON.parse(text);
|
|
10190
|
+
}
|
|
10191
|
+
function collectFile(value, previous = []) {
|
|
10192
|
+
return [...previous, value];
|
|
10193
|
+
}
|
|
10194
|
+
function normalizeEvidenceDevice(value) {
|
|
10195
|
+
if (!value) return void 0;
|
|
10196
|
+
const normalized = value.trim().toLowerCase();
|
|
10197
|
+
if (normalized === "desktop_chrome" || normalized === "desktop_safari") return "desktop";
|
|
10198
|
+
if (normalized === "iphone_15_pro" || normalized === "galaxy_s24") return "mobile";
|
|
10199
|
+
if (normalized === "ipad_pro") return "tablet";
|
|
10200
|
+
return normalized;
|
|
10201
|
+
}
|
|
10202
|
+
function resolveEvidenceVariation(experiment, ref) {
|
|
10203
|
+
if (!ref) return void 0;
|
|
10204
|
+
const exact = experiment.variations?.find(
|
|
10205
|
+
(variation) => variation.id === ref || variation.slug === ref
|
|
10206
|
+
);
|
|
10207
|
+
if (exact) return exact.id;
|
|
10208
|
+
const matches = (experiment.variations ?? []).filter(
|
|
10209
|
+
(variation) => variation.name.toLowerCase() === ref.toLowerCase()
|
|
10210
|
+
);
|
|
10211
|
+
if (matches.length === 1) return matches[0].id;
|
|
10212
|
+
if (matches.length > 1) {
|
|
10213
|
+
throw new Error(`Ambiguous --variation value "${ref}". Use a variation ID or slug.`);
|
|
10214
|
+
}
|
|
10215
|
+
throw new Error(`Unknown --variation value: ${ref}`);
|
|
10216
|
+
}
|
|
10217
|
+
var qaEvidence = new Command("evidence").description("Attach screenshot jobs or upload typed QA evidence").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--job <jobId>", "Attach a terminal screenshot job").option("--file <path>", "Upload an evidence image; repeat for more files", collectFile, []).option("--label <text>", "Evidence label").option("--device <device>", "desktop, mobile, tablet, or a raw device value").option("--variation <ref>", "Variation ID, slug, or name").option("--status <status>", "Evidence status").option("--kind <kind>", "Evidence kind", "screenshot").option("--records <json-or-@path>", "Typed evidence record array").option("--json", "Print the QA run in the standard JSON envelope").action(async function action18(target, opts) {
|
|
10218
|
+
const modes = Number(Boolean(opts.job)) + Number(opts.file.length > 0) + Number(Boolean(opts.records));
|
|
10219
|
+
if (modes !== 1) {
|
|
10220
|
+
fail(this, "Provide exactly one of --job, --file, or --records.", EXIT_CODES.validation);
|
|
10221
|
+
}
|
|
10222
|
+
const creds = requireCredentials();
|
|
10223
|
+
const spinner = await createSpinner("Writing QA evidence...");
|
|
10224
|
+
try {
|
|
10225
|
+
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
10226
|
+
let response;
|
|
10227
|
+
if (opts.job) {
|
|
10228
|
+
response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
|
|
10229
|
+
screenshot_job_id: opts.job
|
|
10230
|
+
});
|
|
10231
|
+
} else if (opts.records) {
|
|
10232
|
+
const parsed = parseJsonValue(opts.records);
|
|
10233
|
+
if (!Array.isArray(parsed)) throw new Error("--records must contain a JSON array.");
|
|
10234
|
+
response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
|
|
10235
|
+
records: parsed
|
|
10236
|
+
});
|
|
10237
|
+
} else {
|
|
10238
|
+
const experiment = await resolveExperiment(resolved.experimentId);
|
|
10239
|
+
if (!experiment) throw new Error("Experiment not found.");
|
|
10240
|
+
const variationId = resolveEvidenceVariation(experiment, opts.variation);
|
|
10241
|
+
const form = new FormData();
|
|
10242
|
+
for (const filePath of opts.file) {
|
|
10243
|
+
const absolutePath = import_node_path5.default.resolve(process.cwd(), filePath);
|
|
10244
|
+
if (!import_node_fs7.default.existsSync(absolutePath)) throw new Error(`Evidence file not found: ${filePath}`);
|
|
10245
|
+
const bytes = import_node_fs7.default.readFileSync(absolutePath);
|
|
10246
|
+
const extension = import_node_path5.default.extname(absolutePath).toLowerCase();
|
|
10247
|
+
const mimeType = extension === ".png" ? "image/png" : extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : "application/octet-stream";
|
|
10248
|
+
form.append("files", new File([bytes], import_node_path5.default.basename(absolutePath), { type: mimeType }));
|
|
10249
|
+
}
|
|
10250
|
+
if (opts.label) form.append("label", opts.label);
|
|
10251
|
+
const device = normalizeEvidenceDevice(opts.device);
|
|
10252
|
+
if (device) form.append("device", device);
|
|
10253
|
+
if (variationId) form.append("variation_id", variationId);
|
|
10254
|
+
if (opts.status) form.append("status", opts.status);
|
|
10255
|
+
if (opts.kind) form.append("kind", opts.kind);
|
|
10256
|
+
response = await uploadExperimentQaEvidence(creds, resolved.experimentId, form);
|
|
10257
|
+
}
|
|
10258
|
+
if (!response.ok || !response.data) {
|
|
10259
|
+
throw new CliError(
|
|
10260
|
+
response.error ?? "Failed to write QA evidence",
|
|
10261
|
+
classifyStatus(response.status),
|
|
10262
|
+
response.errorDetails
|
|
10263
|
+
);
|
|
10264
|
+
}
|
|
10265
|
+
spinner.stop();
|
|
10266
|
+
output(this, { run: response.data.run }, () => printQaRun(response.data.run));
|
|
10267
|
+
} catch (error) {
|
|
10268
|
+
spinner.fail("Failed to write QA evidence");
|
|
10269
|
+
throw error;
|
|
10270
|
+
}
|
|
10271
|
+
});
|
|
10272
|
+
var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
|
|
9814
10273
|
var SCREENSHOT_DEVICES = [
|
|
9815
10274
|
"desktop_chrome",
|
|
9816
10275
|
"iphone_15_pro",
|
|
@@ -9825,11 +10284,11 @@ var screenshots = new Command("screenshots").description(
|
|
|
9825
10284
|
).argument("<experimentRef>", "Experiment ID or slug").option("--url <target>", "Target URL override (not supported by the current v1 API)").option("--devices <list>", `Comma-separated devices (default: ${SCREENSHOT_DEVICES.join(",")})`).option(
|
|
9826
10285
|
"--variations <list>",
|
|
9827
10286
|
"Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned"
|
|
9828
|
-
).option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option(
|
|
10287
|
+
).option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--no-attach-qa", "Do not attach the completed job to the QA run").option(
|
|
9829
10288
|
"--timeout <seconds>",
|
|
9830
10289
|
"Maximum wait for completion; finished results are always downloaded",
|
|
9831
10290
|
"300"
|
|
9832
|
-
).action(async function
|
|
10291
|
+
).action(async function action19(experimentRef, opts) {
|
|
9833
10292
|
if (opts.url)
|
|
9834
10293
|
fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
|
|
9835
10294
|
const timeoutSeconds = Number(opts.timeout);
|
|
@@ -9934,10 +10393,31 @@ var screenshots = new Command("screenshots").description(
|
|
|
9934
10393
|
job = polled.data;
|
|
9935
10394
|
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
9936
10395
|
}
|
|
10396
|
+
if (opts.attachQa !== false) {
|
|
10397
|
+
const attached = await attachExperimentQaEvidence(creds, experiment.id, {
|
|
10398
|
+
screenshot_job_id: job.job_id
|
|
10399
|
+
});
|
|
10400
|
+
if (!attached.ok) {
|
|
10401
|
+
if (attached.status === 403) {
|
|
10402
|
+
console.error(
|
|
10403
|
+
source_default.yellow(
|
|
10404
|
+
"Warning: this API key cannot attach QA evidence; screenshots will still download."
|
|
10405
|
+
)
|
|
10406
|
+
);
|
|
10407
|
+
} else {
|
|
10408
|
+
fail(
|
|
10409
|
+
this,
|
|
10410
|
+
attached.error ?? "Failed to attach screenshot job to QA",
|
|
10411
|
+
classifyStatus(attached.status),
|
|
10412
|
+
attached.errorDetails
|
|
10413
|
+
);
|
|
10414
|
+
}
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
9937
10417
|
if (job.status === "failed")
|
|
9938
10418
|
fail(this, `Screenshot job failed: ${job.error ?? "unknown error"}`);
|
|
9939
|
-
const outputDir =
|
|
9940
|
-
|
|
10419
|
+
const outputDir = import_node_path5.default.resolve(process.cwd(), opts.out);
|
|
10420
|
+
import_node_fs7.default.mkdirSync(outputDir, { recursive: true });
|
|
9941
10421
|
const rows = [];
|
|
9942
10422
|
const errors = [];
|
|
9943
10423
|
const usedFilenames = /* @__PURE__ */ new Set();
|
|
@@ -9979,8 +10459,8 @@ var screenshots = new Command("screenshots").description(
|
|
|
9979
10459
|
try {
|
|
9980
10460
|
const response = await fetch(url);
|
|
9981
10461
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
9982
|
-
await
|
|
9983
|
-
|
|
10462
|
+
await import_node_fs7.default.promises.writeFile(
|
|
10463
|
+
import_node_path5.default.join(outputDir, filename),
|
|
9984
10464
|
Buffer.from(await response.arrayBuffer())
|
|
9985
10465
|
);
|
|
9986
10466
|
workerResults.push({ index, row: [variation.name, device, filename] });
|
|
@@ -10020,9 +10500,9 @@ Examples:
|
|
|
10020
10500
|
|
|
10021
10501
|
// src/commands/push.ts
|
|
10022
10502
|
init_source();
|
|
10023
|
-
var
|
|
10503
|
+
var import_node_fs8 = __toESM(require("node:fs"));
|
|
10024
10504
|
var import_node_os6 = __toESM(require("node:os"));
|
|
10025
|
-
var
|
|
10505
|
+
var import_node_path6 = __toESM(require("node:path"));
|
|
10026
10506
|
var import_node_url2 = require("node:url");
|
|
10027
10507
|
function toApiBody(exp) {
|
|
10028
10508
|
return {
|
|
@@ -10045,8 +10525,25 @@ function toApiBody(exp) {
|
|
|
10045
10525
|
goal_ids: exp.goals?.map((g) => g.id)
|
|
10046
10526
|
};
|
|
10047
10527
|
}
|
|
10528
|
+
async function rewriteExperimentAssets(exp, rewrite, baseDir) {
|
|
10529
|
+
return {
|
|
10530
|
+
...exp,
|
|
10531
|
+
variations: await Promise.all(
|
|
10532
|
+
exp.variations.map(async (variation) => ({
|
|
10533
|
+
...variation,
|
|
10534
|
+
mutations: await rewrite.rewriteUnknown(variation.mutations ?? [], baseDir),
|
|
10535
|
+
css: variation.css === void 0 ? void 0 : await rewrite.rewrite(variation.css, baseDir),
|
|
10536
|
+
custom_css: variation.custom_css === void 0 ? void 0 : await rewrite.rewrite(variation.custom_css, baseDir),
|
|
10537
|
+
js: variation.js === void 0 ? void 0 : await rewrite.rewrite(variation.js, baseDir),
|
|
10538
|
+
custom_js: variation.custom_js === void 0 ? void 0 : await rewrite.rewrite(variation.custom_js, baseDir),
|
|
10539
|
+
resetJs: variation.resetJs === void 0 ? void 0 : await rewrite.rewrite(variation.resetJs, baseDir),
|
|
10540
|
+
reset_js: variation.reset_js === void 0 ? void 0 : await rewrite.rewrite(variation.reset_js, baseDir)
|
|
10541
|
+
}))
|
|
10542
|
+
)
|
|
10543
|
+
};
|
|
10544
|
+
}
|
|
10048
10545
|
async function importConfigModule(absPath) {
|
|
10049
|
-
const ext =
|
|
10546
|
+
const ext = import_node_path6.default.extname(absPath).toLowerCase();
|
|
10050
10547
|
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
10051
10548
|
return await import((0, import_node_url2.pathToFileURL)(absPath).href);
|
|
10052
10549
|
}
|
|
@@ -10060,8 +10557,8 @@ async function importConfigModule(absPath) {
|
|
|
10060
10557
|
` + (err instanceof Error ? err.message : String(err))
|
|
10061
10558
|
);
|
|
10062
10559
|
}
|
|
10063
|
-
const tmpDir =
|
|
10064
|
-
const outFile =
|
|
10560
|
+
const tmpDir = import_node_fs8.default.mkdtempSync(import_node_path6.default.join(import_node_os6.default.tmpdir(), "drip-config-"));
|
|
10561
|
+
const outFile = import_node_path6.default.join(tmpDir, "config.mjs");
|
|
10065
10562
|
try {
|
|
10066
10563
|
await esbuild.build({
|
|
10067
10564
|
entryPoints: [absPath],
|
|
@@ -10077,7 +10574,7 @@ async function importConfigModule(absPath) {
|
|
|
10077
10574
|
return await import(href);
|
|
10078
10575
|
} finally {
|
|
10079
10576
|
try {
|
|
10080
|
-
|
|
10577
|
+
import_node_fs8.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
10081
10578
|
} catch {
|
|
10082
10579
|
}
|
|
10083
10580
|
}
|
|
@@ -10087,7 +10584,7 @@ async function importConfigModule(absPath) {
|
|
|
10087
10584
|
function isRecord(value) {
|
|
10088
10585
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10089
10586
|
}
|
|
10090
|
-
var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function
|
|
10587
|
+
var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action20(opts) {
|
|
10091
10588
|
const creds = requireCredentials();
|
|
10092
10589
|
const configPath = opts.config ?? findConfigPath();
|
|
10093
10590
|
if (!configPath) {
|
|
@@ -10098,10 +10595,10 @@ var pushCommand = new Command("push").description("Push experiment definitions f
|
|
|
10098
10595
|
);
|
|
10099
10596
|
}
|
|
10100
10597
|
const spinner = process.stderr.isTTY ? await createSpinner(`Loading config from ${source_default.cyan(configPath)}...`) : null;
|
|
10598
|
+
const absoluteConfigPath = import_node_path6.default.resolve(configPath);
|
|
10101
10599
|
let experiments;
|
|
10102
10600
|
try {
|
|
10103
|
-
const
|
|
10104
|
-
const mod = await importConfigModule(absPath);
|
|
10601
|
+
const mod = await importConfigModule(absoluteConfigPath);
|
|
10105
10602
|
const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
|
|
10106
10603
|
if (Array.isArray(exported)) {
|
|
10107
10604
|
experiments = exported;
|
|
@@ -10164,17 +10661,57 @@ var pushCommand = new Command("push").description("Push experiment definitions f
|
|
|
10164
10661
|
);
|
|
10165
10662
|
return;
|
|
10166
10663
|
}
|
|
10664
|
+
const assetRewriter = createAssetReferenceRewriter({
|
|
10665
|
+
baseDir: import_node_path6.default.dirname(absoluteConfigPath),
|
|
10666
|
+
baseUrl: creds.baseUrl,
|
|
10667
|
+
enabled: opts.assetUpload !== false,
|
|
10668
|
+
upload: async (file) => {
|
|
10669
|
+
const response = await uploadAssets(creds, [file]);
|
|
10670
|
+
const asset = response.data?.assets?.[0];
|
|
10671
|
+
if (!response.ok || !asset?.id) {
|
|
10672
|
+
throw new Error(response.error ?? `Failed to upload ${file.name}.`);
|
|
10673
|
+
}
|
|
10674
|
+
return {
|
|
10675
|
+
id: asset.id,
|
|
10676
|
+
url: typeof asset.url === "string" ? asset.url : void 0,
|
|
10677
|
+
deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : void 0,
|
|
10678
|
+
delivery_url: typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
|
|
10679
|
+
};
|
|
10680
|
+
},
|
|
10681
|
+
onUpload: ({ name, url }) => {
|
|
10682
|
+
process.stderr.write(`Uploaded asset ${name} -> ${url}
|
|
10683
|
+
`);
|
|
10684
|
+
}
|
|
10685
|
+
});
|
|
10167
10686
|
let created = 0;
|
|
10168
10687
|
let updated = 0;
|
|
10169
10688
|
let failed = 0;
|
|
10170
10689
|
const results2 = [];
|
|
10171
10690
|
for (const exp of experiments) {
|
|
10172
10691
|
const s = process.stderr.isTTY ? await createSpinner(`Pushing ${source_default.cyan(exp.slug)}...`) : null;
|
|
10173
|
-
|
|
10692
|
+
let body5;
|
|
10693
|
+
try {
|
|
10694
|
+
body5 = toApiBody(
|
|
10695
|
+
await rewriteExperimentAssets(exp, assetRewriter, import_node_path6.default.dirname(absoluteConfigPath))
|
|
10696
|
+
);
|
|
10697
|
+
} catch (error) {
|
|
10698
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10699
|
+
s?.fail(`Failed: ${exp.slug}`);
|
|
10700
|
+
printError(message);
|
|
10701
|
+
failed++;
|
|
10702
|
+
results2.push({ slug: exp.slug, status: "failed", error: message });
|
|
10703
|
+
continue;
|
|
10704
|
+
}
|
|
10174
10705
|
const res = await upsertExperimentBySlug(creds, exp.slug, body5);
|
|
10175
10706
|
if (!res.ok) {
|
|
10176
10707
|
s?.fail(`Failed: ${exp.slug}`);
|
|
10177
10708
|
printError(res.error);
|
|
10709
|
+
if (opts.assetUpload === false && /inline|hosted asset/i.test(res.error ?? "")) {
|
|
10710
|
+
process.stderr.write(
|
|
10711
|
+
`Retry with hosted asset upload: apex push --config ${configPath}
|
|
10712
|
+
`
|
|
10713
|
+
);
|
|
10714
|
+
}
|
|
10178
10715
|
failed++;
|
|
10179
10716
|
results2.push({
|
|
10180
10717
|
slug: exp.slug,
|
|
@@ -10353,25 +10890,25 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10353
10890
|
|
|
10354
10891
|
// src/commands/harness.ts
|
|
10355
10892
|
init_source();
|
|
10356
|
-
var
|
|
10357
|
-
var
|
|
10893
|
+
var import_node_fs9 = __toESM(require("node:fs"));
|
|
10894
|
+
var import_node_path7 = __toESM(require("node:path"));
|
|
10358
10895
|
var import_node_child_process6 = require("node:child_process");
|
|
10359
10896
|
function slugify(value, fallback2 = "variation") {
|
|
10360
10897
|
const slug = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
10361
10898
|
return slug || fallback2;
|
|
10362
10899
|
}
|
|
10363
10900
|
function readJson(filePath) {
|
|
10364
|
-
if (!
|
|
10365
|
-
return JSON.parse(
|
|
10901
|
+
if (!import_node_fs9.default.existsSync(filePath)) return null;
|
|
10902
|
+
return JSON.parse(import_node_fs9.default.readFileSync(filePath, "utf8"));
|
|
10366
10903
|
}
|
|
10367
10904
|
function resolveFolder(folder) {
|
|
10368
|
-
return
|
|
10905
|
+
return import_node_path7.default.resolve(process.cwd(), folder);
|
|
10369
10906
|
}
|
|
10370
10907
|
function experimentConfigPath(folder) {
|
|
10371
|
-
return
|
|
10908
|
+
return import_node_path7.default.join(folder, "experiment.config.json");
|
|
10372
10909
|
}
|
|
10373
10910
|
function apexConfigPath(folder) {
|
|
10374
|
-
return
|
|
10911
|
+
return import_node_path7.default.join(folder, "apex.harness.json");
|
|
10375
10912
|
}
|
|
10376
10913
|
function loadHarnessConfig(folder) {
|
|
10377
10914
|
const experiment = readJson(experimentConfigPath(folder)) ?? {};
|
|
@@ -10380,8 +10917,8 @@ function loadHarnessConfig(folder) {
|
|
|
10380
10917
|
const targetOrigin = experiment.targetOrigin?.replace(/\/+$/, "") ?? "";
|
|
10381
10918
|
const defaultPath = experiment.defaultPath ?? "/";
|
|
10382
10919
|
const targetUrl = targetOrigin ? new URL(defaultPath, targetOrigin).toString() : "";
|
|
10383
|
-
const id = experiment.id ??
|
|
10384
|
-
const variation =
|
|
10920
|
+
const id = experiment.id ?? import_node_path7.default.basename(import_node_path7.default.dirname(folder));
|
|
10921
|
+
const variation = import_node_path7.default.basename(folder);
|
|
10385
10922
|
return {
|
|
10386
10923
|
experiment,
|
|
10387
10924
|
apex: {
|
|
@@ -10413,37 +10950,37 @@ function runAbHarness(args, cwd) {
|
|
|
10413
10950
|
function artifactPaths(folder, experiment) {
|
|
10414
10951
|
const variant = experiment.variant;
|
|
10415
10952
|
if (variant?.endsWith(".bundle.js")) {
|
|
10416
|
-
const jsPath =
|
|
10417
|
-
const cssPath =
|
|
10418
|
-
return { jsPath, cssPath:
|
|
10953
|
+
const jsPath = import_node_path7.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
|
|
10954
|
+
const cssPath = import_node_path7.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
|
|
10955
|
+
return { jsPath, cssPath: import_node_fs9.default.existsSync(cssPath) ? cssPath : null };
|
|
10419
10956
|
}
|
|
10420
|
-
const buildDir =
|
|
10421
|
-
const files =
|
|
10957
|
+
const buildDir = import_node_path7.default.join(folder, "build");
|
|
10958
|
+
const files = import_node_fs9.default.existsSync(buildDir) ? import_node_fs9.default.readdirSync(buildDir) : [];
|
|
10422
10959
|
const js = files.find(
|
|
10423
10960
|
(file) => file.endsWith(".js") && !file.endsWith(".bundle.js") && !file.endsWith(".min.js")
|
|
10424
10961
|
);
|
|
10425
10962
|
const css = files.find((file) => file.endsWith(".css") && !file.endsWith(".min.css"));
|
|
10426
10963
|
if (!js) throw new Error("Could not find harness JS artifact in build/");
|
|
10427
10964
|
return {
|
|
10428
|
-
jsPath:
|
|
10429
|
-
cssPath: css ?
|
|
10965
|
+
jsPath: import_node_path7.default.join(buildDir, js),
|
|
10966
|
+
cssPath: css ? import_node_path7.default.join(buildDir, css) : null
|
|
10430
10967
|
};
|
|
10431
10968
|
}
|
|
10432
10969
|
async function uploadVersion(params) {
|
|
10433
10970
|
const creds = requireCredentials();
|
|
10434
10971
|
const { jsPath, cssPath } = artifactPaths(params.folder, params.experiment);
|
|
10435
|
-
if (!
|
|
10972
|
+
if (!import_node_fs9.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
|
|
10436
10973
|
const form = new FormData();
|
|
10437
10974
|
form.append(
|
|
10438
10975
|
"js",
|
|
10439
|
-
new Blob([
|
|
10440
|
-
|
|
10976
|
+
new Blob([import_node_fs9.default.readFileSync(jsPath)], { type: "application/javascript" }),
|
|
10977
|
+
import_node_path7.default.basename(jsPath)
|
|
10441
10978
|
);
|
|
10442
|
-
if (cssPath &&
|
|
10979
|
+
if (cssPath && import_node_fs9.default.existsSync(cssPath) && import_node_fs9.default.statSync(cssPath).size > 0) {
|
|
10443
10980
|
form.append(
|
|
10444
10981
|
"css",
|
|
10445
|
-
new Blob([
|
|
10446
|
-
|
|
10982
|
+
new Blob([import_node_fs9.default.readFileSync(cssPath)], { type: "text/css" }),
|
|
10983
|
+
import_node_path7.default.basename(cssPath)
|
|
10447
10984
|
);
|
|
10448
10985
|
}
|
|
10449
10986
|
form.append(
|
|
@@ -10452,8 +10989,8 @@ async function uploadVersion(params) {
|
|
|
10452
10989
|
...params.apex,
|
|
10453
10990
|
resetJs: cleanupResetJs(params.experiment),
|
|
10454
10991
|
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10455
|
-
jsPath:
|
|
10456
|
-
cssPath: cssPath ?
|
|
10992
|
+
jsPath: import_node_path7.default.relative(params.folder, jsPath),
|
|
10993
|
+
cssPath: cssPath ? import_node_path7.default.relative(params.folder, cssPath) : null
|
|
10457
10994
|
})
|
|
10458
10995
|
);
|
|
10459
10996
|
const res = await uploadHarnessVersion(creds, params.session.sessionId, form);
|
|
@@ -10491,14 +11028,14 @@ async function createSession(folder) {
|
|
|
10491
11028
|
var harnessCommand = new Command("harness").description(
|
|
10492
11029
|
"Develop AB Test Harness projects through Apex live QA sessions"
|
|
10493
11030
|
);
|
|
10494
|
-
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function
|
|
11031
|
+
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action21(folderArg, opts) {
|
|
10495
11032
|
const folder = resolveFolder(folderArg);
|
|
10496
11033
|
const targetPath = apexConfigPath(folder);
|
|
10497
|
-
if (
|
|
11034
|
+
if (import_node_fs9.default.existsSync(targetPath) && !opts.force) {
|
|
10498
11035
|
fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
|
|
10499
11036
|
}
|
|
10500
11037
|
const { apex } = loadHarnessConfig(folder);
|
|
10501
|
-
|
|
11038
|
+
import_node_fs9.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
|
|
10502
11039
|
`, "utf8");
|
|
10503
11040
|
output(
|
|
10504
11041
|
this,
|
|
@@ -10511,7 +11048,7 @@ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment
|
|
|
10511
11048
|
() => printSuccess(`Created ${targetPath}`)
|
|
10512
11049
|
);
|
|
10513
11050
|
});
|
|
10514
|
-
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function
|
|
11051
|
+
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action22(folderArg, opts) {
|
|
10515
11052
|
const folder = resolveFolder(folderArg);
|
|
10516
11053
|
const created = await createSession(folder);
|
|
10517
11054
|
await runAbHarness(["build", folder], folder);
|
|
@@ -10564,7 +11101,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
|
|
|
10564
11101
|
}
|
|
10565
11102
|
};
|
|
10566
11103
|
let debounce = null;
|
|
10567
|
-
const watcher =
|
|
11104
|
+
const watcher = import_node_fs9.default.watch(folder, { recursive: true }, (_event, filename) => {
|
|
10568
11105
|
const name = String(filename ?? "");
|
|
10569
11106
|
if (!name || name.startsWith("build/") || name.includes("/build/")) return;
|
|
10570
11107
|
if (debounce) clearTimeout(debounce);
|
|
@@ -10578,7 +11115,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
|
|
|
10578
11115
|
process.exitCode = 0;
|
|
10579
11116
|
});
|
|
10580
11117
|
});
|
|
10581
|
-
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function
|
|
11118
|
+
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
|
|
10582
11119
|
const folder = resolveFolder(folderArg);
|
|
10583
11120
|
const created = await createSession(folder);
|
|
10584
11121
|
await runAbHarness(["verify", folder], folder);
|
|
@@ -10606,9 +11143,9 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
|
|
|
10606
11143
|
|
|
10607
11144
|
// src/commands/operator.ts
|
|
10608
11145
|
init_source();
|
|
10609
|
-
var
|
|
10610
|
-
var
|
|
10611
|
-
var
|
|
11146
|
+
var import_node_crypto5 = __toESM(require("node:crypto"));
|
|
11147
|
+
var import_node_fs10 = __toESM(require("node:fs"));
|
|
11148
|
+
var import_node_path8 = __toESM(require("node:path"));
|
|
10612
11149
|
var CANONICAL_FILE_RE = /^(src\/(?:index\.js|index\.scss|info\.js|mutations\.json)|experiment\.config\.json|metrics\.json|targeting\.json|dev-doc\.md|brand\.md|goals\/[a-zA-Z0-9_.-]+\.json|segments\/[a-zA-Z0-9_.-]+\.json|network\/[a-zA-Z0-9_.-]+\.recipe\.json)$/;
|
|
10613
11150
|
function workspaceIssueFile(field, evidence) {
|
|
10614
11151
|
if (field === "custom_js" || field === "reset_js") return "src/index.js";
|
|
@@ -10652,7 +11189,7 @@ function formatWorkspaceBlockingIssues(value) {
|
|
|
10652
11189
|
}).join("\n");
|
|
10653
11190
|
}
|
|
10654
11191
|
function sha256Text2(value) {
|
|
10655
|
-
return
|
|
11192
|
+
return import_node_crypto5.default.createHash("sha256").update(value).digest("hex");
|
|
10656
11193
|
}
|
|
10657
11194
|
function byteLength(value) {
|
|
10658
11195
|
return Buffer.byteLength(value, "utf8");
|
|
@@ -10662,28 +11199,28 @@ function slugify2(value, fallback2 = "operator") {
|
|
|
10662
11199
|
return slug || fallback2;
|
|
10663
11200
|
}
|
|
10664
11201
|
function folderPath2(folder) {
|
|
10665
|
-
return
|
|
11202
|
+
return import_node_path8.default.resolve(process.cwd(), folder);
|
|
10666
11203
|
}
|
|
10667
11204
|
function apexDir(folder) {
|
|
10668
|
-
return
|
|
11205
|
+
return import_node_path8.default.join(folder, ".apex");
|
|
10669
11206
|
}
|
|
10670
11207
|
function sessionPath(folder) {
|
|
10671
|
-
return
|
|
11208
|
+
return import_node_path8.default.join(apexDir(folder), "session.json");
|
|
10672
11209
|
}
|
|
10673
11210
|
function readJsonFile(filePath) {
|
|
10674
|
-
if (!
|
|
10675
|
-
return JSON.parse(
|
|
11211
|
+
if (!import_node_fs10.default.existsSync(filePath)) return null;
|
|
11212
|
+
return JSON.parse(import_node_fs10.default.readFileSync(filePath, "utf8"));
|
|
10676
11213
|
}
|
|
10677
11214
|
function writeJsonFile(filePath, value) {
|
|
10678
|
-
|
|
10679
|
-
|
|
11215
|
+
import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(filePath), { recursive: true });
|
|
11216
|
+
import_node_fs10.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
10680
11217
|
`, "utf8");
|
|
10681
11218
|
}
|
|
10682
11219
|
function readSession(folder) {
|
|
10683
11220
|
const session = readJsonFile(sessionPath(folder));
|
|
10684
11221
|
if (!session) {
|
|
10685
|
-
const filesystemMarker =
|
|
10686
|
-
if (
|
|
11222
|
+
const filesystemMarker = import_node_path8.default.join(apexDir(folder), "experiment-filesystem.json");
|
|
11223
|
+
if (import_node_fs10.default.existsSync(filesystemMarker)) {
|
|
10687
11224
|
throw new Error(
|
|
10688
11225
|
"This folder is an experiment filesystem (apex experiments \u2026), not an operator workspace \u2014 run `apex experiments plan` here, or `apex workspace checkout` into a different folder."
|
|
10689
11226
|
);
|
|
@@ -10693,9 +11230,9 @@ function readSession(folder) {
|
|
|
10693
11230
|
return session;
|
|
10694
11231
|
}
|
|
10695
11232
|
function writeSession(folder, session) {
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
11233
|
+
import_node_fs10.default.mkdirSync(apexDir(folder), { recursive: true });
|
|
11234
|
+
import_node_fs10.default.writeFileSync(
|
|
11235
|
+
import_node_path8.default.join(apexDir(folder), ".gitignore"),
|
|
10699
11236
|
"session.json\nharness-session.json\n",
|
|
10700
11237
|
"utf8"
|
|
10701
11238
|
);
|
|
@@ -10719,15 +11256,15 @@ function listLocalWorkspaceFiles(folder) {
|
|
|
10719
11256
|
"brand.md"
|
|
10720
11257
|
];
|
|
10721
11258
|
function visit2(dir) {
|
|
10722
|
-
if (!
|
|
10723
|
-
for (const entry of
|
|
10724
|
-
const absPath =
|
|
11259
|
+
if (!import_node_fs10.default.existsSync(dir)) return;
|
|
11260
|
+
for (const entry of import_node_fs10.default.readdirSync(dir, { withFileTypes: true })) {
|
|
11261
|
+
const absPath = import_node_path8.default.join(dir, entry.name);
|
|
10725
11262
|
if (entry.isDirectory()) {
|
|
10726
11263
|
visit2(absPath);
|
|
10727
11264
|
continue;
|
|
10728
11265
|
}
|
|
10729
|
-
const relativePath = validateCanonicalPath(
|
|
10730
|
-
const content =
|
|
11266
|
+
const relativePath = validateCanonicalPath(import_node_path8.default.relative(folder, absPath));
|
|
11267
|
+
const content = import_node_fs10.default.readFileSync(absPath, "utf8");
|
|
10731
11268
|
files.push({
|
|
10732
11269
|
path: relativePath,
|
|
10733
11270
|
absPath,
|
|
@@ -10737,11 +11274,11 @@ function listLocalWorkspaceFiles(folder) {
|
|
|
10737
11274
|
});
|
|
10738
11275
|
}
|
|
10739
11276
|
}
|
|
10740
|
-
for (const root of roots) visit2(
|
|
11277
|
+
for (const root of roots) visit2(import_node_path8.default.join(folder, root));
|
|
10741
11278
|
for (const file of topLevel) {
|
|
10742
|
-
const absPath =
|
|
10743
|
-
if (!
|
|
10744
|
-
const content =
|
|
11279
|
+
const absPath = import_node_path8.default.join(folder, file);
|
|
11280
|
+
if (!import_node_fs10.default.existsSync(absPath)) continue;
|
|
11281
|
+
const content = import_node_fs10.default.readFileSync(absPath, "utf8");
|
|
10745
11282
|
files.push({
|
|
10746
11283
|
path: file,
|
|
10747
11284
|
absPath,
|
|
@@ -10765,7 +11302,7 @@ function validateJsonFiles(files) {
|
|
|
10765
11302
|
}
|
|
10766
11303
|
}
|
|
10767
11304
|
function readExperimentConfig(folder) {
|
|
10768
|
-
return readJsonFile(
|
|
11305
|
+
return readJsonFile(import_node_path8.default.join(folder, "experiment.config.json")) ?? {};
|
|
10769
11306
|
}
|
|
10770
11307
|
function resolveVariation(exp, variationArg) {
|
|
10771
11308
|
const variations = exp.variations ?? [];
|
|
@@ -10782,9 +11319,9 @@ function resolveVariation(exp, variationArg) {
|
|
|
10782
11319
|
function writeWorkspaceFiles(folder, workspace) {
|
|
10783
11320
|
for (const file of workspace.files) {
|
|
10784
11321
|
const relativePath = validateCanonicalPath(file.path);
|
|
10785
|
-
const absPath =
|
|
10786
|
-
|
|
10787
|
-
|
|
11322
|
+
const absPath = import_node_path8.default.join(folder, relativePath);
|
|
11323
|
+
import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(absPath), { recursive: true });
|
|
11324
|
+
import_node_fs10.default.writeFileSync(absPath, file.content, "utf8");
|
|
10788
11325
|
}
|
|
10789
11326
|
}
|
|
10790
11327
|
function sessionFromWorkspace(input) {
|
|
@@ -10853,10 +11390,10 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
|
|
|
10853
11390
|
form.append(
|
|
10854
11391
|
"js",
|
|
10855
11392
|
new Blob([js.content], { type: "application/javascript" }),
|
|
10856
|
-
|
|
11393
|
+
import_node_path8.default.basename(js.path)
|
|
10857
11394
|
);
|
|
10858
11395
|
if (css && css.content.trim()) {
|
|
10859
|
-
form.append("css", new Blob([css.content], { type: "text/css" }),
|
|
11396
|
+
form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path8.default.basename(css.path));
|
|
10860
11397
|
}
|
|
10861
11398
|
form.append("manifest", JSON.stringify(manifest));
|
|
10862
11399
|
const res = await uploadHarnessVersion(creds, sessionId, form);
|
|
@@ -10953,7 +11490,7 @@ Examples:
|
|
|
10953
11490
|
apex workspace publish ./workspace
|
|
10954
11491
|
`
|
|
10955
11492
|
);
|
|
10956
|
-
operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function
|
|
11493
|
+
operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function action24(experimentId, opts) {
|
|
10957
11494
|
try {
|
|
10958
11495
|
const creds = requireCredentials();
|
|
10959
11496
|
const expRes = await getExperiment(creds, experimentId);
|
|
@@ -10975,7 +11512,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
|
|
|
10975
11512
|
classifyStatus(workspace.status),
|
|
10976
11513
|
workspace.errorDetails
|
|
10977
11514
|
);
|
|
10978
|
-
|
|
11515
|
+
import_node_fs10.default.mkdirSync(folder, { recursive: true });
|
|
10979
11516
|
writeWorkspaceFiles(folder, workspace.data);
|
|
10980
11517
|
writeSession(
|
|
10981
11518
|
folder,
|
|
@@ -11003,7 +11540,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
|
|
|
11003
11540
|
throw error;
|
|
11004
11541
|
}
|
|
11005
11542
|
});
|
|
11006
|
-
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function
|
|
11543
|
+
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
|
|
11007
11544
|
try {
|
|
11008
11545
|
const folder = folderPath2(folderArg);
|
|
11009
11546
|
const session = readSession(folder);
|
|
@@ -11035,14 +11572,14 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
|
|
|
11035
11572
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
11036
11573
|
}
|
|
11037
11574
|
});
|
|
11038
|
-
operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function
|
|
11575
|
+
operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function action26(folderArg, opts) {
|
|
11039
11576
|
try {
|
|
11040
11577
|
const folder = folderPath2(folderArg);
|
|
11041
11578
|
const session = readSession(folder);
|
|
11042
11579
|
const harnessSession = await createExactHarnessSession(folder, session);
|
|
11043
11580
|
const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
|
|
11044
|
-
|
|
11045
|
-
writeJsonFile(
|
|
11581
|
+
import_node_fs10.default.mkdirSync(apexDir(folder), { recursive: true });
|
|
11582
|
+
writeJsonFile(import_node_path8.default.join(apexDir(folder), "harness-session.json"), {
|
|
11046
11583
|
sessionId: harnessSession.sessionId,
|
|
11047
11584
|
experimentId: harnessSession.experimentId,
|
|
11048
11585
|
variationId: harnessSession.variationId,
|
|
@@ -11097,7 +11634,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
11097
11634
|
}
|
|
11098
11635
|
};
|
|
11099
11636
|
let timer = null;
|
|
11100
|
-
const watcher =
|
|
11637
|
+
const watcher = import_node_fs10.default.watch(folder, { recursive: true }, (_event, filename) => {
|
|
11101
11638
|
const name = String(filename ?? "");
|
|
11102
11639
|
if (!name || name.startsWith(".apex/") || name.includes("/.apex/")) return;
|
|
11103
11640
|
if (timer) clearTimeout(timer);
|
|
@@ -11114,7 +11651,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
11114
11651
|
throw error;
|
|
11115
11652
|
}
|
|
11116
11653
|
});
|
|
11117
|
-
operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function
|
|
11654
|
+
operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function action27(folderArg, opts) {
|
|
11118
11655
|
try {
|
|
11119
11656
|
const result = await runPublish(folderArg, opts);
|
|
11120
11657
|
const build = result.build;
|
|
@@ -11132,8 +11669,8 @@ operatorCommand.command("publish").argument("[folder]", "Operator workspace fold
|
|
|
11132
11669
|
|
|
11133
11670
|
// src/commands/roadmap.ts
|
|
11134
11671
|
init_source();
|
|
11135
|
-
var
|
|
11136
|
-
var
|
|
11672
|
+
var import_node_fs11 = __toESM(require("node:fs"));
|
|
11673
|
+
var import_node_path9 = __toESM(require("node:path"));
|
|
11137
11674
|
var LEGACY_OUTPUT_REMOVAL_VERSION = "0.7.0";
|
|
11138
11675
|
function truncate(value, max) {
|
|
11139
11676
|
const text = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
@@ -11158,7 +11695,7 @@ function briefLine(item) {
|
|
|
11158
11695
|
].filter(Boolean);
|
|
11159
11696
|
return parts.join("\n");
|
|
11160
11697
|
}
|
|
11161
|
-
var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function
|
|
11698
|
+
var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function action28(opts) {
|
|
11162
11699
|
const creds = requireCredentials();
|
|
11163
11700
|
const limit = parseInt(opts.limit, 10);
|
|
11164
11701
|
const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
|
|
@@ -11212,7 +11749,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
|
|
|
11212
11749
|
}
|
|
11213
11750
|
);
|
|
11214
11751
|
});
|
|
11215
|
-
var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function
|
|
11752
|
+
var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function action29(id, opts) {
|
|
11216
11753
|
if (opts.output !== void 0) {
|
|
11217
11754
|
process.stderr.write(
|
|
11218
11755
|
`Warning: --output is deprecated for apex roadmap handoff; use --out instead. --output will be removed in v${LEGACY_OUTPUT_REMOVAL_VERSION}.
|
|
@@ -11238,9 +11775,9 @@ var handoff = new Command("handoff").description("Get the full build brief for a
|
|
|
11238
11775
|
`;
|
|
11239
11776
|
const outputData = opts.instructions ? { roadmapItemId: id, instructions: String(payload.instructions ?? "") } : payload;
|
|
11240
11777
|
if (opts.out || opts.output) {
|
|
11241
|
-
const target =
|
|
11242
|
-
|
|
11243
|
-
|
|
11778
|
+
const target = import_node_path9.default.resolve(process.cwd(), opts.out ?? opts.output);
|
|
11779
|
+
import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(target), { recursive: true });
|
|
11780
|
+
import_node_fs11.default.writeFileSync(target, rendered, "utf8");
|
|
11244
11781
|
output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
|
|
11245
11782
|
return;
|
|
11246
11783
|
}
|
|
@@ -11254,8 +11791,8 @@ roadmapCommand.addCommand(handoff);
|
|
|
11254
11791
|
|
|
11255
11792
|
// src/commands/tracking.ts
|
|
11256
11793
|
init_source();
|
|
11257
|
-
var
|
|
11258
|
-
var
|
|
11794
|
+
var import_node_fs12 = __toESM(require("node:fs"));
|
|
11795
|
+
var import_node_path10 = __toESM(require("node:path"));
|
|
11259
11796
|
function normalizeUrl(input) {
|
|
11260
11797
|
const value = input.trim();
|
|
11261
11798
|
if (!value) throw new Error("URL cannot be empty");
|
|
@@ -11301,17 +11838,17 @@ function commonCookieDomain(hostnames) {
|
|
|
11301
11838
|
return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
|
|
11302
11839
|
}
|
|
11303
11840
|
function defaultManifestPath() {
|
|
11304
|
-
return
|
|
11841
|
+
return import_node_path10.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
|
|
11305
11842
|
}
|
|
11306
11843
|
function readManifest(filePath) {
|
|
11307
|
-
if (!
|
|
11308
|
-
const parsed = JSON.parse(
|
|
11844
|
+
if (!import_node_fs12.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
|
|
11845
|
+
const parsed = JSON.parse(import_node_fs12.default.readFileSync(filePath, "utf8"));
|
|
11309
11846
|
if (parsed.schemaVersion !== 1) throw new Error("Unsupported tracking manifest schema");
|
|
11310
11847
|
return parsed;
|
|
11311
11848
|
}
|
|
11312
11849
|
function writeManifest(filePath, manifest) {
|
|
11313
|
-
|
|
11314
|
-
|
|
11850
|
+
import_node_fs12.default.mkdirSync(import_node_path10.default.dirname(filePath), { recursive: true });
|
|
11851
|
+
import_node_fs12.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
|
|
11315
11852
|
`, "utf8");
|
|
11316
11853
|
}
|
|
11317
11854
|
function buildSnippet(shopId, endpoint) {
|
|
@@ -11378,7 +11915,7 @@ function buildManifest(urlArgs, opts) {
|
|
|
11378
11915
|
};
|
|
11379
11916
|
}
|
|
11380
11917
|
function manifestPath(input) {
|
|
11381
|
-
return
|
|
11918
|
+
return import_node_path10.default.resolve(process.cwd(), input ?? defaultManifestPath());
|
|
11382
11919
|
}
|
|
11383
11920
|
function printManifestPlan(manifest) {
|
|
11384
11921
|
console.log(source_default.bold("Apex tracking ingestion plan"));
|
|
@@ -11431,7 +11968,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
|
|
|
11431
11968
|
console.log(`${index + 1}. ${check}`);
|
|
11432
11969
|
});
|
|
11433
11970
|
});
|
|
11434
|
-
trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function
|
|
11971
|
+
trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function action30(opts) {
|
|
11435
11972
|
const creds = requireCredentials();
|
|
11436
11973
|
const res = await verifyTracking(creds, {
|
|
11437
11974
|
minutes: Number.parseInt(opts.minutes, 10),
|
|
@@ -11485,8 +12022,8 @@ trackingCommand.command("verify").description("Verify live tracking events for t
|
|
|
11485
12022
|
|
|
11486
12023
|
// src/commands/goals.ts
|
|
11487
12024
|
init_source();
|
|
11488
|
-
var
|
|
11489
|
-
var
|
|
12025
|
+
var import_node_fs13 = __toESM(require("node:fs"));
|
|
12026
|
+
var import_node_path11 = __toESM(require("node:path"));
|
|
11490
12027
|
|
|
11491
12028
|
// ../sdk/src/safe-regex.ts
|
|
11492
12029
|
var MAX_PATTERN_LENGTH = 1024;
|
|
@@ -11662,7 +12199,7 @@ function printSuggestions(data) {
|
|
|
11662
12199
|
function parseCapture(command, value) {
|
|
11663
12200
|
if (!value) return void 0;
|
|
11664
12201
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11665
|
-
const text = value.startsWith("@") ||
|
|
12202
|
+
const text = value.startsWith("@") || import_node_fs13.default.existsSync(source) ? import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), source), "utf8") : value;
|
|
11666
12203
|
try {
|
|
11667
12204
|
return JSON.parse(text);
|
|
11668
12205
|
} catch (error) {
|
|
@@ -11688,7 +12225,7 @@ function printGoal(goal) {
|
|
|
11688
12225
|
console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
|
|
11689
12226
|
console.log();
|
|
11690
12227
|
}
|
|
11691
|
-
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function
|
|
12228
|
+
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
|
|
11692
12229
|
const creds = requireCredentials();
|
|
11693
12230
|
const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
|
|
11694
12231
|
if (!res.ok || !res.data) {
|
|
@@ -11712,7 +12249,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
|
|
|
11712
12249
|
);
|
|
11713
12250
|
});
|
|
11714
12251
|
});
|
|
11715
|
-
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function
|
|
12252
|
+
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action32(opts) {
|
|
11716
12253
|
const creds = requireCredentials();
|
|
11717
12254
|
const body5 = cleanBody({
|
|
11718
12255
|
name: opts.name,
|
|
@@ -11741,7 +12278,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
|
|
|
11741
12278
|
var update = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").option(
|
|
11742
12279
|
"--value-property <property>",
|
|
11743
12280
|
"Numeric event property for value goals; pass null to clear"
|
|
11744
|
-
).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function
|
|
12281
|
+
).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action33(id, opts) {
|
|
11745
12282
|
const creds = requireCredentials();
|
|
11746
12283
|
const body5 = cleanBody({
|
|
11747
12284
|
name: opts.name,
|
|
@@ -11773,7 +12310,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
|
|
|
11773
12310
|
var test = new Command("test").description("Dry run a goal URL pattern against traffic").option(
|
|
11774
12311
|
"--pattern <regex>",
|
|
11775
12312
|
"URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)"
|
|
11776
|
-
).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function
|
|
12313
|
+
).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action34(opts) {
|
|
11777
12314
|
if (!opts.pattern && !opts.goal)
|
|
11778
12315
|
fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
|
|
11779
12316
|
if (opts.pattern && opts.goal)
|
|
@@ -11855,7 +12392,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
|
|
|
11855
12392
|
}
|
|
11856
12393
|
);
|
|
11857
12394
|
});
|
|
11858
|
-
var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function
|
|
12395
|
+
var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function action35(opts) {
|
|
11859
12396
|
const creds = requireCredentials();
|
|
11860
12397
|
const res = await suggestGoals(creds);
|
|
11861
12398
|
if (!res.ok || !res.data)
|
|
@@ -11915,12 +12452,12 @@ Examples:
|
|
|
11915
12452
|
|
|
11916
12453
|
// src/commands/funnels.ts
|
|
11917
12454
|
init_source();
|
|
11918
|
-
var
|
|
11919
|
-
var
|
|
12455
|
+
var import_node_fs14 = __toESM(require("node:fs"));
|
|
12456
|
+
var import_node_path12 = __toESM(require("node:path"));
|
|
11920
12457
|
function readSteps(command, file) {
|
|
11921
12458
|
let parsed;
|
|
11922
12459
|
try {
|
|
11923
|
-
parsed = JSON.parse(
|
|
12460
|
+
parsed = JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(process.cwd(), file), "utf8"));
|
|
11924
12461
|
} catch (error) {
|
|
11925
12462
|
fail(
|
|
11926
12463
|
command,
|
|
@@ -11945,7 +12482,7 @@ function printFunnel(funnel) {
|
|
|
11945
12482
|
console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
|
|
11946
12483
|
console.log();
|
|
11947
12484
|
}
|
|
11948
|
-
var list3 = new Command("list").alias("ls").description("List funnels").action(async function
|
|
12485
|
+
var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
|
|
11949
12486
|
const res = await listFunnels(requireCredentials());
|
|
11950
12487
|
if (!res.ok || !res.data)
|
|
11951
12488
|
fail(
|
|
@@ -11964,7 +12501,7 @@ var list3 = new Command("list").alias("ls").description("List funnels").action(a
|
|
|
11964
12501
|
)
|
|
11965
12502
|
);
|
|
11966
12503
|
});
|
|
11967
|
-
var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function
|
|
12504
|
+
var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action37(opts) {
|
|
11968
12505
|
const res = await createFunnel(requireCredentials(), {
|
|
11969
12506
|
name: opts.name,
|
|
11970
12507
|
steps: readSteps(this, opts.stepsFile)
|
|
@@ -11978,7 +12515,7 @@ var create3 = new Command("create").description("Create a funnel").requiredOptio
|
|
|
11978
12515
|
);
|
|
11979
12516
|
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
11980
12517
|
});
|
|
11981
|
-
var update2 = new Command("update").description("Replace a funnel's steps").argument("<id>", "Funnel ID").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function
|
|
12518
|
+
var update2 = new Command("update").description("Replace a funnel's steps").argument("<id>", "Funnel ID").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action38(id, opts) {
|
|
11982
12519
|
const res = await updateFunnel(requireCredentials(), id, {
|
|
11983
12520
|
steps: readSteps(this, opts.stepsFile)
|
|
11984
12521
|
});
|
|
@@ -11991,7 +12528,7 @@ var update2 = new Command("update").description("Replace a funnel's steps").argu
|
|
|
11991
12528
|
);
|
|
11992
12529
|
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
11993
12530
|
});
|
|
11994
|
-
var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function
|
|
12531
|
+
var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
|
|
11995
12532
|
const res = await deleteFunnel(requireCredentials(), id);
|
|
11996
12533
|
if (!res.ok)
|
|
11997
12534
|
fail(
|
|
@@ -12002,7 +12539,7 @@ var del2 = new Command("delete").description("Delete a funnel").argument("<id>",
|
|
|
12002
12539
|
);
|
|
12003
12540
|
output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
|
|
12004
12541
|
});
|
|
12005
|
-
var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function
|
|
12542
|
+
var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action40(id, opts) {
|
|
12006
12543
|
if (!/^[1-9]\d*$/.test(opts.days))
|
|
12007
12544
|
fail(this, "--days must be a positive integer", EXIT_CODES.validation);
|
|
12008
12545
|
const days = Number.parseInt(opts.days, 10);
|
|
@@ -12026,7 +12563,7 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
|
|
|
12026
12563
|
)
|
|
12027
12564
|
);
|
|
12028
12565
|
});
|
|
12029
|
-
var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function
|
|
12566
|
+
var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action41(opts) {
|
|
12030
12567
|
const steps = readSteps(this, opts.stepsFile);
|
|
12031
12568
|
output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
|
|
12032
12569
|
});
|
|
@@ -12087,11 +12624,11 @@ async function load2(command) {
|
|
|
12087
12624
|
}
|
|
12088
12625
|
return result.data;
|
|
12089
12626
|
}
|
|
12090
|
-
var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function
|
|
12627
|
+
var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
|
|
12091
12628
|
const { environments } = await load2(this);
|
|
12092
12629
|
output(this, { environments }, () => printEnvironments(environments.items));
|
|
12093
12630
|
});
|
|
12094
|
-
var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function
|
|
12631
|
+
var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function action43(domain, opts) {
|
|
12095
12632
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
12096
12633
|
if (!normalizedDomain) {
|
|
12097
12634
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -12138,7 +12675,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
|
|
|
12138
12675
|
() => printEnvironments(result.data.environments.items)
|
|
12139
12676
|
);
|
|
12140
12677
|
});
|
|
12141
|
-
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function
|
|
12678
|
+
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
|
|
12142
12679
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
12143
12680
|
if (!normalizedDomain) {
|
|
12144
12681
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -12202,11 +12739,11 @@ function printConsent(data) {
|
|
|
12202
12739
|
`Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`
|
|
12203
12740
|
);
|
|
12204
12741
|
}
|
|
12205
|
-
var get2 = new Command("get").description("Show consent settings").action(async function
|
|
12742
|
+
var get2 = new Command("get").description("Show consent settings").action(async function action45() {
|
|
12206
12743
|
const data = await load3(this);
|
|
12207
12744
|
output(this, data, () => printConsent(data));
|
|
12208
12745
|
});
|
|
12209
|
-
var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function
|
|
12746
|
+
var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function action46(opts) {
|
|
12210
12747
|
const consentMode = {
|
|
12211
12748
|
enabled: true,
|
|
12212
12749
|
provider: opts.provider,
|
|
@@ -12227,7 +12764,7 @@ var set = new Command("set").description("Enable and configure consent gating").
|
|
|
12227
12764
|
warn(this, result.data.precompile);
|
|
12228
12765
|
output(this, result.data, () => printConsent(result.data));
|
|
12229
12766
|
});
|
|
12230
|
-
var off = new Command("off").description("Disable consent gating").action(async function
|
|
12767
|
+
var off = new Command("off").description("Disable consent gating").action(async function action47() {
|
|
12231
12768
|
const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
|
|
12232
12769
|
if (!result.ok || !result.data)
|
|
12233
12770
|
fail(
|
|
@@ -12242,22 +12779,34 @@ var off = new Command("off").description("Disable consent gating").action(async
|
|
|
12242
12779
|
var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
|
|
12243
12780
|
|
|
12244
12781
|
// src/commands/triggers.ts
|
|
12245
|
-
var
|
|
12246
|
-
var
|
|
12782
|
+
var import_node_fs15 = __toESM(require("node:fs"));
|
|
12783
|
+
var import_node_path13 = __toESM(require("node:path"));
|
|
12247
12784
|
|
|
12248
12785
|
// src/trigger-proposal.ts
|
|
12249
|
-
function
|
|
12786
|
+
function runtimePath(path24) {
|
|
12787
|
+
return path24.replaceAll("[]", ".0");
|
|
12788
|
+
}
|
|
12789
|
+
function fieldsFrom(value, prefix = "", descriptorList = false) {
|
|
12250
12790
|
if (!value || typeof value !== "object") return [];
|
|
12251
|
-
if (Array.isArray(value))
|
|
12791
|
+
if (Array.isArray(value)) {
|
|
12792
|
+
return value.flatMap(
|
|
12793
|
+
(item, index) => fieldsFrom(
|
|
12794
|
+
item,
|
|
12795
|
+
descriptorList ? prefix : prefix ? `${prefix}.${index}` : String(index),
|
|
12796
|
+
descriptorList
|
|
12797
|
+
)
|
|
12798
|
+
);
|
|
12799
|
+
}
|
|
12252
12800
|
const record2 = value;
|
|
12253
12801
|
const explicitPath = typeof record2.path === "string" ? record2.path : void 0;
|
|
12254
|
-
if (explicitPath) {
|
|
12802
|
+
if (descriptorList && explicitPath) {
|
|
12255
12803
|
const rawValues = record2.values ?? record2.samples ?? record2.examples ?? record2.example;
|
|
12256
12804
|
const values = Array.isArray(rawValues) ? rawValues : rawValues === void 0 ? [] : [rawValues];
|
|
12257
12805
|
const magnitude = [record2.mean, record2.average, record2.max, record2.magnitude].find(
|
|
12258
12806
|
(candidate) => typeof candidate === "number"
|
|
12259
12807
|
);
|
|
12260
|
-
|
|
12808
|
+
const numericType = Array.isArray(record2.types) && record2.types.length > 0 && record2.types.every((type) => type === "num" || type === "numstr" || type === "numstr_comma");
|
|
12809
|
+
return [{ path: runtimePath(explicitPath), values, magnitude, numericType }];
|
|
12261
12810
|
}
|
|
12262
12811
|
return Object.entries(record2).flatMap(([key, child]) => {
|
|
12263
12812
|
if (["eventName", "event_name", "name", "count", "frequency", "evidence"].includes(key))
|
|
@@ -12275,6 +12824,27 @@ function fieldsFrom(value, prefix = "") {
|
|
|
12275
12824
|
return fieldsFrom(child, next);
|
|
12276
12825
|
});
|
|
12277
12826
|
}
|
|
12827
|
+
function isDescriptorArray(value) {
|
|
12828
|
+
const descriptorKeys = /* @__PURE__ */ new Set([
|
|
12829
|
+
"path",
|
|
12830
|
+
"values",
|
|
12831
|
+
"samples",
|
|
12832
|
+
"examples",
|
|
12833
|
+
"example",
|
|
12834
|
+
"mean",
|
|
12835
|
+
"average",
|
|
12836
|
+
"max",
|
|
12837
|
+
"magnitude",
|
|
12838
|
+
"types",
|
|
12839
|
+
"seen",
|
|
12840
|
+
"type_collision"
|
|
12841
|
+
]);
|
|
12842
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => {
|
|
12843
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
12844
|
+
const record2 = item;
|
|
12845
|
+
return typeof record2.path === "string" && record2.path.length > 0 && Object.keys(record2).every((key) => descriptorKeys.has(key));
|
|
12846
|
+
});
|
|
12847
|
+
}
|
|
12278
12848
|
function candidatesFrom(report) {
|
|
12279
12849
|
if (!report || typeof report !== "object") return [];
|
|
12280
12850
|
const root = report;
|
|
@@ -12292,15 +12862,15 @@ function candidatesFrom(report) {
|
|
|
12292
12862
|
}
|
|
12293
12863
|
return [root];
|
|
12294
12864
|
}
|
|
12295
|
-
function score2(
|
|
12865
|
+
function score2(path24, patterns) {
|
|
12296
12866
|
return patterns.reduce(
|
|
12297
|
-
(total, pattern, index) => total + (pattern.test(
|
|
12867
|
+
(total, pattern, index) => total + (pattern.test(path24) ? patterns.length - index : 0),
|
|
12298
12868
|
0
|
|
12299
12869
|
);
|
|
12300
12870
|
}
|
|
12301
12871
|
function best(fields, patterns, numeric2 = false) {
|
|
12302
12872
|
const ranked = fields.filter(
|
|
12303
|
-
(field) => !numeric2 || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
|
|
12873
|
+
(field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
|
|
12304
12874
|
).map((field) => ({ field, score: score2(field.path.toLowerCase(), patterns) })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || (b.field.magnitude ?? 0) - (a.field.magnitude ?? 0));
|
|
12305
12875
|
if (!ranked.length) return [];
|
|
12306
12876
|
const top = ranked[0].score;
|
|
@@ -12322,9 +12892,9 @@ function proposeTriggers(report) {
|
|
|
12322
12892
|
eventName
|
|
12323
12893
|
))
|
|
12324
12894
|
continue;
|
|
12325
|
-
const
|
|
12326
|
-
|
|
12327
|
-
);
|
|
12895
|
+
const descriptors = candidate.fields ?? candidate.paths;
|
|
12896
|
+
const payload = candidate.properties ?? candidate.payload ?? candidate;
|
|
12897
|
+
const fields = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
|
|
12328
12898
|
const revenues = best(
|
|
12329
12899
|
fields,
|
|
12330
12900
|
[/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
|
|
@@ -12394,6 +12964,20 @@ function proposeTriggers(report) {
|
|
|
12394
12964
|
return output2;
|
|
12395
12965
|
}
|
|
12396
12966
|
|
|
12967
|
+
// src/generated/runtime-settings-trigger-constants.json
|
|
12968
|
+
var runtime_settings_trigger_constants_default = {
|
|
12969
|
+
dotPathPattern: "^[A-Za-z_$][\\w$]*(?:\\.(?:\\d+|[A-Za-z_$][\\w$]*))*$",
|
|
12970
|
+
signalPathPattern: "^signal:[A-Za-z_$][\\w$]*$",
|
|
12971
|
+
valueTypes: ["string", "number", "boolean", "string_array", "json"],
|
|
12972
|
+
whenOps: ["eq", "neq", "gt", "lt", "exists", "not_exists"],
|
|
12973
|
+
maxWhenEntries: 5,
|
|
12974
|
+
maxSourcePaths: 5,
|
|
12975
|
+
maxTransformMapEntries: 20,
|
|
12976
|
+
maxTransformMapKeyChars: 100,
|
|
12977
|
+
maxTransformMapValueChars: 100,
|
|
12978
|
+
maxTriggers: 20
|
|
12979
|
+
};
|
|
12980
|
+
|
|
12397
12981
|
// src/commands/triggers.ts
|
|
12398
12982
|
var allowed = /* @__PURE__ */ new Set([
|
|
12399
12983
|
"eventName",
|
|
@@ -12402,11 +12986,15 @@ var allowed = /* @__PURE__ */ new Set([
|
|
|
12402
12986
|
"reEvaluate",
|
|
12403
12987
|
"trackPageview",
|
|
12404
12988
|
"trackEventName",
|
|
12989
|
+
"when",
|
|
12405
12990
|
"trackRevenue",
|
|
12406
|
-
"mappings"
|
|
12991
|
+
"mappings",
|
|
12992
|
+
"properties"
|
|
12407
12993
|
]);
|
|
12408
|
-
var pathPattern =
|
|
12409
|
-
var
|
|
12994
|
+
var pathPattern = new RegExp(runtime_settings_trigger_constants_default.dotPathPattern);
|
|
12995
|
+
var signalPathPattern = new RegExp(runtime_settings_trigger_constants_default.signalPathPattern);
|
|
12996
|
+
var valueTypes = new Set(runtime_settings_trigger_constants_default.valueTypes);
|
|
12997
|
+
var whenOps = new Set(runtime_settings_trigger_constants_default.whenOps);
|
|
12410
12998
|
function strictObject(value, location, keys) {
|
|
12411
12999
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12412
13000
|
throw new Error(`${location} must be an object`);
|
|
@@ -12426,26 +13014,126 @@ function strictPath(value, location, required = false) {
|
|
|
12426
13014
|
`${location} must use dot-path syntax with property or array-index segments (for example: ecommerce.items.0.price); bracket syntax is not accepted`
|
|
12427
13015
|
);
|
|
12428
13016
|
}
|
|
13017
|
+
function strictSource(value, location, allowSignal, required = false) {
|
|
13018
|
+
if (value === void 0 && !required) return;
|
|
13019
|
+
const sources = Array.isArray(value) ? value : [value];
|
|
13020
|
+
if (Array.isArray(value) && (value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxSourcePaths))
|
|
13021
|
+
throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxSourcePaths} paths`);
|
|
13022
|
+
sources.forEach((source, index) => {
|
|
13023
|
+
const sourceLocation = Array.isArray(value) ? `${location}[${index}]` : location;
|
|
13024
|
+
if (allowSignal && typeof source === "string" && signalPathPattern.test(source)) return;
|
|
13025
|
+
strictPath(source, sourceLocation, true);
|
|
13026
|
+
});
|
|
13027
|
+
}
|
|
13028
|
+
function defaultMatchesValueType(value, valueType) {
|
|
13029
|
+
if (valueType === "number") {
|
|
13030
|
+
if (typeof value !== "string") return Number.isFinite(Number(value));
|
|
13031
|
+
const text = String(value).trim();
|
|
13032
|
+
const normalized = /^-?\d{1,3}(\.\d{3})+,\d+$/.test(text) ? text.replace(/\./g, "").replace(",", ".") : /^-?\d+,\d+$/.test(text) ? text.replace(",", ".") : text;
|
|
13033
|
+
return Number.isFinite(Number(normalized));
|
|
13034
|
+
}
|
|
13035
|
+
if (valueType === "boolean") {
|
|
13036
|
+
if (typeof value === "boolean") return true;
|
|
13037
|
+
return ["1", "true", "yes", "on", "0", "false", "no", "off"].includes(
|
|
13038
|
+
String(value).trim().toLowerCase()
|
|
13039
|
+
);
|
|
13040
|
+
}
|
|
13041
|
+
return true;
|
|
13042
|
+
}
|
|
13043
|
+
function strictTransform(value, valueType, location) {
|
|
13044
|
+
if (value === void 0) return;
|
|
13045
|
+
const transform = strictObject(
|
|
13046
|
+
value,
|
|
13047
|
+
location,
|
|
13048
|
+
/* @__PURE__ */ new Set(["trim", "lowercase", "map", "scale", "default"])
|
|
13049
|
+
);
|
|
13050
|
+
optionalType(transform.trim, "boolean", `${location}.trim`);
|
|
13051
|
+
optionalType(transform.lowercase, "boolean", `${location}.lowercase`);
|
|
13052
|
+
if (transform.map !== void 0) {
|
|
13053
|
+
if (!transform.map || typeof transform.map !== "object" || Array.isArray(transform.map))
|
|
13054
|
+
throw new Error(`${location}.map must be an object`);
|
|
13055
|
+
const map = transform.map;
|
|
13056
|
+
const entries = Object.entries(map);
|
|
13057
|
+
if (entries.length > runtime_settings_trigger_constants_default.maxTransformMapEntries)
|
|
13058
|
+
throw new Error(
|
|
13059
|
+
`${location}.map must contain at most ${runtime_settings_trigger_constants_default.maxTransformMapEntries} entries`
|
|
13060
|
+
);
|
|
13061
|
+
for (const [key, mappedValue] of entries) {
|
|
13062
|
+
if (key.length > runtime_settings_trigger_constants_default.maxTransformMapKeyChars)
|
|
13063
|
+
throw new Error(
|
|
13064
|
+
`${location}.map keys must be at most ${runtime_settings_trigger_constants_default.maxTransformMapKeyChars} characters`
|
|
13065
|
+
);
|
|
13066
|
+
if (typeof mappedValue !== "string" || mappedValue.length > runtime_settings_trigger_constants_default.maxTransformMapValueChars)
|
|
13067
|
+
throw new Error(
|
|
13068
|
+
`${location}.map.${key} must be a string of at most ${runtime_settings_trigger_constants_default.maxTransformMapValueChars} characters`
|
|
13069
|
+
);
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
13072
|
+
if (transform.scale !== void 0 && (typeof transform.scale !== "number" || !Number.isFinite(transform.scale) || transform.scale === 0))
|
|
13073
|
+
throw new Error(`${location}.scale must be finite and non-zero`);
|
|
13074
|
+
if (transform.default !== void 0 && !["string", "number", "boolean"].includes(typeof transform.default))
|
|
13075
|
+
throw new Error(`${location}.default must be a string, number, or boolean`);
|
|
13076
|
+
if (transform.default !== void 0 && !defaultMatchesValueType(transform.default, valueType))
|
|
13077
|
+
throw new Error(`${location}.default must coerce to the mapping valueType`);
|
|
13078
|
+
}
|
|
12429
13079
|
function strictMapping(value, location, revenueProperty) {
|
|
12430
13080
|
const keys = /* @__PURE__ */ new Set([
|
|
12431
13081
|
"from",
|
|
12432
13082
|
"to",
|
|
12433
13083
|
"valueType",
|
|
13084
|
+
"transform",
|
|
12434
13085
|
...revenueProperty ? [] : ["ttlDays", "persist"]
|
|
12435
13086
|
]);
|
|
12436
13087
|
const mapping = strictObject(value, location, keys);
|
|
12437
|
-
|
|
13088
|
+
strictSource(mapping.from, `${location}.from`, revenueProperty, true);
|
|
12438
13089
|
strictPath(mapping.to, `${location}.to`, true);
|
|
12439
13090
|
if (mapping.valueType !== void 0 && !valueTypes.has(String(mapping.valueType)))
|
|
12440
13091
|
throw new Error(`${location}.valueType must be string, number, boolean, string_array, or json`);
|
|
13092
|
+
strictTransform(mapping.transform, mapping.valueType, `${location}.transform`);
|
|
12441
13093
|
if (!revenueProperty) {
|
|
12442
13094
|
optionalType(mapping.ttlDays, "number", `${location}.ttlDays`);
|
|
12443
13095
|
optionalType(mapping.persist, "boolean", `${location}.persist`);
|
|
12444
13096
|
}
|
|
12445
13097
|
}
|
|
13098
|
+
function strictWhen(value, location) {
|
|
13099
|
+
if (value === void 0) return;
|
|
13100
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxWhenEntries) {
|
|
13101
|
+
throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxWhenEntries} entries`);
|
|
13102
|
+
}
|
|
13103
|
+
value.forEach((entry, index) => {
|
|
13104
|
+
const entryLocation = `${location}[${index}]`;
|
|
13105
|
+
const condition = strictObject(entry, entryLocation, /* @__PURE__ */ new Set(["path", "op", "value"]));
|
|
13106
|
+
strictPath(condition.path, `${entryLocation}.path`, true);
|
|
13107
|
+
if (typeof condition.op !== "string" || !whenOps.has(condition.op)) {
|
|
13108
|
+
throw new Error(
|
|
13109
|
+
`${entryLocation}.op must be ${runtime_settings_trigger_constants_default.whenOps.slice(0, -1).join(", ")}, or ${runtime_settings_trigger_constants_default.whenOps.at(-1)}`
|
|
13110
|
+
);
|
|
13111
|
+
}
|
|
13112
|
+
const hasValue = Object.prototype.hasOwnProperty.call(condition, "value");
|
|
13113
|
+
if (condition.op === "exists" || condition.op === "not_exists") {
|
|
13114
|
+
if (hasValue) throw new Error(`${entryLocation}.value is forbidden for ${condition.op}`);
|
|
13115
|
+
return;
|
|
13116
|
+
}
|
|
13117
|
+
if (!hasValue) throw new Error(`${entryLocation}.value is required for ${condition.op}`);
|
|
13118
|
+
if (condition.op === "gt" || condition.op === "lt") {
|
|
13119
|
+
if (typeof condition.value !== "number" || !Number.isFinite(condition.value)) {
|
|
13120
|
+
throw new Error(`${entryLocation}.value must be a number for ${condition.op}`);
|
|
13121
|
+
}
|
|
13122
|
+
return;
|
|
13123
|
+
}
|
|
13124
|
+
if (!["string", "number", "boolean"].includes(typeof condition.value)) {
|
|
13125
|
+
throw new Error(`${entryLocation}.value must be a string, number, or boolean`);
|
|
13126
|
+
}
|
|
13127
|
+
if (typeof condition.value === "number" && !Number.isFinite(condition.value)) {
|
|
13128
|
+
throw new Error(`${entryLocation}.value must be finite`);
|
|
13129
|
+
}
|
|
13130
|
+
});
|
|
13131
|
+
}
|
|
12446
13132
|
function strictTriggers(value) {
|
|
12447
|
-
if (!Array.isArray(value) || value.length >
|
|
12448
|
-
throw new Error(
|
|
13133
|
+
if (!Array.isArray(value) || value.length > runtime_settings_trigger_constants_default.maxTriggers)
|
|
13134
|
+
throw new Error(
|
|
13135
|
+
`trigger file must contain an array of at most ${runtime_settings_trigger_constants_default.maxTriggers} triggers`
|
|
13136
|
+
);
|
|
12449
13137
|
return value.map((item, index) => {
|
|
12450
13138
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
12451
13139
|
throw new Error(`trigger[${index}] must be an object`);
|
|
@@ -12458,6 +13146,7 @@ function strictTriggers(value) {
|
|
|
12458
13146
|
optionalType(record2[key], "boolean", `trigger[${index}].${key}`);
|
|
12459
13147
|
for (const key of ["dataLayerName", "trackEventName"])
|
|
12460
13148
|
optionalType(record2[key], "string", `trigger[${index}].${key}`);
|
|
13149
|
+
strictWhen(record2.when, `trigger[${index}].when`);
|
|
12461
13150
|
if (record2.trackRevenue !== void 0) {
|
|
12462
13151
|
const revenue = strictObject(
|
|
12463
13152
|
record2.trackRevenue,
|
|
@@ -12474,7 +13163,7 @@ function strictTriggers(value) {
|
|
|
12474
13163
|
);
|
|
12475
13164
|
optionalType(revenue.enabled, "boolean", `trigger[${index}].trackRevenue.enabled`);
|
|
12476
13165
|
for (const key of ["revenueFrom", "orderIdFrom", "currencyFrom", "costFrom", "profitFrom"])
|
|
12477
|
-
|
|
13166
|
+
strictSource(revenue[key], `trigger[${index}].trackRevenue.${key}`, false);
|
|
12478
13167
|
if (revenue.properties !== void 0) {
|
|
12479
13168
|
if (!Array.isArray(revenue.properties))
|
|
12480
13169
|
throw new Error(`trigger[${index}].trackRevenue.properties must be an array`);
|
|
@@ -12490,9 +13179,24 @@ function strictTriggers(value) {
|
|
|
12490
13179
|
(mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].mappings[${mappingIndex}]`, false)
|
|
12491
13180
|
);
|
|
12492
13181
|
}
|
|
13182
|
+
if (record2.properties !== void 0) {
|
|
13183
|
+
if (!Array.isArray(record2.properties))
|
|
13184
|
+
throw new Error(`trigger[${index}].properties must be an array`);
|
|
13185
|
+
record2.properties.forEach(
|
|
13186
|
+
(mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].properties[${mappingIndex}]`, true)
|
|
13187
|
+
);
|
|
13188
|
+
}
|
|
12493
13189
|
return record2;
|
|
12494
13190
|
});
|
|
12495
13191
|
}
|
|
13192
|
+
function writeWarnings(result) {
|
|
13193
|
+
const warnings = result.warnings;
|
|
13194
|
+
if (!Array.isArray(warnings)) return;
|
|
13195
|
+
for (const warning of warnings) {
|
|
13196
|
+
if (typeof warning === "string") process.stderr.write(`WARNING: ${warning}
|
|
13197
|
+
`);
|
|
13198
|
+
}
|
|
13199
|
+
}
|
|
12496
13200
|
var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12497
13201
|
async function identify(command, requestedShop) {
|
|
12498
13202
|
let credentials = requireCredentials();
|
|
@@ -12569,7 +13273,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
|
|
|
12569
13273
|
var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger array").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(async function(opts) {
|
|
12570
13274
|
let triggers;
|
|
12571
13275
|
try {
|
|
12572
|
-
triggers = strictTriggers(JSON.parse(
|
|
13276
|
+
triggers = strictTriggers(JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.file), "utf8")));
|
|
12573
13277
|
} catch (error) {
|
|
12574
13278
|
fail(
|
|
12575
13279
|
this,
|
|
@@ -12584,20 +13288,19 @@ var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger arra
|
|
|
12584
13288
|
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12585
13289
|
...data
|
|
12586
13290
|
};
|
|
12587
|
-
output(
|
|
12588
|
-
|
|
12589
|
-
result,
|
|
12590
|
-
() => process.stdout.write(
|
|
13291
|
+
output(this, result, () => {
|
|
13292
|
+
process.stdout.write(
|
|
12591
13293
|
`TRIGGERS_SET shop_id=${identity.shop_id} domain=${identity.domain} updated_at=${result.updated_at} count=${triggers.length}
|
|
12592
13294
|
`
|
|
12593
|
-
)
|
|
12594
|
-
|
|
13295
|
+
);
|
|
13296
|
+
writeWarnings(result);
|
|
13297
|
+
});
|
|
12595
13298
|
});
|
|
12596
13299
|
var propose = new Command("propose").requiredOption("--from-file <u2-report.json>", "U2 discovery report").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(function(opts) {
|
|
12597
13300
|
let proposals;
|
|
12598
13301
|
let report;
|
|
12599
13302
|
try {
|
|
12600
|
-
report = JSON.parse(
|
|
13303
|
+
report = JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.fromFile), "utf8"));
|
|
12601
13304
|
proposals = proposeTriggers(report);
|
|
12602
13305
|
} catch (error) {
|
|
12603
13306
|
fail(this, error instanceof Error ? error.message : String(error), 2);
|
|
@@ -12656,14 +13359,13 @@ var recipe = new Command("apply-recipe").argument("<recipeId>").requiredOption("
|
|
|
12656
13359
|
domain: identity.domain,
|
|
12657
13360
|
applied_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12658
13361
|
};
|
|
12659
|
-
output(
|
|
12660
|
-
|
|
12661
|
-
result,
|
|
12662
|
-
() => process.stdout.write(
|
|
13362
|
+
output(this, result, () => {
|
|
13363
|
+
process.stdout.write(
|
|
12663
13364
|
`TRIGGERS_APPLIED shop_id=${identity.shop_id} domain=${identity.domain} applied_at=${result.applied_at} recipe=${recipeId}
|
|
12664
13365
|
`
|
|
12665
|
-
)
|
|
12666
|
-
|
|
13366
|
+
);
|
|
13367
|
+
writeWarnings(result);
|
|
13368
|
+
});
|
|
12667
13369
|
});
|
|
12668
13370
|
var enable = new Command("enable-capture").requiredOption("--hours <n>").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").option("--url-pattern <p>", "URL pattern", collect, []).action(async function(opts) {
|
|
12669
13371
|
const hours = Number(opts.hours);
|
|
@@ -12750,11 +13452,11 @@ async function patchWithRetry(command, build) {
|
|
|
12750
13452
|
warn2(command, result.data.precompile);
|
|
12751
13453
|
return result.data;
|
|
12752
13454
|
}
|
|
12753
|
-
var list5 = new Command("list").alias("ls").description("List install domains").action(async function
|
|
13455
|
+
var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
|
|
12754
13456
|
const data = await load4(this);
|
|
12755
13457
|
output(this, data, () => printDomains(data));
|
|
12756
13458
|
});
|
|
12757
|
-
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function
|
|
13459
|
+
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
|
|
12758
13460
|
const added = domain.map(normalizeDomain);
|
|
12759
13461
|
const data = await patchWithRetry(
|
|
12760
13462
|
this,
|
|
@@ -12762,7 +13464,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
|
|
|
12762
13464
|
);
|
|
12763
13465
|
output(this, data, () => printDomains(data));
|
|
12764
13466
|
});
|
|
12765
|
-
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function
|
|
13467
|
+
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
|
|
12766
13468
|
const removeSet = new Set(domain.map(normalizeDomain));
|
|
12767
13469
|
const data = await patchWithRetry(
|
|
12768
13470
|
this,
|
|
@@ -12770,7 +13472,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
|
|
|
12770
13472
|
);
|
|
12771
13473
|
output(this, data, () => printDomains(data));
|
|
12772
13474
|
});
|
|
12773
|
-
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function
|
|
13475
|
+
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
|
|
12774
13476
|
const replacement = domain.map(normalizeDomain);
|
|
12775
13477
|
const data = await patchWithRetry(this, () => replacement);
|
|
12776
13478
|
output(this, data, () => printDomains(data));
|
|
@@ -12862,14 +13564,14 @@ async function patchWithRetry2(command, build) {
|
|
|
12862
13564
|
);
|
|
12863
13565
|
return result.data;
|
|
12864
13566
|
}
|
|
12865
|
-
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function
|
|
13567
|
+
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
|
|
12866
13568
|
const data = await load5(this);
|
|
12867
13569
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12868
13570
|
});
|
|
12869
13571
|
var set4 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option(
|
|
12870
13572
|
"--signed",
|
|
12871
13573
|
"Require signed handover tokens (the default; unsigning is a platform-support action)"
|
|
12872
|
-
).action(async function
|
|
13574
|
+
).action(async function action53(opts) {
|
|
12873
13575
|
const domains = opts.domain.map(normalizeDomain2);
|
|
12874
13576
|
if (domains.some((domain) => !domain))
|
|
12875
13577
|
fail(
|
|
@@ -12889,14 +13591,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
|
|
|
12889
13591
|
}));
|
|
12890
13592
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12891
13593
|
});
|
|
12892
|
-
var off2 = new Command("off").description("Disable cross-domain handover").action(async function
|
|
13594
|
+
var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
|
|
12893
13595
|
const data = await patchWithRetry2(this, (current) => ({
|
|
12894
13596
|
externalTracking: { domains: [], signed: true },
|
|
12895
13597
|
ifRevision: current.revision
|
|
12896
13598
|
}));
|
|
12897
13599
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12898
13600
|
});
|
|
12899
|
-
var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function
|
|
13601
|
+
var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action55(opts) {
|
|
12900
13602
|
const config = await load5(this);
|
|
12901
13603
|
const target = normalizeDomain2(opts.toDomain);
|
|
12902
13604
|
if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
|
|
@@ -13939,7 +14641,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
|
|
|
13939
14641
|
).option("--check <id>", "Run one check (for example purchase_capture)").option(
|
|
13940
14642
|
"--worker <url>",
|
|
13941
14643
|
`Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
|
|
13942
|
-
).action(async function
|
|
14644
|
+
).action(async function action56(options) {
|
|
13943
14645
|
const data = await runDoctor(options);
|
|
13944
14646
|
if (options.check && data.checks.length === 0)
|
|
13945
14647
|
fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
|
|
@@ -13961,7 +14663,7 @@ function printDoctorReport(checks) {
|
|
|
13961
14663
|
var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
|
|
13962
14664
|
"--since <date>",
|
|
13963
14665
|
"Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
|
|
13964
|
-
).action(async function
|
|
14666
|
+
).action(async function action57(opts) {
|
|
13965
14667
|
if (opts.since !== void 0 && parseDay(opts.since) === null)
|
|
13966
14668
|
fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
|
|
13967
14669
|
const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
|
|
@@ -14021,7 +14723,7 @@ function analyticsSubcommand(name, description, defaultDays) {
|
|
|
14021
14723
|
command.option("--planning-days <n>", "Planning window in days", "180");
|
|
14022
14724
|
command.option("--limit <n>", "Maximum page rows", "50");
|
|
14023
14725
|
}
|
|
14024
|
-
return command.action(async function
|
|
14726
|
+
return command.action(async function action71(opts) {
|
|
14025
14727
|
const result = await getAnalytics(requireCredentials(), name, {
|
|
14026
14728
|
days: positiveInteger(this, opts.days, "--days"),
|
|
14027
14729
|
planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
|
|
@@ -14041,7 +14743,7 @@ function urlsCommand() {
|
|
|
14041
14743
|
return new Command("urls").description("Show storefront URL inventory from pageview analytics").option("-d, --days <n>", "Analytics window in days", "14").option("--host <host>", "Filter inventory to one host").option("--contains <substring>", "Server-side path substring filter").option(
|
|
14042
14744
|
"--match <regex>",
|
|
14043
14745
|
"Client-side regex filter; exhausts inventory pages before filtering"
|
|
14044
|
-
).option("--limit <n>", "Maximum rows to print", "50").action(async function
|
|
14746
|
+
).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
|
|
14045
14747
|
const creds = requireCredentials();
|
|
14046
14748
|
const days = positiveInteger(this, opts.days, "--days") ?? 14;
|
|
14047
14749
|
const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
|
|
@@ -14235,10 +14937,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
14235
14937
|
const chunks = [];
|
|
14236
14938
|
let bytes = 0;
|
|
14237
14939
|
let settled = false;
|
|
14238
|
-
const settle = (
|
|
14940
|
+
const settle = (action71) => {
|
|
14239
14941
|
if (settled) return;
|
|
14240
14942
|
settled = true;
|
|
14241
|
-
|
|
14943
|
+
action71();
|
|
14242
14944
|
};
|
|
14243
14945
|
stream.on("data", (chunk) => {
|
|
14244
14946
|
if (settled) return;
|
|
@@ -14309,11 +15011,11 @@ function pinnedFetch(url, addresses, limits) {
|
|
|
14309
15011
|
else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
|
|
14310
15012
|
};
|
|
14311
15013
|
let settled = false;
|
|
14312
|
-
const settle = (
|
|
15014
|
+
const settle = (action71) => {
|
|
14313
15015
|
if (settled) return;
|
|
14314
15016
|
settled = true;
|
|
14315
15017
|
clearTimeout(deadline);
|
|
14316
|
-
|
|
15018
|
+
action71();
|
|
14317
15019
|
};
|
|
14318
15020
|
const req = send3(
|
|
14319
15021
|
url,
|
|
@@ -14648,7 +15350,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
|
|
|
14648
15350
|
var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description("Verify snippet, served config, and recent tracking for the current shop").option("--url <url>", "Check one explicit bound storefront URL").option("--shop-id <id>", "Apex shop ID to verify (falls back to DRIP_SHOP_ID)").option(
|
|
14649
15351
|
"--worker-url <url>",
|
|
14650
15352
|
"Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
|
|
14651
|
-
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function
|
|
15353
|
+
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
|
|
14652
15354
|
const creds = requireCredentials();
|
|
14653
15355
|
const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
|
|
14654
15356
|
if (!shopId)
|
|
@@ -14792,8 +15494,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
|
|
|
14792
15494
|
|
|
14793
15495
|
// src/commands/flags.ts
|
|
14794
15496
|
init_source();
|
|
14795
|
-
var
|
|
14796
|
-
var
|
|
15497
|
+
var import_node_fs16 = __toESM(require("node:fs"));
|
|
15498
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
14797
15499
|
|
|
14798
15500
|
// src/edge-status.ts
|
|
14799
15501
|
function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
|
|
@@ -14835,7 +15537,7 @@ function body(command, value, opts) {
|
|
|
14835
15537
|
let parsed = {};
|
|
14836
15538
|
if (value) {
|
|
14837
15539
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
14838
|
-
const text = value.startsWith("@") ||
|
|
15540
|
+
const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
|
|
14839
15541
|
try {
|
|
14840
15542
|
parsed = JSON.parse(text);
|
|
14841
15543
|
} catch (e) {
|
|
@@ -14948,11 +15650,11 @@ function write(name) {
|
|
|
14948
15650
|
});
|
|
14949
15651
|
return c;
|
|
14950
15652
|
}
|
|
14951
|
-
function lifecycle(
|
|
14952
|
-
return new Command(
|
|
14953
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind, id,
|
|
15653
|
+
function lifecycle(action71) {
|
|
15654
|
+
return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
|
|
15655
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
|
|
14954
15656
|
if (!r.ok || !r.data)
|
|
14955
|
-
fail(this, r.error ?? `Failed to ${
|
|
15657
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
14956
15658
|
output(this, { flag: r.data }, () => detail(r.data));
|
|
14957
15659
|
});
|
|
14958
15660
|
}
|
|
@@ -14968,8 +15670,8 @@ Examples:
|
|
|
14968
15670
|
);
|
|
14969
15671
|
|
|
14970
15672
|
// src/commands/backlog.ts
|
|
14971
|
-
var
|
|
14972
|
-
var
|
|
15673
|
+
var import_node_fs17 = __toESM(require("node:fs"));
|
|
15674
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
14973
15675
|
init_source();
|
|
14974
15676
|
var kind2 = "backlog";
|
|
14975
15677
|
var singular = "backlog item";
|
|
@@ -14977,7 +15679,7 @@ function parse(command, value) {
|
|
|
14977
15679
|
if (!value) return {};
|
|
14978
15680
|
try {
|
|
14979
15681
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
14980
|
-
const text = value.startsWith("@") ||
|
|
15682
|
+
const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
|
|
14981
15683
|
const body5 = JSON.parse(text);
|
|
14982
15684
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
|
|
14983
15685
|
throw new Error("body must be an object");
|
|
@@ -15027,24 +15729,24 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
|
|
|
15027
15729
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
15028
15730
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
15029
15731
|
});
|
|
15030
|
-
function write2(
|
|
15031
|
-
const c = new Command(
|
|
15032
|
-
`${
|
|
15732
|
+
function write2(action71) {
|
|
15733
|
+
const c = new Command(action71).description(
|
|
15734
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular}`
|
|
15033
15735
|
);
|
|
15034
|
-
if (
|
|
15736
|
+
if (action71 === "update") c.argument("<id>", `${singular} ID`);
|
|
15035
15737
|
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
15036
15738
|
"--file <path>",
|
|
15037
15739
|
"JSON body file"
|
|
15038
15740
|
);
|
|
15039
15741
|
c.action(async function(...args) {
|
|
15040
15742
|
const o = this.opts();
|
|
15041
|
-
const id =
|
|
15042
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
15743
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
15744
|
+
const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15043
15745
|
const body5 = parse(this, raw);
|
|
15044
15746
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
15045
|
-
const r =
|
|
15747
|
+
const r = action71 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
|
|
15046
15748
|
if (!r.ok || !r.data)
|
|
15047
|
-
fail(this, r.error ?? `Failed to ${
|
|
15749
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15048
15750
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
15049
15751
|
});
|
|
15050
15752
|
return c;
|
|
@@ -15093,8 +15795,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
|
|
|
15093
15795
|
var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
|
|
15094
15796
|
|
|
15095
15797
|
// src/commands/signals.ts
|
|
15096
|
-
var
|
|
15097
|
-
var
|
|
15798
|
+
var import_node_fs18 = __toESM(require("node:fs"));
|
|
15799
|
+
var import_node_path16 = __toESM(require("node:path"));
|
|
15098
15800
|
init_source();
|
|
15099
15801
|
var kind3 = "signals";
|
|
15100
15802
|
var singular2 = "signal";
|
|
@@ -15102,7 +15804,7 @@ function parse2(command, value) {
|
|
|
15102
15804
|
if (!value) return {};
|
|
15103
15805
|
try {
|
|
15104
15806
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15105
|
-
const text = value.startsWith("@") ||
|
|
15807
|
+
const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
|
|
15106
15808
|
const body5 = JSON.parse(text);
|
|
15107
15809
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
|
|
15108
15810
|
throw new Error("body must be an object");
|
|
@@ -15152,24 +15854,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
|
|
|
15152
15854
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
15153
15855
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
15154
15856
|
});
|
|
15155
|
-
function write3(
|
|
15156
|
-
const c = new Command(
|
|
15157
|
-
`${
|
|
15857
|
+
function write3(action71) {
|
|
15858
|
+
const c = new Command(action71).description(
|
|
15859
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular2}`
|
|
15158
15860
|
);
|
|
15159
|
-
if (
|
|
15861
|
+
if (action71 === "update") c.argument("<id>", `${singular2} ID`);
|
|
15160
15862
|
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
15161
15863
|
"--file <path>",
|
|
15162
15864
|
"JSON body file"
|
|
15163
15865
|
);
|
|
15164
15866
|
c.action(async function(...args) {
|
|
15165
15867
|
const o = this.opts();
|
|
15166
|
-
const id =
|
|
15167
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
15868
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
15869
|
+
const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15168
15870
|
const body5 = parse2(this, raw);
|
|
15169
15871
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
15170
|
-
const r =
|
|
15872
|
+
const r = action71 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
|
|
15171
15873
|
if (!r.ok || !r.data)
|
|
15172
|
-
fail(this, r.error ?? `Failed to ${
|
|
15874
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15173
15875
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
15174
15876
|
});
|
|
15175
15877
|
return c;
|
|
@@ -15184,14 +15886,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
|
|
|
15184
15886
|
|
|
15185
15887
|
// src/commands/personalizations.ts
|
|
15186
15888
|
init_source();
|
|
15187
|
-
var
|
|
15188
|
-
var
|
|
15889
|
+
var import_node_fs19 = __toESM(require("node:fs"));
|
|
15890
|
+
var import_node_path17 = __toESM(require("node:path"));
|
|
15189
15891
|
var kind4 = "personalizations";
|
|
15190
15892
|
function body2(command, value, opts) {
|
|
15191
15893
|
let parsed = {};
|
|
15192
15894
|
if (value) {
|
|
15193
15895
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15194
|
-
const text = value.startsWith("@") ||
|
|
15896
|
+
const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
|
|
15195
15897
|
try {
|
|
15196
15898
|
parsed = JSON.parse(text);
|
|
15197
15899
|
} catch (e) {
|
|
@@ -15261,11 +15963,11 @@ function write4(name) {
|
|
|
15261
15963
|
});
|
|
15262
15964
|
return c;
|
|
15263
15965
|
}
|
|
15264
|
-
function lifecycle2(
|
|
15265
|
-
return new Command(
|
|
15266
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind4, id,
|
|
15966
|
+
function lifecycle2(action71) {
|
|
15967
|
+
return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
|
|
15968
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
|
|
15267
15969
|
if (!r.ok || !r.data)
|
|
15268
|
-
fail(this, r.error ?? `Failed to ${
|
|
15970
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15269
15971
|
output(this, { personalization: r.data }, () => detail4(r.data));
|
|
15270
15972
|
});
|
|
15271
15973
|
}
|
|
@@ -15273,13 +15975,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
|
|
|
15273
15975
|
|
|
15274
15976
|
// src/commands/targeting-resources.ts
|
|
15275
15977
|
init_source();
|
|
15276
|
-
var
|
|
15277
|
-
var
|
|
15978
|
+
var import_node_fs20 = __toESM(require("node:fs"));
|
|
15979
|
+
var import_node_path18 = __toESM(require("node:path"));
|
|
15278
15980
|
function body3(command, value) {
|
|
15279
15981
|
if (!value) return {};
|
|
15280
15982
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15281
15983
|
try {
|
|
15282
|
-
const text = value.startsWith("@") ||
|
|
15984
|
+
const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
|
|
15283
15985
|
return JSON.parse(text);
|
|
15284
15986
|
} catch (error) {
|
|
15285
15987
|
fail(
|
|
@@ -15347,21 +16049,21 @@ function targetingResourceCommand(kind5) {
|
|
|
15347
16049
|
);
|
|
15348
16050
|
})
|
|
15349
16051
|
);
|
|
15350
|
-
for (const
|
|
15351
|
-
const command = new Command(
|
|
15352
|
-
`${
|
|
16052
|
+
for (const action71 of ["create", "update"]) {
|
|
16053
|
+
const command = new Command(action71).description(
|
|
16054
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular3}`
|
|
15353
16055
|
);
|
|
15354
|
-
if (
|
|
16056
|
+
if (action71 === "update") command.argument("<id>");
|
|
15355
16057
|
command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
|
|
15356
16058
|
const opts = this.opts();
|
|
15357
|
-
const id =
|
|
15358
|
-
const value = opts.file ? `@${opts.file}` : String(args[
|
|
16059
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
16060
|
+
const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15359
16061
|
const requestBody = body3(this, value);
|
|
15360
|
-
if (
|
|
16062
|
+
if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
|
|
15361
16063
|
fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
|
|
15362
16064
|
const response = await requestTargetingResource(
|
|
15363
16065
|
requireCredentials(),
|
|
15364
|
-
|
|
16066
|
+
action71 === "create" ? "POST" : "PATCH",
|
|
15365
16067
|
kind5,
|
|
15366
16068
|
id,
|
|
15367
16069
|
requestBody
|
|
@@ -15369,7 +16071,7 @@ function targetingResourceCommand(kind5) {
|
|
|
15369
16071
|
if (!response.ok || !response.data)
|
|
15370
16072
|
fail(
|
|
15371
16073
|
this,
|
|
15372
|
-
response.error ?? `Failed to ${
|
|
16074
|
+
response.error ?? `Failed to ${action71}`,
|
|
15373
16075
|
classifyStatus(response.status),
|
|
15374
16076
|
response.errorDetails
|
|
15375
16077
|
);
|
|
@@ -15378,7 +16080,7 @@ function targetingResourceCommand(kind5) {
|
|
|
15378
16080
|
{
|
|
15379
16081
|
[kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
|
|
15380
16082
|
},
|
|
15381
|
-
() => console.log(source_default.green(`${
|
|
16083
|
+
() => console.log(source_default.green(`${action71}d ${singular3}`))
|
|
15382
16084
|
);
|
|
15383
16085
|
});
|
|
15384
16086
|
root.addCommand(command);
|
|
@@ -15452,7 +16154,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
|
|
|
15452
16154
|
// src/commands/brand.ts
|
|
15453
16155
|
init_source();
|
|
15454
16156
|
var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
|
|
15455
|
-
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function
|
|
16157
|
+
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
|
|
15456
16158
|
const creds = requireCredentials();
|
|
15457
16159
|
const res = await getBrandContext(creds);
|
|
15458
16160
|
if (res.status === 404) {
|
|
@@ -15485,7 +16187,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
|
|
|
15485
16187
|
|
|
15486
16188
|
// src/commands/targeting.ts
|
|
15487
16189
|
init_source();
|
|
15488
|
-
var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function
|
|
16190
|
+
var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action60(url, opts) {
|
|
15489
16191
|
const creds = requireCredentials();
|
|
15490
16192
|
const res = await testTargeting(creds, url, { status: opts.status });
|
|
15491
16193
|
if (!res.ok || !res.data) {
|
|
@@ -15530,13 +16232,21 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
|
|
|
15530
16232
|
|
|
15531
16233
|
// src/commands/sos.ts
|
|
15532
16234
|
init_source();
|
|
15533
|
-
var
|
|
15534
|
-
var
|
|
15535
|
-
var SOS_TYPES = /* @__PURE__ */ new Set([
|
|
16235
|
+
var import_node_fs21 = __toESM(require("node:fs"));
|
|
16236
|
+
var import_node_path19 = __toESM(require("node:path"));
|
|
16237
|
+
var SOS_TYPES = /* @__PURE__ */ new Set([
|
|
16238
|
+
"full_handover",
|
|
16239
|
+
"design",
|
|
16240
|
+
"development",
|
|
16241
|
+
"qa",
|
|
16242
|
+
"config",
|
|
16243
|
+
"bug",
|
|
16244
|
+
"help"
|
|
16245
|
+
]);
|
|
15536
16246
|
function readText(value) {
|
|
15537
16247
|
if (!value) return void 0;
|
|
15538
16248
|
if (!value.startsWith("@")) return value;
|
|
15539
|
-
return
|
|
16249
|
+
return import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), value.slice(1)), "utf8");
|
|
15540
16250
|
}
|
|
15541
16251
|
function printTicket(ticket, reused) {
|
|
15542
16252
|
console.log();
|
|
@@ -15547,11 +16257,26 @@ function printTicket(ticket, reused) {
|
|
|
15547
16257
|
console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
|
|
15548
16258
|
console.log();
|
|
15549
16259
|
}
|
|
15550
|
-
|
|
16260
|
+
function printCreatedTicket(result) {
|
|
16261
|
+
if ("reused" in result) {
|
|
16262
|
+
printTicket(result.ticket, result.reused);
|
|
16263
|
+
return;
|
|
16264
|
+
}
|
|
16265
|
+
console.log();
|
|
16266
|
+
console.log(source_default.bold(result.deduped ? "Reused customer ticket" : "Created customer ticket"));
|
|
16267
|
+
console.log(` Reference: ${source_default.dim(result.ticket.reference)}`);
|
|
16268
|
+
console.log(` Status: ${result.ticket.status}`);
|
|
16269
|
+
console.log();
|
|
16270
|
+
}
|
|
16271
|
+
var create4 = new Command("create").description("Create an Apex SOS ticket").option(
|
|
16272
|
+
"--type <type>",
|
|
16273
|
+
"full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
|
|
16274
|
+
"qa"
|
|
16275
|
+
).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action61(descriptionArg, opts) {
|
|
15551
16276
|
if (!SOS_TYPES.has(opts.type)) {
|
|
15552
16277
|
fail(
|
|
15553
16278
|
this,
|
|
15554
|
-
"Invalid SOS type. Use full_handover, design, development, qa, config, or
|
|
16279
|
+
"Invalid SOS type. Use full_handover, design, development, qa, config, bug, or help. Internal types require a DRIP account.",
|
|
15555
16280
|
3
|
|
15556
16281
|
);
|
|
15557
16282
|
}
|
|
@@ -15572,9 +16297,9 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
15572
16297
|
res.errorDetails
|
|
15573
16298
|
);
|
|
15574
16299
|
}
|
|
15575
|
-
output(this, res.data, () =>
|
|
16300
|
+
output(this, res.data, () => printCreatedTicket(res.data));
|
|
15576
16301
|
});
|
|
15577
|
-
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function
|
|
16302
|
+
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
|
|
15578
16303
|
const creds = requireCredentials();
|
|
15579
16304
|
const res = await listMySos(creds);
|
|
15580
16305
|
if (!res.ok || !res.data) {
|
|
@@ -15655,13 +16380,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
|
|
|
15655
16380
|
var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
|
|
15656
16381
|
|
|
15657
16382
|
// src/commands/shops.ts
|
|
15658
|
-
var
|
|
15659
|
-
var
|
|
16383
|
+
var import_node_fs22 = __toESM(require("node:fs"));
|
|
16384
|
+
var import_node_path20 = __toESM(require("node:path"));
|
|
15660
16385
|
function body4(command, raw) {
|
|
15661
16386
|
if (!raw) return {};
|
|
15662
16387
|
try {
|
|
15663
16388
|
const source = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
15664
|
-
const text = raw.startsWith("@") ||
|
|
16389
|
+
const text = raw.startsWith("@") || import_node_fs22.default.existsSync(source) ? import_node_fs22.default.readFileSync(import_node_path20.default.resolve(source), "utf8") : raw;
|
|
15665
16390
|
const value = JSON.parse(text);
|
|
15666
16391
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15667
16392
|
throw new Error("body must be an object");
|
|
@@ -15889,7 +16614,7 @@ function commandSchema(command) {
|
|
|
15889
16614
|
};
|
|
15890
16615
|
}
|
|
15891
16616
|
function createSchemaCommand(program3) {
|
|
15892
|
-
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function
|
|
16617
|
+
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action71() {
|
|
15893
16618
|
const schema = commandSchema(program3);
|
|
15894
16619
|
output(this, schema, () => {
|
|
15895
16620
|
process.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
@@ -16042,7 +16767,7 @@ function parseContainer(source, id, dataLayerName = "dataLayer") {
|
|
|
16042
16767
|
vtpPaths,
|
|
16043
16768
|
ecommerceFrom: [...ecommerce][0] ?? null,
|
|
16044
16769
|
serverContainerUrl: [...serverUrls][0] ?? null,
|
|
16045
|
-
piiPaths: vtpPaths.filter((
|
|
16770
|
+
piiPaths: vtpPaths.filter((path24) => /email|phone|address|name/i.test(path24))
|
|
16046
16771
|
}
|
|
16047
16772
|
};
|
|
16048
16773
|
}
|
|
@@ -16101,6 +16826,145 @@ function minutesValue(value) {
|
|
|
16101
16826
|
throw new CliError("--minutes must be an integer between 1 and 20160", EXIT_CODES.validation);
|
|
16102
16827
|
return parsed;
|
|
16103
16828
|
}
|
|
16829
|
+
function isStableField(field) {
|
|
16830
|
+
return field.types.length > 0 && !field.type_collision;
|
|
16831
|
+
}
|
|
16832
|
+
function baseFieldTypes(field) {
|
|
16833
|
+
return new Set(field.types.map((type) => type.split(":")[0]));
|
|
16834
|
+
}
|
|
16835
|
+
function isContainerField(field) {
|
|
16836
|
+
const types = baseFieldTypes(field);
|
|
16837
|
+
return types.has("obj") || types.has("arr");
|
|
16838
|
+
}
|
|
16839
|
+
function hasSyntheticPlaceholder(path24) {
|
|
16840
|
+
return path24.split(".").map((segment) => segment.replace(/\[\]$/, "")).some((segment) => /^<[^>]+>$/.test(segment));
|
|
16841
|
+
}
|
|
16842
|
+
function ambiguousDottedPrefix(event, path24) {
|
|
16843
|
+
const segments = path24.split(".");
|
|
16844
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
16845
|
+
const prefix = segments.slice(0, index).join(".");
|
|
16846
|
+
const parent = event.fields.find((field) => field.path === prefix);
|
|
16847
|
+
if (!parent || !isContainerField(parent)) return prefix;
|
|
16848
|
+
}
|
|
16849
|
+
return void 0;
|
|
16850
|
+
}
|
|
16851
|
+
function dimensionEligibility(event, field) {
|
|
16852
|
+
const path24 = field.path;
|
|
16853
|
+
if (hasSyntheticPlaceholder(path24)) {
|
|
16854
|
+
return {
|
|
16855
|
+
eligible: false,
|
|
16856
|
+
message: `Path ${JSON.stringify(path24)} is a synthetic placeholder and cannot create a dimension. Choose an observed scalar path.`
|
|
16857
|
+
};
|
|
16858
|
+
}
|
|
16859
|
+
if (isContainerField(field)) {
|
|
16860
|
+
return {
|
|
16861
|
+
eligible: false,
|
|
16862
|
+
message: `Path ${JSON.stringify(path24)} is container-typed. Choose an observed scalar path.`
|
|
16863
|
+
};
|
|
16864
|
+
}
|
|
16865
|
+
if (path24.includes("[]")) {
|
|
16866
|
+
return {
|
|
16867
|
+
eligible: false,
|
|
16868
|
+
message: `Path ${JSON.stringify(path24)} includes an array-element segment and cannot create a reliable dimension. Choose a scalar path outside an array.`
|
|
16869
|
+
};
|
|
16870
|
+
}
|
|
16871
|
+
if (!isStableField(field)) {
|
|
16872
|
+
return {
|
|
16873
|
+
eligible: false,
|
|
16874
|
+
message: `Path ${JSON.stringify(path24)} does not have a stable observed type. Choose a path without a type collision.`
|
|
16875
|
+
};
|
|
16876
|
+
}
|
|
16877
|
+
if (!event.event) {
|
|
16878
|
+
return {
|
|
16879
|
+
eligible: false,
|
|
16880
|
+
message: `Path ${JSON.stringify(path24)} has no observed event name. Choose a path from a named event.`
|
|
16881
|
+
};
|
|
16882
|
+
}
|
|
16883
|
+
const ambiguousPrefix = ambiguousDottedPrefix(event, path24);
|
|
16884
|
+
if (ambiguousPrefix) {
|
|
16885
|
+
return {
|
|
16886
|
+
eligible: false,
|
|
16887
|
+
message: `Path ${JSON.stringify(path24)} could be a literal key containing dots because the discovery report has no observed container at ${JSON.stringify(ambiguousPrefix)}. Apex cannot safely create a capture for it.`
|
|
16888
|
+
};
|
|
16889
|
+
}
|
|
16890
|
+
return { eligible: true };
|
|
16891
|
+
}
|
|
16892
|
+
function isNumericField(field) {
|
|
16893
|
+
const numericTypes = /* @__PURE__ */ new Set(["num", "numstr", "numstr_comma"]);
|
|
16894
|
+
return field.types.length > 0 && field.types.every((type) => numericTypes.has(type.split(":")[0]));
|
|
16895
|
+
}
|
|
16896
|
+
function defaultDimensionLabel(path24) {
|
|
16897
|
+
const words = path24.split(/[._-]+/).filter(Boolean).join(" ");
|
|
16898
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : path24;
|
|
16899
|
+
}
|
|
16900
|
+
function quoteShellArgument(value) {
|
|
16901
|
+
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
16902
|
+
}
|
|
16903
|
+
function createDimensionInvocation(path24, eventName) {
|
|
16904
|
+
return `apex discover --create-dimension ${quoteShellArgument(path24)} --event ${quoteShellArgument(eventName)}`;
|
|
16905
|
+
}
|
|
16906
|
+
function resolveDimensionObservation(command, report, path24, eventName) {
|
|
16907
|
+
const matches = report.data_layer.flatMap(
|
|
16908
|
+
(event) => event.fields.filter((field) => field.path === path24).map((field) => ({ event, field }))
|
|
16909
|
+
);
|
|
16910
|
+
if (!matches.length) {
|
|
16911
|
+
fail(
|
|
16912
|
+
command,
|
|
16913
|
+
`Path ${JSON.stringify(path24)} was not found in the current discovery report. Run apex discover and copy an observed path exactly.`,
|
|
16914
|
+
EXIT_CODES.validation
|
|
16915
|
+
);
|
|
16916
|
+
}
|
|
16917
|
+
let selected2;
|
|
16918
|
+
if (eventName !== void 0) {
|
|
16919
|
+
const eventMatches = matches.filter((match2) => match2.event.event === eventName);
|
|
16920
|
+
if (!eventMatches.length) {
|
|
16921
|
+
fail(
|
|
16922
|
+
command,
|
|
16923
|
+
`Path ${JSON.stringify(path24)} was not observed on event ${JSON.stringify(eventName)}. Run apex discover and copy the event name exactly.`,
|
|
16924
|
+
EXIT_CODES.validation
|
|
16925
|
+
);
|
|
16926
|
+
}
|
|
16927
|
+
const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
|
|
16928
|
+
if (layers.length > 1) {
|
|
16929
|
+
fail(
|
|
16930
|
+
command,
|
|
16931
|
+
`Path ${JSON.stringify(path24)} on event ${JSON.stringify(eventName)} was observed on multiple data layers: ${layers.join(", ")}. Dimensions bind to one layer; this ambiguity cannot be resolved automatically \u2014 capture the value under one layer, or define the dimension in the dashboard with the data layer name set explicitly.`,
|
|
16932
|
+
EXIT_CODES.validation
|
|
16933
|
+
);
|
|
16934
|
+
}
|
|
16935
|
+
selected2 = eventMatches[0];
|
|
16936
|
+
} else {
|
|
16937
|
+
const namedEvents = [
|
|
16938
|
+
...new Set(matches.map((match2) => match2.event.event).filter(Boolean))
|
|
16939
|
+
].sort();
|
|
16940
|
+
if (namedEvents.length > 1) {
|
|
16941
|
+
fail(
|
|
16942
|
+
command,
|
|
16943
|
+
`Path ${JSON.stringify(path24)} was observed on multiple events: ${namedEvents.join(", ")}. Retry with --event <name>.`,
|
|
16944
|
+
EXIT_CODES.validation
|
|
16945
|
+
);
|
|
16946
|
+
}
|
|
16947
|
+
const eventMatches = namedEvents.length ? matches.filter((match2) => match2.event.event === namedEvents[0]) : matches;
|
|
16948
|
+
const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
|
|
16949
|
+
if (layers.length > 1) {
|
|
16950
|
+
fail(
|
|
16951
|
+
command,
|
|
16952
|
+
`Path ${JSON.stringify(path24)} on event ${JSON.stringify(namedEvents[0] ?? "")} was observed on multiple data layers: ${layers.join(", ")}. Dimensions bind to one layer; this ambiguity cannot be resolved automatically \u2014 capture the value under one layer, or define the dimension in the dashboard with the data layer name set explicitly.`,
|
|
16953
|
+
EXIT_CODES.validation
|
|
16954
|
+
);
|
|
16955
|
+
}
|
|
16956
|
+
selected2 = eventMatches[0];
|
|
16957
|
+
}
|
|
16958
|
+
if (!selected2)
|
|
16959
|
+
fail(command, "No matching dimension observation was selected", EXIT_CODES.validation);
|
|
16960
|
+
const eligibility = dimensionEligibility(selected2.event, selected2.field);
|
|
16961
|
+
if (!eligibility.eligible) fail(command, eligibility.message, EXIT_CODES.validation);
|
|
16962
|
+
return {
|
|
16963
|
+
eventName: selected2.event.event,
|
|
16964
|
+
dlName: selected2.event.dlName,
|
|
16965
|
+
field: selected2.field
|
|
16966
|
+
};
|
|
16967
|
+
}
|
|
16104
16968
|
function renderDiscovery(report) {
|
|
16105
16969
|
const lines = [
|
|
16106
16970
|
`Observed tracking schema (${report.window.minutes} minutes)`,
|
|
@@ -16120,14 +16984,18 @@ function renderDiscovery(report) {
|
|
|
16120
16984
|
`
|
|
16121
16985
|
${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} observation(s)${event.matched ? " \u2014 matched" : ""}; t_ms p50=${event.t_ms_p50 ?? "n/a"}, p95=${event.t_ms_p95 ?? "n/a"}, pre-init=${event.pre_init_count}`
|
|
16122
16986
|
);
|
|
16123
|
-
for (const
|
|
16987
|
+
for (const path24 of event.paths)
|
|
16124
16988
|
lines.push(
|
|
16125
|
-
` URL ${
|
|
16989
|
+
` URL ${path24.path}: ${path24.pageviews} pageviews, ${path24.count} pushes, ${path24.pspReturn_count} PSP returns`
|
|
16126
16990
|
);
|
|
16127
|
-
for (const field of event.fields)
|
|
16991
|
+
for (const field of event.fields) {
|
|
16128
16992
|
lines.push(
|
|
16129
16993
|
`${field.type_collision ? " \u26A0 TYPE COLLISION " : field.types.some((type) => /^(?:num)(?::|$)/.test(type)) && field.types.some((type) => /^(?:numstr|numstr_comma)(?::|$)/.test(type)) ? " INFO numeric representation " : " "}${field.path}: ${field.types.join(" | ")} (${field.seen})`
|
|
16130
16994
|
);
|
|
16995
|
+
if (dimensionEligibility(event, field).eligible) {
|
|
16996
|
+
lines.push(` Create dimension: ${createDimensionInvocation(field.path, event.event)}`);
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16131
16999
|
}
|
|
16132
17000
|
if (report.trigger_diagnoses.length) {
|
|
16133
17001
|
lines.push("\n\u26A0 Trigger diagnoses");
|
|
@@ -16161,16 +17029,30 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
|
|
|
16161
17029
|
return `${lines.join("\n")}
|
|
16162
17030
|
`;
|
|
16163
17031
|
}
|
|
16164
|
-
var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").action(async function
|
|
17032
|
+
var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function action63(opts) {
|
|
16165
17033
|
if (opts.gtm && opts.gtmUrl)
|
|
16166
17034
|
fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
|
|
17035
|
+
if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
|
|
17036
|
+
fail(
|
|
17037
|
+
this,
|
|
17038
|
+
"GTM mining cannot be combined with --create-dimension. Run discovery and creation as separate commands.",
|
|
17039
|
+
EXIT_CODES.validation
|
|
17040
|
+
);
|
|
17041
|
+
}
|
|
17042
|
+
if (!opts.createDimension) {
|
|
17043
|
+
const createOnlyFlag = opts.event !== void 0 ? "--event" : opts.key !== void 0 ? "--key" : opts.label !== void 0 ? "--label" : void 0;
|
|
17044
|
+
if (createOnlyFlag)
|
|
17045
|
+
fail(this, `${createOnlyFlag} requires --create-dimension`, EXIT_CODES.validation);
|
|
17046
|
+
}
|
|
16167
17047
|
const gtmInput = opts.gtmUrl ?? opts.gtm;
|
|
16168
17048
|
const minutes = minutesValue(opts.minutes) ?? 10080;
|
|
16169
17049
|
let report;
|
|
16170
17050
|
let apiSucceeded = false;
|
|
17051
|
+
let discoveryCredentials;
|
|
16171
17052
|
let apiFailure;
|
|
16172
17053
|
try {
|
|
16173
|
-
|
|
17054
|
+
discoveryCredentials = requireCredentials();
|
|
17055
|
+
const result = await discoverTrackingSchema(discoveryCredentials, minutes);
|
|
16174
17056
|
if (result.ok && result.data) {
|
|
16175
17057
|
report = result.data;
|
|
16176
17058
|
apiSucceeded = true;
|
|
@@ -16186,7 +17068,7 @@ var discoverCommand = new Command("discover").description("Read back the trackin
|
|
|
16186
17068
|
...error instanceof CliError ? { details: error.details, cliError: error } : {}
|
|
16187
17069
|
};
|
|
16188
17070
|
}
|
|
16189
|
-
if (!report && !gtmInput) {
|
|
17071
|
+
if (!report && (!gtmInput || opts.createDimension)) {
|
|
16190
17072
|
if (apiFailure?.cliError) throw apiFailure.cliError;
|
|
16191
17073
|
fail(
|
|
16192
17074
|
this,
|
|
@@ -16241,20 +17123,92 @@ var discoverCommand = new Command("discover").description("Read back the trackin
|
|
|
16241
17123
|
report.gtm = gtm;
|
|
16242
17124
|
report.sources.gtm_static = gtm.containers.length > 0;
|
|
16243
17125
|
}
|
|
17126
|
+
if (opts.createDimension) {
|
|
17127
|
+
if (!apiSucceeded || !discoveryCredentials) {
|
|
17128
|
+
fail(
|
|
17129
|
+
this,
|
|
17130
|
+
apiFailure?.message ?? "Discovery API data is required to create a dimension",
|
|
17131
|
+
classifyStatus(apiFailure?.status ?? 0),
|
|
17132
|
+
apiFailure?.details
|
|
17133
|
+
);
|
|
17134
|
+
}
|
|
17135
|
+
const observation = resolveDimensionObservation(
|
|
17136
|
+
this,
|
|
17137
|
+
report,
|
|
17138
|
+
opts.createDimension,
|
|
17139
|
+
opts.event
|
|
17140
|
+
);
|
|
17141
|
+
const definition = {
|
|
17142
|
+
key: opts.key ?? opts.createDimension.toLowerCase(),
|
|
17143
|
+
label: opts.label ?? defaultDimensionLabel(opts.createDimension),
|
|
17144
|
+
value_type: isNumericField(observation.field) ? "number" : "string",
|
|
17145
|
+
capture: {
|
|
17146
|
+
mode: "data_layer",
|
|
17147
|
+
...observation.dlName !== "dataLayer" ? { dataLayerName: observation.dlName } : {},
|
|
17148
|
+
eventName: observation.eventName,
|
|
17149
|
+
from: opts.createDimension,
|
|
17150
|
+
persist: true
|
|
17151
|
+
},
|
|
17152
|
+
is_dimension: true
|
|
17153
|
+
};
|
|
17154
|
+
let created;
|
|
17155
|
+
try {
|
|
17156
|
+
created = await createInsight(discoveryCredentials, "signals", definition);
|
|
17157
|
+
} catch (error) {
|
|
17158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17159
|
+
fail(
|
|
17160
|
+
this,
|
|
17161
|
+
`Dimension was not created: ${message}`,
|
|
17162
|
+
error instanceof CliError ? error.exitCode : EXIT_CODES.general,
|
|
17163
|
+
error instanceof CliError ? error.details : void 0
|
|
17164
|
+
);
|
|
17165
|
+
}
|
|
17166
|
+
if (!created.ok || !created.data) {
|
|
17167
|
+
const reason = created.error ?? "The signals API rejected the request";
|
|
17168
|
+
if (created.status === 409) {
|
|
17169
|
+
fail(
|
|
17170
|
+
this,
|
|
17171
|
+
`Dimension was not created: ${reason} Choose another --key or archive an existing dimension in Audience > Attributes, then retry.`,
|
|
17172
|
+
classifyStatus(created.status),
|
|
17173
|
+
created.errorDetails
|
|
17174
|
+
);
|
|
17175
|
+
}
|
|
17176
|
+
if (created.status === 400) {
|
|
17177
|
+
fail(
|
|
17178
|
+
this,
|
|
17179
|
+
`Dimension was not created: ${reason} Check --create-dimension, --event, --key, and --label, then retry.`,
|
|
17180
|
+
classifyStatus(created.status),
|
|
17181
|
+
created.errorDetails
|
|
17182
|
+
);
|
|
17183
|
+
}
|
|
17184
|
+
fail(
|
|
17185
|
+
this,
|
|
17186
|
+
`Dimension was not created: ${reason}`,
|
|
17187
|
+
classifyStatus(created.status),
|
|
17188
|
+
created.errorDetails
|
|
17189
|
+
);
|
|
17190
|
+
}
|
|
17191
|
+
output(this, { signal: created.data }, () => {
|
|
17192
|
+
process.stdout.write(`Created dimension ${created.data.id}.
|
|
17193
|
+
`);
|
|
17194
|
+
process.stdout.write("Next: open Audience > Attributes in the Apex dashboard.\n");
|
|
17195
|
+
});
|
|
17196
|
+
return;
|
|
17197
|
+
}
|
|
16244
17198
|
output(this, report, () => process.stdout.write(renderDiscovery(report)));
|
|
16245
17199
|
});
|
|
16246
17200
|
|
|
16247
17201
|
// src/commands/skills.ts
|
|
16248
|
-
var
|
|
16249
|
-
var
|
|
17202
|
+
var import_node_fs23 = __toESM(require("node:fs"));
|
|
17203
|
+
var import_node_path21 = __toESM(require("node:path"));
|
|
16250
17204
|
function packageRoot() {
|
|
16251
17205
|
const candidates = [
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
17206
|
+
import_node_path21.default.resolve(__dirname, ".."),
|
|
17207
|
+
import_node_path21.default.resolve(import_node_path21.default.dirname(process.argv[1] ?? process.cwd()), ".."),
|
|
17208
|
+
import_node_path21.default.resolve(process.cwd(), "packages/cli")
|
|
16255
17209
|
];
|
|
16256
17210
|
for (const candidate of candidates) {
|
|
16257
|
-
if (
|
|
17211
|
+
if (import_node_fs23.default.existsSync(import_node_path21.default.join(candidate, "AGENTS.md"))) return candidate;
|
|
16258
17212
|
}
|
|
16259
17213
|
throw new Error(
|
|
16260
17214
|
`Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
|
|
@@ -16263,22 +17217,22 @@ function packageRoot() {
|
|
|
16263
17217
|
function skillPaths() {
|
|
16264
17218
|
const root = packageRoot();
|
|
16265
17219
|
return {
|
|
16266
|
-
agents:
|
|
16267
|
-
skill:
|
|
17220
|
+
agents: import_node_path21.default.join(root, "AGENTS.md"),
|
|
17221
|
+
skill: import_node_path21.default.join(root, "skills", "apex-cli", "SKILL.md")
|
|
16268
17222
|
};
|
|
16269
17223
|
}
|
|
16270
17224
|
function installSkill(paths, targetRoot) {
|
|
16271
|
-
const missing = [paths.skill, paths.agents].filter((filePath) => !
|
|
17225
|
+
const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs23.default.existsSync(filePath));
|
|
16272
17226
|
if (missing.length > 0) {
|
|
16273
17227
|
throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
|
|
16274
17228
|
}
|
|
16275
|
-
const target =
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
17229
|
+
const target = import_node_path21.default.resolve(targetRoot, "apex-cli");
|
|
17230
|
+
import_node_fs23.default.mkdirSync(target, { recursive: true });
|
|
17231
|
+
import_node_fs23.default.copyFileSync(paths.skill, import_node_path21.default.join(target, "SKILL.md"));
|
|
17232
|
+
import_node_fs23.default.copyFileSync(paths.agents, import_node_path21.default.join(target, "AGENTS.md"));
|
|
16279
17233
|
return target;
|
|
16280
17234
|
}
|
|
16281
|
-
var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function
|
|
17235
|
+
var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action64(opts) {
|
|
16282
17236
|
let paths;
|
|
16283
17237
|
try {
|
|
16284
17238
|
paths = skillPaths();
|
|
@@ -16308,7 +17262,7 @@ function switchValue(value) {
|
|
|
16308
17262
|
throw new Error("Use on or off");
|
|
16309
17263
|
}
|
|
16310
17264
|
var settingsGet = new Command("settings").description("Show platform operator settings").action(async function() {
|
|
16311
|
-
const res = await getAdminSettings(
|
|
17265
|
+
const res = await getAdminSettings(requireUserCredentials());
|
|
16312
17266
|
if (!res.ok || !res.data)
|
|
16313
17267
|
fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
|
|
16314
17268
|
output(
|
|
@@ -16340,14 +17294,14 @@ var set5 = new Command("set").description("Set platform kill switches").option("
|
|
|
16340
17294
|
`apex admin settings set ${switches} --confirm`
|
|
16341
17295
|
);
|
|
16342
17296
|
}
|
|
16343
|
-
const res = await setAdminSettings(
|
|
17297
|
+
const res = await setAdminSettings(requireUserCredentials(), body5);
|
|
16344
17298
|
if (!res.ok || !res.data)
|
|
16345
17299
|
fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
|
|
16346
17300
|
output(this, res.data, () => console.log("Platform settings updated."));
|
|
16347
17301
|
});
|
|
16348
17302
|
settingsGet.addCommand(set5);
|
|
16349
17303
|
var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
|
|
16350
|
-
const res = await getAdminQuarantine(
|
|
17304
|
+
const res = await getAdminQuarantine(requireUserCredentials());
|
|
16351
17305
|
if (!res.ok || !res.data)
|
|
16352
17306
|
fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
|
|
16353
17307
|
output(
|
|
@@ -16364,7 +17318,56 @@ var quarantine = new Command("quarantine").description("List tracking quarantine
|
|
|
16364
17318
|
)
|
|
16365
17319
|
);
|
|
16366
17320
|
});
|
|
16367
|
-
|
|
17321
|
+
async function confirmArchive(command, shopId, force) {
|
|
17322
|
+
if (force) return true;
|
|
17323
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
17324
|
+
confirmationRequired(
|
|
17325
|
+
command,
|
|
17326
|
+
"Archiving a shop requires confirmation in non-TTY mode.",
|
|
17327
|
+
`apex admin shops archive ${JSON.stringify(shopId)} --force`
|
|
17328
|
+
);
|
|
17329
|
+
}
|
|
17330
|
+
const readline3 = await import("node:readline");
|
|
17331
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
17332
|
+
const answer = await new Promise((resolve) => {
|
|
17333
|
+
rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve);
|
|
17334
|
+
});
|
|
17335
|
+
rl.close();
|
|
17336
|
+
if (answer.toLowerCase() !== "y") {
|
|
17337
|
+
console.error("Aborted.");
|
|
17338
|
+
return false;
|
|
17339
|
+
}
|
|
17340
|
+
return true;
|
|
17341
|
+
}
|
|
17342
|
+
var archive2 = new Command("archive").description("Archive a shop and disable Apex delivery").argument("<shopId>", "Shop ID").option("-f, --force", "Skip confirmation").action(async function(shopId, opts) {
|
|
17343
|
+
if (!await confirmArchive(this, shopId, opts.force)) return;
|
|
17344
|
+
const res = await archiveAdminShop(requireUserCredentials(), shopId);
|
|
17345
|
+
if (!res.ok || !res.data)
|
|
17346
|
+
fail(
|
|
17347
|
+
this,
|
|
17348
|
+
res.error ?? "Failed to archive shop",
|
|
17349
|
+
classifyStatus(res.status),
|
|
17350
|
+
res.errorDetails
|
|
17351
|
+
);
|
|
17352
|
+
output(this, res.data, () => console.log("Shop archived."));
|
|
17353
|
+
});
|
|
17354
|
+
var restore2 = new Command("restore").description("Restore an archived shop").argument("<shopId>", "Shop ID").action(async function(shopId) {
|
|
17355
|
+
const res = await restoreAdminShop(requireUserCredentials(), shopId);
|
|
17356
|
+
if (!res.ok || !res.data)
|
|
17357
|
+
fail(
|
|
17358
|
+
this,
|
|
17359
|
+
res.error ?? "Failed to restore shop",
|
|
17360
|
+
classifyStatus(res.status),
|
|
17361
|
+
res.errorDetails
|
|
17362
|
+
);
|
|
17363
|
+
output(
|
|
17364
|
+
this,
|
|
17365
|
+
res.data,
|
|
17366
|
+
() => console.log("Shop restored. Delivery remains controlled by the kill switch.")
|
|
17367
|
+
);
|
|
17368
|
+
});
|
|
17369
|
+
var shops = new Command("shops").description("Archive and restore shops on the platform admin plane").addCommand(archive2).addCommand(restore2);
|
|
17370
|
+
var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops);
|
|
16368
17371
|
|
|
16369
17372
|
// src/commands/membership.ts
|
|
16370
17373
|
init_source();
|
|
@@ -16408,7 +17411,12 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
|
|
|
16408
17411
|
orgCommand.command("members").action(async function() {
|
|
16409
17412
|
await run2(this, listOrgMembers(requireUserCredentials()), "list members");
|
|
16410
17413
|
});
|
|
16411
|
-
orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").
|
|
17414
|
+
orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
|
|
17415
|
+
new Option(
|
|
17416
|
+
"--default-shop-role <role>",
|
|
17417
|
+
"Default shop role for every brand in the workspace (workspace defaults are internal roles)"
|
|
17418
|
+
).choices(["editor"]).default("editor")
|
|
17419
|
+
).action(async function(email, o) {
|
|
16412
17420
|
if (o.role !== void 0) {
|
|
16413
17421
|
process.stderr.write(
|
|
16414
17422
|
"Warning: --role is deprecated for apex org invite; use --org-role instead. --role will be removed in v0.7.0.\n"
|
|
@@ -16452,7 +17460,12 @@ var teamCommand = new Command("team").description("Manage a shop team");
|
|
|
16452
17460
|
teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
|
|
16453
17461
|
await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
|
|
16454
17462
|
});
|
|
16455
|
-
teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").
|
|
17463
|
+
teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
|
|
17464
|
+
new Option(
|
|
17465
|
+
"--role <role>",
|
|
17466
|
+
"Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
|
|
17467
|
+
).choices(["editor", "client_viewer"]).makeOptionMandatory()
|
|
17468
|
+
).action(async function(email, o) {
|
|
16456
17469
|
await run2(
|
|
16457
17470
|
this,
|
|
16458
17471
|
inviteTeam(requireUserCredentials(), email, o.shop, o.role),
|
|
@@ -16466,7 +17479,12 @@ teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id
|
|
|
16466
17479
|
"revoke invitation"
|
|
16467
17480
|
);
|
|
16468
17481
|
});
|
|
16469
|
-
teamCommand.command("set-role").argument("<memberId>").
|
|
17482
|
+
teamCommand.command("set-role").argument("<memberId>").addArgument(
|
|
17483
|
+
new Argument(
|
|
17484
|
+
"<role>",
|
|
17485
|
+
"Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
|
|
17486
|
+
).choices(["editor", "client_viewer"])
|
|
17487
|
+
).requiredOption("--shop <id>").action(async function(id, role, o) {
|
|
16470
17488
|
await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
|
|
16471
17489
|
});
|
|
16472
17490
|
teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
|
|
@@ -16636,8 +17654,8 @@ async function fetchText(url) {
|
|
|
16636
17654
|
});
|
|
16637
17655
|
return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
|
|
16638
17656
|
}
|
|
16639
|
-
function abs(base,
|
|
16640
|
-
return new URL(
|
|
17657
|
+
function abs(base, path24) {
|
|
17658
|
+
return new URL(path24, base).toString();
|
|
16641
17659
|
}
|
|
16642
17660
|
function looksLikeShopifyCart(text) {
|
|
16643
17661
|
try {
|
|
@@ -16657,9 +17675,9 @@ async function collectSignals(url) {
|
|
|
16657
17675
|
const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
|
|
16658
17676
|
const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
|
|
16659
17677
|
if (!protectedHit) {
|
|
16660
|
-
for (const
|
|
17678
|
+
for (const path24 of ["/cart.js"]) {
|
|
16661
17679
|
try {
|
|
16662
|
-
const secondary = await fetchText(abs(primary.finalUrl || url,
|
|
17680
|
+
const secondary = await fetchText(abs(primary.finalUrl || url, path24));
|
|
16663
17681
|
if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
|
|
16664
17682
|
chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
|
|
16665
17683
|
}
|
|
@@ -16838,7 +17856,7 @@ function selectedEvents(opts) {
|
|
|
16838
17856
|
}
|
|
16839
17857
|
var list14 = new Command("list").alias("ls").description(
|
|
16840
17858
|
"List webhooks. JSON includes availableEvents; secrets are never returned after create."
|
|
16841
|
-
).action(async function
|
|
17859
|
+
).action(async function action65() {
|
|
16842
17860
|
const data = await loadWebhookIndex(this);
|
|
16843
17861
|
output(this, data, () => printWebhooks(data.data));
|
|
16844
17862
|
});
|
|
@@ -16847,7 +17865,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
16847
17865
|
"Webhook event to subscribe to; repeat for multiple events",
|
|
16848
17866
|
(value, previous = []) => [...previous, value],
|
|
16849
17867
|
[]
|
|
16850
|
-
).action(async function
|
|
17868
|
+
).action(async function action66(opts) {
|
|
16851
17869
|
const events = selectedEvents(opts);
|
|
16852
17870
|
if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
|
|
16853
17871
|
const index = await loadWebhookIndex(this);
|
|
@@ -16879,7 +17897,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
16879
17897
|
}
|
|
16880
17898
|
);
|
|
16881
17899
|
});
|
|
16882
|
-
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function
|
|
17900
|
+
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
|
|
16883
17901
|
requireShopSecretKey(this);
|
|
16884
17902
|
const result = handleResult(
|
|
16885
17903
|
this,
|
|
@@ -16888,7 +17906,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
|
|
|
16888
17906
|
);
|
|
16889
17907
|
output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
|
|
16890
17908
|
});
|
|
16891
|
-
var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function
|
|
17909
|
+
var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action68(id, opts) {
|
|
16892
17910
|
const limit = Number.parseInt(opts.limit, 10);
|
|
16893
17911
|
if (!Number.isInteger(limit) || limit < 1)
|
|
16894
17912
|
fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
|
|
@@ -16905,7 +17923,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
|
|
|
16905
17923
|
"Replacement event; repeat for multiple events",
|
|
16906
17924
|
(value, previous = []) => [...previous, value],
|
|
16907
17925
|
[]
|
|
16908
|
-
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function
|
|
17926
|
+
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
|
|
16909
17927
|
if (opts.active && opts.inactive)
|
|
16910
17928
|
fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
|
|
16911
17929
|
const events = selectedEvents(opts);
|
|
@@ -16938,7 +17956,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
|
|
|
16938
17956
|
);
|
|
16939
17957
|
output(this, { webhook: result }, () => printWebhooks([result]));
|
|
16940
17958
|
});
|
|
16941
|
-
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function
|
|
17959
|
+
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
|
|
16942
17960
|
requireShopSecretKey(this);
|
|
16943
17961
|
const result = handleResult(
|
|
16944
17962
|
this,
|
|
@@ -16957,10 +17975,10 @@ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks"
|
|
|
16957
17975
|
|
|
16958
17976
|
// src/commands/simulate.ts
|
|
16959
17977
|
init_source();
|
|
16960
|
-
var
|
|
16961
|
-
var
|
|
17978
|
+
var import_node_fs24 = __toESM(require("node:fs"));
|
|
17979
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
16962
17980
|
function readJson2(file) {
|
|
16963
|
-
const text = file === "-" ?
|
|
17981
|
+
const text = file === "-" ? import_node_fs24.default.readFileSync(0, "utf8") : import_node_fs24.default.readFileSync(import_node_path22.default.resolve(file), "utf8");
|
|
16964
17982
|
try {
|
|
16965
17983
|
return JSON.parse(text);
|
|
16966
17984
|
} catch (error) {
|
|
@@ -17082,8 +18100,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
|
|
|
17082
18100
|
});
|
|
17083
18101
|
|
|
17084
18102
|
// src/commands/config-resources.ts
|
|
17085
|
-
var
|
|
17086
|
-
var
|
|
18103
|
+
var import_node_fs25 = __toESM(require("node:fs"));
|
|
18104
|
+
var import_node_path23 = __toESM(require("node:path"));
|
|
17087
18105
|
function show(resource) {
|
|
17088
18106
|
process.stdout.write(`${JSON.stringify(resource, null, 2)}
|
|
17089
18107
|
`);
|
|
@@ -17156,12 +18174,12 @@ function readFamily(family, label) {
|
|
|
17156
18174
|
}
|
|
17157
18175
|
var assetReads = readFamily("assets", "assets");
|
|
17158
18176
|
var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
|
|
17159
|
-
const absolute =
|
|
17160
|
-
if (!
|
|
17161
|
-
const fileStat =
|
|
18177
|
+
const absolute = import_node_path23.default.resolve(process.cwd(), file);
|
|
18178
|
+
if (!import_node_fs25.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
|
|
18179
|
+
const fileStat = import_node_fs25.default.statSync(absolute);
|
|
17162
18180
|
if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
|
|
17163
18181
|
if (fileStat.size > MAX_ASSET_SIZE_BYTES) fail(this, `${file} is larger than 10 MB`, 3);
|
|
17164
|
-
const mimeType = ASSET_MIME_TYPES[
|
|
18182
|
+
const mimeType = ASSET_MIME_TYPES[import_node_path23.default.extname(absolute).toLowerCase()];
|
|
17165
18183
|
if (!mimeType)
|
|
17166
18184
|
fail(
|
|
17167
18185
|
this,
|
|
@@ -17170,8 +18188,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
|
|
|
17170
18188
|
);
|
|
17171
18189
|
const r = await uploadAssets(requireCredentials(), [
|
|
17172
18190
|
{
|
|
17173
|
-
name:
|
|
17174
|
-
bytes:
|
|
18191
|
+
name: import_node_path23.default.basename(absolute),
|
|
18192
|
+
bytes: import_node_fs25.default.readFileSync(absolute),
|
|
17175
18193
|
type: mimeType
|
|
17176
18194
|
}
|
|
17177
18195
|
]);
|