@drip-apex/apex-cli 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +43 -1
- package/dist/cli.js +1671 -442
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +36 -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)
|
|
@@ -6988,14 +6988,14 @@ var init_open = __esm({
|
|
|
6988
6988
|
}
|
|
6989
6989
|
const subprocess = import_node_child_process5.default.spawn(command, cliArguments, childProcessOptions);
|
|
6990
6990
|
if (options.wait) {
|
|
6991
|
-
return new Promise((
|
|
6991
|
+
return new Promise((resolve2, reject) => {
|
|
6992
6992
|
subprocess.once("error", reject);
|
|
6993
6993
|
subprocess.once("close", (exitCode) => {
|
|
6994
6994
|
if (!options.allowNonzeroExitCode && exitCode > 0) {
|
|
6995
6995
|
reject(new Error(`Exited with code ${exitCode}`));
|
|
6996
6996
|
return;
|
|
6997
6997
|
}
|
|
6998
|
-
|
|
6998
|
+
resolve2(subprocess);
|
|
6999
6999
|
});
|
|
7000
7000
|
});
|
|
7001
7001
|
}
|
|
@@ -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.3",
|
|
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();
|
|
@@ -8341,9 +8369,9 @@ function resolveSelection(tree, input) {
|
|
|
8341
8369
|
function ask(question) {
|
|
8342
8370
|
const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
8343
8371
|
return new Promise(
|
|
8344
|
-
(
|
|
8372
|
+
(resolve2) => rl.question(question, (answer) => {
|
|
8345
8373
|
rl.close();
|
|
8346
|
-
|
|
8374
|
+
resolve2(answer.trim());
|
|
8347
8375
|
})
|
|
8348
8376
|
);
|
|
8349
8377
|
}
|
|
@@ -8421,7 +8449,7 @@ function prompt(question, hide = false) {
|
|
|
8421
8449
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
8422
8450
|
throw new Error("Interactive input is unavailable in non-TTY mode.");
|
|
8423
8451
|
}
|
|
8424
|
-
return new Promise((
|
|
8452
|
+
return new Promise((resolve2) => {
|
|
8425
8453
|
const rl = import_node_readline2.default.createInterface({
|
|
8426
8454
|
input: process.stdin,
|
|
8427
8455
|
output: process.stderr
|
|
@@ -8441,13 +8469,13 @@ function prompt(question, hide = false) {
|
|
|
8441
8469
|
stdout.write = originalWrite;
|
|
8442
8470
|
console.error();
|
|
8443
8471
|
rl.close();
|
|
8444
|
-
|
|
8472
|
+
resolve2(answer.trim());
|
|
8445
8473
|
});
|
|
8446
8474
|
masking = true;
|
|
8447
8475
|
} else {
|
|
8448
8476
|
rl.question(question, (answer) => {
|
|
8449
8477
|
rl.close();
|
|
8450
|
-
|
|
8478
|
+
resolve2(answer.trim());
|
|
8451
8479
|
});
|
|
8452
8480
|
}
|
|
8453
8481
|
});
|
|
@@ -8471,9 +8499,9 @@ 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
|
-
return new Promise((
|
|
8504
|
+
return new Promise((resolve2, reject) => {
|
|
8477
8505
|
let settled = false;
|
|
8478
8506
|
let timeout = null;
|
|
8479
8507
|
const finish = (err, apiKey) => {
|
|
@@ -8485,7 +8513,7 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
|
|
|
8485
8513
|
} catch {
|
|
8486
8514
|
}
|
|
8487
8515
|
if (err) reject(err);
|
|
8488
|
-
else
|
|
8516
|
+
else resolve2(apiKey ?? "");
|
|
8489
8517
|
};
|
|
8490
8518
|
const server = import_node_http.default.createServer((req, res) => {
|
|
8491
8519
|
if (req.method === "OPTIONS") {
|
|
@@ -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) {
|
|
@@ -9131,8 +9414,8 @@ async function confirmApply(message) {
|
|
|
9131
9414
|
}
|
|
9132
9415
|
const readline3 = await import("node:readline");
|
|
9133
9416
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
9134
|
-
const answer = await new Promise((
|
|
9135
|
-
rl.question(source_default.yellow(`${message} [y/N] `),
|
|
9417
|
+
const answer = await new Promise((resolve2) => {
|
|
9418
|
+
rl.question(source_default.yellow(`${message} [y/N] `), resolve2);
|
|
9136
9419
|
});
|
|
9137
9420
|
rl.close();
|
|
9138
9421
|
return answer.toLowerCase() === "y";
|
|
@@ -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();
|
|
@@ -9469,8 +9828,8 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
|
|
|
9469
9828
|
input: process.stdin,
|
|
9470
9829
|
output: process.stderr
|
|
9471
9830
|
});
|
|
9472
|
-
const answer = await new Promise((
|
|
9473
|
-
rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `),
|
|
9831
|
+
const answer = await new Promise((resolve2) => {
|
|
9832
|
+
rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve2);
|
|
9474
9833
|
});
|
|
9475
9834
|
rl.close();
|
|
9476
9835
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -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);
|
|
@@ -9898,7 +10357,7 @@ var screenshots = new Command("screenshots").description(
|
|
|
9898
10357
|
if (screenshotNow() >= deadline)
|
|
9899
10358
|
fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
9900
10359
|
await new Promise(
|
|
9901
|
-
(
|
|
10360
|
+
(resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
|
|
9902
10361
|
);
|
|
9903
10362
|
if (screenshotNow() >= deadline)
|
|
9904
10363
|
fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
@@ -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: {
|
|
@@ -10401,11 +10938,11 @@ function cleanupResetJs(experiment) {
|
|
|
10401
10938
|
}
|
|
10402
10939
|
function runAbHarness(args, cwd) {
|
|
10403
10940
|
const bin = process.env.AB_HARNESS_BIN || "ab-harness";
|
|
10404
|
-
return new Promise((
|
|
10941
|
+
return new Promise((resolve2, reject) => {
|
|
10405
10942
|
const child = (0, import_node_child_process6.spawn)(bin, args, { cwd, stdio: "inherit" });
|
|
10406
10943
|
child.on("error", reject);
|
|
10407
10944
|
child.on("exit", (code) => {
|
|
10408
|
-
if (code === 0)
|
|
10945
|
+
if (code === 0) resolve2();
|
|
10409
10946
|
else reject(new Error(`${bin} ${args.join(" ")} exited with ${code}`));
|
|
10410
10947
|
});
|
|
10411
10948
|
});
|
|
@@ -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)
|
|
@@ -11879,7 +12416,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
|
|
|
11879
12416
|
const readline3 = await import("node:readline");
|
|
11880
12417
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
11881
12418
|
const answer = await new Promise(
|
|
11882
|
-
(
|
|
12419
|
+
(resolve2) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve2)
|
|
11883
12420
|
);
|
|
11884
12421
|
rl.close();
|
|
11885
12422
|
if (answer.toLowerCase() !== "y") continue;
|
|
@@ -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(
|
|
@@ -12241,40 +12778,283 @@ var off = new Command("off").description("Disable consent gating").action(async
|
|
|
12241
12778
|
});
|
|
12242
12779
|
var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
|
|
12243
12780
|
|
|
12244
|
-
// src/commands/
|
|
12245
|
-
var
|
|
12246
|
-
var
|
|
12781
|
+
// src/commands/project-code.ts
|
|
12782
|
+
var import_node_fs15 = require("node:fs");
|
|
12783
|
+
var import_node_path13 = require("node:path");
|
|
12247
12784
|
|
|
12248
|
-
// src/
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12785
|
+
// src/commands/runtime-settings-shop.ts
|
|
12786
|
+
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;
|
|
12787
|
+
async function identifyRuntimeSettingsShop(command, requested) {
|
|
12788
|
+
let credentials = requireCredentials();
|
|
12789
|
+
if (credentials.credentialKind === "user") {
|
|
12790
|
+
let shopId = requested;
|
|
12791
|
+
if (!uuidPattern.test(requested)) {
|
|
12792
|
+
const context = await getAccountContext({
|
|
12793
|
+
...credentials,
|
|
12794
|
+
resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
|
|
12795
|
+
});
|
|
12796
|
+
if (!context.ok || !context.data)
|
|
12797
|
+
fail(command, context.error ?? "Shop discovery failed", classifyStatus(context.status));
|
|
12798
|
+
const match2 = context.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
|
|
12799
|
+
if (!match2)
|
|
12800
|
+
fail(command, "--shop domain is not available to this user", EXIT_CODES.validation);
|
|
12801
|
+
shopId = match2.id;
|
|
12802
|
+
}
|
|
12803
|
+
credentials = { ...credentials, resolvedContext: { ...credentials.resolvedContext, shopId } };
|
|
12804
|
+
}
|
|
12805
|
+
const response = await getInstallDomains(credentials);
|
|
12806
|
+
if (!response.ok || !response.data)
|
|
12807
|
+
fail(command, response.error ?? "Shop identity unavailable", classifyStatus(response.status));
|
|
12808
|
+
const { shop_id, domain } = response.data;
|
|
12809
|
+
if (credentials.credentialKind === "user" && shop_id.toLowerCase() !== credentials.resolvedContext?.shopId?.toLowerCase()) {
|
|
12810
|
+
fail(
|
|
12811
|
+
command,
|
|
12812
|
+
"Shop identity does not match the requested user-key context",
|
|
12813
|
+
EXIT_CODES.validation
|
|
12259
12814
|
);
|
|
12260
|
-
return [{ path: explicitPath, values, magnitude }];
|
|
12261
12815
|
}
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12816
|
+
if (requested.toLowerCase() !== shop_id.toLowerCase() && requested.toLowerCase() !== domain.toLowerCase()) {
|
|
12817
|
+
fail(command, "--shop does not match the authenticated shop", EXIT_CODES.validation);
|
|
12818
|
+
}
|
|
12819
|
+
setEnvelopeContext({ shop_id, credential: credentials.credentialKind ?? "shop" });
|
|
12820
|
+
return { shop_id, domain, credentials };
|
|
12821
|
+
}
|
|
12822
|
+
|
|
12823
|
+
// src/commands/project-code.ts
|
|
12824
|
+
var fields = ["enabled", "css", "helperJs", "js"];
|
|
12825
|
+
function readPatch(command, file) {
|
|
12826
|
+
let input;
|
|
12827
|
+
try {
|
|
12828
|
+
input = JSON.parse((0, import_node_fs15.readFileSync)(file, "utf8"));
|
|
12829
|
+
} catch (error) {
|
|
12830
|
+
fail(command, `Cannot read project code JSON: ${String(error)}`, EXIT_CODES.validation);
|
|
12831
|
+
}
|
|
12832
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length === 0) {
|
|
12833
|
+
fail(command, "Project code must be a nonempty partial object", EXIT_CODES.validation);
|
|
12834
|
+
}
|
|
12835
|
+
for (const [key, value] of Object.entries(input)) {
|
|
12836
|
+
if (!fields.includes(key))
|
|
12837
|
+
fail(command, `Unknown project code field: ${key}`, EXIT_CODES.validation);
|
|
12838
|
+
if (key === "enabled" ? typeof value !== "boolean" : value !== null && (typeof value !== "string" || value.length > 2e4)) {
|
|
12839
|
+
fail(
|
|
12840
|
+
command,
|
|
12841
|
+
`${key} must be ${key === "enabled" ? "a boolean" : "null or a string of at most 20,000 characters"}`,
|
|
12842
|
+
EXIT_CODES.validation
|
|
12843
|
+
);
|
|
12844
|
+
}
|
|
12845
|
+
}
|
|
12846
|
+
return input;
|
|
12847
|
+
}
|
|
12848
|
+
async function readCurrent(command, shop) {
|
|
12849
|
+
const response = await getRuntimeSettings(shop.credentials);
|
|
12850
|
+
if (!response.ok || !response.data)
|
|
12851
|
+
fail(
|
|
12852
|
+
command,
|
|
12853
|
+
response.error ?? "Runtime settings read failed",
|
|
12854
|
+
classifyStatus(response.status)
|
|
12855
|
+
);
|
|
12856
|
+
const data = response.data;
|
|
12857
|
+
if (data.shop_id !== shop.shop_id)
|
|
12858
|
+
fail(command, "Runtime settings returned a different shop", EXIT_CODES.validation);
|
|
12859
|
+
const projectCode = data.runtime_settings?.projectCode;
|
|
12860
|
+
if (!projectCode || typeof projectCode.enabled !== "boolean" || [projectCode.css, projectCode.helperJs, projectCode.js].some(
|
|
12861
|
+
(code) => typeof code !== "string"
|
|
12862
|
+
) || !(data.version === null || typeof data.version === "string" && data.version.length > 0)) {
|
|
12863
|
+
fail(
|
|
12864
|
+
command,
|
|
12865
|
+
"Runtime settings must include complete project code and an opaque version",
|
|
12866
|
+
EXIT_CODES.general
|
|
12867
|
+
);
|
|
12868
|
+
}
|
|
12869
|
+
return { projectCode, version: data.version };
|
|
12870
|
+
}
|
|
12871
|
+
function effectiveDiff(before, patch) {
|
|
12872
|
+
const after = { ...before };
|
|
12873
|
+
if (patch.enabled !== void 0) after.enabled = patch.enabled;
|
|
12874
|
+
for (const key of ["css", "helperJs", "js"]) {
|
|
12875
|
+
if (patch[key] !== void 0) after[key] = patch[key]?.trim() || "";
|
|
12876
|
+
}
|
|
12877
|
+
const changedFields = fields.filter((field) => before[field] !== after[field]);
|
|
12878
|
+
const changes = Object.fromEntries(
|
|
12879
|
+
changedFields.map((field) => [field, { before: before[field], after: after[field] }])
|
|
12880
|
+
);
|
|
12881
|
+
return { changedFields, changes, before, after };
|
|
12882
|
+
}
|
|
12883
|
+
var projectCodeCommand = new Command("project-code").description(
|
|
12884
|
+
"Inspect, preview, and back up shop project code"
|
|
12885
|
+
);
|
|
12886
|
+
projectCodeCommand.addCommand(
|
|
12887
|
+
new Command("get").description("Read complete project code and its opaque version").requiredOption("--shop <domain-or-id>", "Shop domain or UUID").action(async (options, command) => {
|
|
12888
|
+
const { credentials: _credentials, ...identity } = await identifyRuntimeSettingsShop(
|
|
12889
|
+
command,
|
|
12890
|
+
options.shop
|
|
12891
|
+
);
|
|
12892
|
+
const current = await readCurrent(command, { ...identity, credentials: _credentials });
|
|
12893
|
+
output(
|
|
12894
|
+
command,
|
|
12895
|
+
{ ...identity, ...current },
|
|
12896
|
+
() => process.stdout.write(`${JSON.stringify({ ...identity, ...current }, null, 2)}
|
|
12897
|
+
`)
|
|
12898
|
+
);
|
|
12899
|
+
})
|
|
12900
|
+
);
|
|
12901
|
+
for (const mode of ["diff", "set"]) {
|
|
12902
|
+
const command = new Command(mode).description(
|
|
12903
|
+
mode === "diff" ? "Preview an effective partial project code change without writing" : "Back up prior code and apply a version-checked partial change"
|
|
12904
|
+
).requiredOption("--file <path>", "Partial projectCode JSON file").requiredOption("--shop <domain-or-id>", "Shop domain or UUID");
|
|
12905
|
+
if (mode === "set")
|
|
12906
|
+
command.requiredOption(
|
|
12907
|
+
"--backup <path>",
|
|
12908
|
+
"New backup file; an existing file is never replaced"
|
|
12909
|
+
);
|
|
12910
|
+
command.action(async (options, actionCommand) => {
|
|
12911
|
+
const patch = readPatch(actionCommand, options.file);
|
|
12912
|
+
const shop = await identifyRuntimeSettingsShop(actionCommand, options.shop);
|
|
12913
|
+
const current = await readCurrent(actionCommand, shop);
|
|
12914
|
+
const { credentials: _credentials, ...identity } = shop;
|
|
12915
|
+
const diff = effectiveDiff(current.projectCode, patch);
|
|
12916
|
+
if (mode === "diff" || diff.changedFields.length === 0) {
|
|
12917
|
+
const result2 = {
|
|
12918
|
+
...identity,
|
|
12919
|
+
status: diff.changedFields.length ? "changed" : "unchanged",
|
|
12920
|
+
version: current.version,
|
|
12921
|
+
diff
|
|
12922
|
+
};
|
|
12923
|
+
output(
|
|
12924
|
+
actionCommand,
|
|
12925
|
+
result2,
|
|
12926
|
+
() => process.stdout.write(`${JSON.stringify(result2, null, 2)}
|
|
12927
|
+
`)
|
|
12928
|
+
);
|
|
12929
|
+
return;
|
|
12930
|
+
}
|
|
12931
|
+
const backupPath = (0, import_node_path13.resolve)(options.backup);
|
|
12932
|
+
try {
|
|
12933
|
+
(0, import_node_fs15.writeFileSync)(
|
|
12934
|
+
backupPath,
|
|
12935
|
+
`${JSON.stringify({ ...identity, ...current, capturedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
12936
|
+
`,
|
|
12937
|
+
{ flag: "wx", mode: 384 }
|
|
12938
|
+
);
|
|
12939
|
+
} catch (error) {
|
|
12940
|
+
fail(
|
|
12941
|
+
actionCommand,
|
|
12942
|
+
`Backup failed; project code was not sent: ${String(error)}`,
|
|
12943
|
+
EXIT_CODES.general
|
|
12944
|
+
);
|
|
12945
|
+
}
|
|
12946
|
+
const response = await updateRuntimeSettings(shop.credentials, {
|
|
12947
|
+
projectCode: patch,
|
|
12948
|
+
expectedVersion: current.version
|
|
12949
|
+
});
|
|
12950
|
+
if (!response.ok || !response.data) {
|
|
12951
|
+
fail(
|
|
12952
|
+
actionCommand,
|
|
12953
|
+
response.error ?? "Project code update failed",
|
|
12954
|
+
classifyStatus(response.status),
|
|
12955
|
+
{ ...response.errorDetails, backupPath }
|
|
12956
|
+
);
|
|
12957
|
+
}
|
|
12958
|
+
const data = response.data;
|
|
12959
|
+
const warnings = [...data.warnings ?? []];
|
|
12960
|
+
if (data.precompile?.status === "warning" || data.precompile?.status === "error") {
|
|
12961
|
+
warnings.push(
|
|
12962
|
+
`Project code saved; publication ${data.precompile.status}: ${data.precompile.summary}`
|
|
12963
|
+
);
|
|
12964
|
+
}
|
|
12965
|
+
if (data.shop_id !== identity.shop_id)
|
|
12966
|
+
warnings.push(
|
|
12967
|
+
"The write response has a different shop identity; inspect the saved state before another write."
|
|
12968
|
+
);
|
|
12969
|
+
for (const warning of warnings) process.stderr.write(`WARNING: ${warning}
|
|
12970
|
+
`);
|
|
12971
|
+
const result = {
|
|
12972
|
+
...identity,
|
|
12973
|
+
status: "saved",
|
|
12974
|
+
diff,
|
|
12975
|
+
backupPath,
|
|
12976
|
+
version: data.version,
|
|
12977
|
+
writtenVersion: data.writtenVersion,
|
|
12978
|
+
precompile: data.precompile,
|
|
12979
|
+
warnings
|
|
12980
|
+
};
|
|
12981
|
+
output(
|
|
12982
|
+
actionCommand,
|
|
12983
|
+
result,
|
|
12984
|
+
() => process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
12985
|
+
`)
|
|
12986
|
+
);
|
|
12987
|
+
});
|
|
12988
|
+
projectCodeCommand.addCommand(command);
|
|
12989
|
+
}
|
|
12990
|
+
|
|
12991
|
+
// src/commands/triggers.ts
|
|
12992
|
+
var import_node_fs16 = __toESM(require("node:fs"));
|
|
12993
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
12994
|
+
|
|
12995
|
+
// src/trigger-proposal.ts
|
|
12996
|
+
function runtimePath(path24) {
|
|
12997
|
+
return path24.replaceAll("[]", ".0");
|
|
12998
|
+
}
|
|
12999
|
+
function fieldsFrom(value, prefix = "", descriptorList = false) {
|
|
13000
|
+
if (!value || typeof value !== "object") return [];
|
|
13001
|
+
if (Array.isArray(value)) {
|
|
13002
|
+
return value.flatMap(
|
|
13003
|
+
(item, index) => fieldsFrom(
|
|
13004
|
+
item,
|
|
13005
|
+
descriptorList ? prefix : prefix ? `${prefix}.${index}` : String(index),
|
|
13006
|
+
descriptorList
|
|
13007
|
+
)
|
|
13008
|
+
);
|
|
13009
|
+
}
|
|
13010
|
+
const record2 = value;
|
|
13011
|
+
const explicitPath = typeof record2.path === "string" ? record2.path : void 0;
|
|
13012
|
+
if (descriptorList && explicitPath) {
|
|
13013
|
+
const rawValues = record2.values ?? record2.samples ?? record2.examples ?? record2.example;
|
|
13014
|
+
const values = Array.isArray(rawValues) ? rawValues : rawValues === void 0 ? [] : [rawValues];
|
|
13015
|
+
const magnitude = [record2.mean, record2.average, record2.max, record2.magnitude].find(
|
|
13016
|
+
(candidate) => typeof candidate === "number"
|
|
13017
|
+
);
|
|
13018
|
+
const numericType = Array.isArray(record2.types) && record2.types.length > 0 && record2.types.every((type) => type === "num" || type === "numstr" || type === "numstr_comma");
|
|
13019
|
+
return [{ path: runtimePath(explicitPath), values, magnitude, numericType }];
|
|
13020
|
+
}
|
|
13021
|
+
return Object.entries(record2).flatMap(([key, child]) => {
|
|
13022
|
+
if (["eventName", "event_name", "name", "count", "frequency", "evidence"].includes(key))
|
|
13023
|
+
return [];
|
|
13024
|
+
const next = prefix ? `${prefix}.${key}` : key;
|
|
13025
|
+
if (child === null || ["string", "number", "boolean"].includes(typeof child)) {
|
|
13026
|
+
return [{ path: next, values: [child] }];
|
|
13027
|
+
}
|
|
13028
|
+
if (Array.isArray(child) && child.every((item) => item === null || typeof item !== "object")) {
|
|
13029
|
+
return [{ path: next, values: child }];
|
|
13030
|
+
}
|
|
13031
|
+
if (Array.isArray(child)) {
|
|
12273
13032
|
return [{ path: next, values: [child] }, ...fieldsFrom(child, next)];
|
|
12274
13033
|
}
|
|
12275
13034
|
return fieldsFrom(child, next);
|
|
12276
13035
|
});
|
|
12277
13036
|
}
|
|
13037
|
+
function isDescriptorArray(value) {
|
|
13038
|
+
const descriptorKeys = /* @__PURE__ */ new Set([
|
|
13039
|
+
"path",
|
|
13040
|
+
"values",
|
|
13041
|
+
"samples",
|
|
13042
|
+
"examples",
|
|
13043
|
+
"example",
|
|
13044
|
+
"mean",
|
|
13045
|
+
"average",
|
|
13046
|
+
"max",
|
|
13047
|
+
"magnitude",
|
|
13048
|
+
"types",
|
|
13049
|
+
"seen",
|
|
13050
|
+
"type_collision"
|
|
13051
|
+
]);
|
|
13052
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => {
|
|
13053
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
13054
|
+
const record2 = item;
|
|
13055
|
+
return typeof record2.path === "string" && record2.path.length > 0 && Object.keys(record2).every((key) => descriptorKeys.has(key));
|
|
13056
|
+
});
|
|
13057
|
+
}
|
|
12278
13058
|
function candidatesFrom(report) {
|
|
12279
13059
|
if (!report || typeof report !== "object") return [];
|
|
12280
13060
|
const root = report;
|
|
@@ -12292,15 +13072,15 @@ function candidatesFrom(report) {
|
|
|
12292
13072
|
}
|
|
12293
13073
|
return [root];
|
|
12294
13074
|
}
|
|
12295
|
-
function score2(
|
|
13075
|
+
function score2(path24, patterns) {
|
|
12296
13076
|
return patterns.reduce(
|
|
12297
|
-
(total, pattern, index) => total + (pattern.test(
|
|
13077
|
+
(total, pattern, index) => total + (pattern.test(path24) ? patterns.length - index : 0),
|
|
12298
13078
|
0
|
|
12299
13079
|
);
|
|
12300
13080
|
}
|
|
12301
|
-
function best(
|
|
12302
|
-
const ranked =
|
|
12303
|
-
(field) => !numeric2 || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
|
|
13081
|
+
function best(fields2, patterns, numeric2 = false) {
|
|
13082
|
+
const ranked = fields2.filter(
|
|
13083
|
+
(field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
|
|
12304
13084
|
).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
13085
|
if (!ranked.length) return [];
|
|
12306
13086
|
const top = ranked[0].score;
|
|
@@ -12322,17 +13102,17 @@ function proposeTriggers(report) {
|
|
|
12322
13102
|
eventName
|
|
12323
13103
|
))
|
|
12324
13104
|
continue;
|
|
12325
|
-
const
|
|
12326
|
-
|
|
12327
|
-
);
|
|
13105
|
+
const descriptors = candidate.fields ?? candidate.paths;
|
|
13106
|
+
const payload = candidate.properties ?? candidate.payload ?? candidate;
|
|
13107
|
+
const fields2 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
|
|
12328
13108
|
const revenues = best(
|
|
12329
|
-
|
|
13109
|
+
fields2,
|
|
12330
13110
|
[/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
|
|
12331
13111
|
true
|
|
12332
13112
|
);
|
|
12333
|
-
const orderIds = best(
|
|
12334
|
-
const currencies = best(
|
|
12335
|
-
const items = best(
|
|
13113
|
+
const orderIds = best(fields2, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
|
|
13114
|
+
const currencies = best(fields2, [/currency/, /(^|\.)cur(rency)?$/]);
|
|
13115
|
+
const items = best(fields2, [/(^|\.)((line_?)?items|products)$/]);
|
|
12336
13116
|
if (!revenues.length || !orderIds.length || !currencies.length) continue;
|
|
12337
13117
|
for (const revenue of revenues)
|
|
12338
13118
|
for (const orderId of orderIds)
|
|
@@ -12394,6 +13174,20 @@ function proposeTriggers(report) {
|
|
|
12394
13174
|
return output2;
|
|
12395
13175
|
}
|
|
12396
13176
|
|
|
13177
|
+
// src/generated/runtime-settings-trigger-constants.json
|
|
13178
|
+
var runtime_settings_trigger_constants_default = {
|
|
13179
|
+
dotPathPattern: "^[A-Za-z_$][\\w$]*(?:\\.(?:\\d+|[A-Za-z_$][\\w$]*))*$",
|
|
13180
|
+
signalPathPattern: "^signal:[A-Za-z_$][\\w$]*$",
|
|
13181
|
+
valueTypes: ["string", "number", "boolean", "string_array", "json"],
|
|
13182
|
+
whenOps: ["eq", "neq", "gt", "lt", "exists", "not_exists"],
|
|
13183
|
+
maxWhenEntries: 5,
|
|
13184
|
+
maxSourcePaths: 5,
|
|
13185
|
+
maxTransformMapEntries: 20,
|
|
13186
|
+
maxTransformMapKeyChars: 100,
|
|
13187
|
+
maxTransformMapValueChars: 100,
|
|
13188
|
+
maxTriggers: 20
|
|
13189
|
+
};
|
|
13190
|
+
|
|
12397
13191
|
// src/commands/triggers.ts
|
|
12398
13192
|
var allowed = /* @__PURE__ */ new Set([
|
|
12399
13193
|
"eventName",
|
|
@@ -12402,11 +13196,15 @@ var allowed = /* @__PURE__ */ new Set([
|
|
|
12402
13196
|
"reEvaluate",
|
|
12403
13197
|
"trackPageview",
|
|
12404
13198
|
"trackEventName",
|
|
13199
|
+
"when",
|
|
12405
13200
|
"trackRevenue",
|
|
12406
|
-
"mappings"
|
|
13201
|
+
"mappings",
|
|
13202
|
+
"properties"
|
|
12407
13203
|
]);
|
|
12408
|
-
var pathPattern =
|
|
12409
|
-
var
|
|
13204
|
+
var pathPattern = new RegExp(runtime_settings_trigger_constants_default.dotPathPattern);
|
|
13205
|
+
var signalPathPattern = new RegExp(runtime_settings_trigger_constants_default.signalPathPattern);
|
|
13206
|
+
var valueTypes = new Set(runtime_settings_trigger_constants_default.valueTypes);
|
|
13207
|
+
var whenOps = new Set(runtime_settings_trigger_constants_default.whenOps);
|
|
12410
13208
|
function strictObject(value, location, keys) {
|
|
12411
13209
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12412
13210
|
throw new Error(`${location} must be an object`);
|
|
@@ -12426,26 +13224,126 @@ function strictPath(value, location, required = false) {
|
|
|
12426
13224
|
`${location} must use dot-path syntax with property or array-index segments (for example: ecommerce.items.0.price); bracket syntax is not accepted`
|
|
12427
13225
|
);
|
|
12428
13226
|
}
|
|
13227
|
+
function strictSource(value, location, allowSignal, required = false) {
|
|
13228
|
+
if (value === void 0 && !required) return;
|
|
13229
|
+
const sources = Array.isArray(value) ? value : [value];
|
|
13230
|
+
if (Array.isArray(value) && (value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxSourcePaths))
|
|
13231
|
+
throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxSourcePaths} paths`);
|
|
13232
|
+
sources.forEach((source, index) => {
|
|
13233
|
+
const sourceLocation = Array.isArray(value) ? `${location}[${index}]` : location;
|
|
13234
|
+
if (allowSignal && typeof source === "string" && signalPathPattern.test(source)) return;
|
|
13235
|
+
strictPath(source, sourceLocation, true);
|
|
13236
|
+
});
|
|
13237
|
+
}
|
|
13238
|
+
function defaultMatchesValueType(value, valueType) {
|
|
13239
|
+
if (valueType === "number") {
|
|
13240
|
+
if (typeof value !== "string") return Number.isFinite(Number(value));
|
|
13241
|
+
const text = String(value).trim();
|
|
13242
|
+
const normalized = /^-?\d{1,3}(\.\d{3})+,\d+$/.test(text) ? text.replace(/\./g, "").replace(",", ".") : /^-?\d+,\d+$/.test(text) ? text.replace(",", ".") : text;
|
|
13243
|
+
return Number.isFinite(Number(normalized));
|
|
13244
|
+
}
|
|
13245
|
+
if (valueType === "boolean") {
|
|
13246
|
+
if (typeof value === "boolean") return true;
|
|
13247
|
+
return ["1", "true", "yes", "on", "0", "false", "no", "off"].includes(
|
|
13248
|
+
String(value).trim().toLowerCase()
|
|
13249
|
+
);
|
|
13250
|
+
}
|
|
13251
|
+
return true;
|
|
13252
|
+
}
|
|
13253
|
+
function strictTransform(value, valueType, location) {
|
|
13254
|
+
if (value === void 0) return;
|
|
13255
|
+
const transform = strictObject(
|
|
13256
|
+
value,
|
|
13257
|
+
location,
|
|
13258
|
+
/* @__PURE__ */ new Set(["trim", "lowercase", "map", "scale", "default"])
|
|
13259
|
+
);
|
|
13260
|
+
optionalType(transform.trim, "boolean", `${location}.trim`);
|
|
13261
|
+
optionalType(transform.lowercase, "boolean", `${location}.lowercase`);
|
|
13262
|
+
if (transform.map !== void 0) {
|
|
13263
|
+
if (!transform.map || typeof transform.map !== "object" || Array.isArray(transform.map))
|
|
13264
|
+
throw new Error(`${location}.map must be an object`);
|
|
13265
|
+
const map = transform.map;
|
|
13266
|
+
const entries = Object.entries(map);
|
|
13267
|
+
if (entries.length > runtime_settings_trigger_constants_default.maxTransformMapEntries)
|
|
13268
|
+
throw new Error(
|
|
13269
|
+
`${location}.map must contain at most ${runtime_settings_trigger_constants_default.maxTransformMapEntries} entries`
|
|
13270
|
+
);
|
|
13271
|
+
for (const [key, mappedValue] of entries) {
|
|
13272
|
+
if (key.length > runtime_settings_trigger_constants_default.maxTransformMapKeyChars)
|
|
13273
|
+
throw new Error(
|
|
13274
|
+
`${location}.map keys must be at most ${runtime_settings_trigger_constants_default.maxTransformMapKeyChars} characters`
|
|
13275
|
+
);
|
|
13276
|
+
if (typeof mappedValue !== "string" || mappedValue.length > runtime_settings_trigger_constants_default.maxTransformMapValueChars)
|
|
13277
|
+
throw new Error(
|
|
13278
|
+
`${location}.map.${key} must be a string of at most ${runtime_settings_trigger_constants_default.maxTransformMapValueChars} characters`
|
|
13279
|
+
);
|
|
13280
|
+
}
|
|
13281
|
+
}
|
|
13282
|
+
if (transform.scale !== void 0 && (typeof transform.scale !== "number" || !Number.isFinite(transform.scale) || transform.scale === 0))
|
|
13283
|
+
throw new Error(`${location}.scale must be finite and non-zero`);
|
|
13284
|
+
if (transform.default !== void 0 && !["string", "number", "boolean"].includes(typeof transform.default))
|
|
13285
|
+
throw new Error(`${location}.default must be a string, number, or boolean`);
|
|
13286
|
+
if (transform.default !== void 0 && !defaultMatchesValueType(transform.default, valueType))
|
|
13287
|
+
throw new Error(`${location}.default must coerce to the mapping valueType`);
|
|
13288
|
+
}
|
|
12429
13289
|
function strictMapping(value, location, revenueProperty) {
|
|
12430
13290
|
const keys = /* @__PURE__ */ new Set([
|
|
12431
13291
|
"from",
|
|
12432
13292
|
"to",
|
|
12433
13293
|
"valueType",
|
|
13294
|
+
"transform",
|
|
12434
13295
|
...revenueProperty ? [] : ["ttlDays", "persist"]
|
|
12435
13296
|
]);
|
|
12436
13297
|
const mapping = strictObject(value, location, keys);
|
|
12437
|
-
|
|
13298
|
+
strictSource(mapping.from, `${location}.from`, revenueProperty, true);
|
|
12438
13299
|
strictPath(mapping.to, `${location}.to`, true);
|
|
12439
13300
|
if (mapping.valueType !== void 0 && !valueTypes.has(String(mapping.valueType)))
|
|
12440
13301
|
throw new Error(`${location}.valueType must be string, number, boolean, string_array, or json`);
|
|
13302
|
+
strictTransform(mapping.transform, mapping.valueType, `${location}.transform`);
|
|
12441
13303
|
if (!revenueProperty) {
|
|
12442
13304
|
optionalType(mapping.ttlDays, "number", `${location}.ttlDays`);
|
|
12443
13305
|
optionalType(mapping.persist, "boolean", `${location}.persist`);
|
|
12444
13306
|
}
|
|
12445
13307
|
}
|
|
13308
|
+
function strictWhen(value, location) {
|
|
13309
|
+
if (value === void 0) return;
|
|
13310
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxWhenEntries) {
|
|
13311
|
+
throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxWhenEntries} entries`);
|
|
13312
|
+
}
|
|
13313
|
+
value.forEach((entry, index) => {
|
|
13314
|
+
const entryLocation = `${location}[${index}]`;
|
|
13315
|
+
const condition = strictObject(entry, entryLocation, /* @__PURE__ */ new Set(["path", "op", "value"]));
|
|
13316
|
+
strictPath(condition.path, `${entryLocation}.path`, true);
|
|
13317
|
+
if (typeof condition.op !== "string" || !whenOps.has(condition.op)) {
|
|
13318
|
+
throw new Error(
|
|
13319
|
+
`${entryLocation}.op must be ${runtime_settings_trigger_constants_default.whenOps.slice(0, -1).join(", ")}, or ${runtime_settings_trigger_constants_default.whenOps.at(-1)}`
|
|
13320
|
+
);
|
|
13321
|
+
}
|
|
13322
|
+
const hasValue = Object.prototype.hasOwnProperty.call(condition, "value");
|
|
13323
|
+
if (condition.op === "exists" || condition.op === "not_exists") {
|
|
13324
|
+
if (hasValue) throw new Error(`${entryLocation}.value is forbidden for ${condition.op}`);
|
|
13325
|
+
return;
|
|
13326
|
+
}
|
|
13327
|
+
if (!hasValue) throw new Error(`${entryLocation}.value is required for ${condition.op}`);
|
|
13328
|
+
if (condition.op === "gt" || condition.op === "lt") {
|
|
13329
|
+
if (typeof condition.value !== "number" || !Number.isFinite(condition.value)) {
|
|
13330
|
+
throw new Error(`${entryLocation}.value must be a number for ${condition.op}`);
|
|
13331
|
+
}
|
|
13332
|
+
return;
|
|
13333
|
+
}
|
|
13334
|
+
if (!["string", "number", "boolean"].includes(typeof condition.value)) {
|
|
13335
|
+
throw new Error(`${entryLocation}.value must be a string, number, or boolean`);
|
|
13336
|
+
}
|
|
13337
|
+
if (typeof condition.value === "number" && !Number.isFinite(condition.value)) {
|
|
13338
|
+
throw new Error(`${entryLocation}.value must be finite`);
|
|
13339
|
+
}
|
|
13340
|
+
});
|
|
13341
|
+
}
|
|
12446
13342
|
function strictTriggers(value) {
|
|
12447
|
-
if (!Array.isArray(value) || value.length >
|
|
12448
|
-
throw new Error(
|
|
13343
|
+
if (!Array.isArray(value) || value.length > runtime_settings_trigger_constants_default.maxTriggers)
|
|
13344
|
+
throw new Error(
|
|
13345
|
+
`trigger file must contain an array of at most ${runtime_settings_trigger_constants_default.maxTriggers} triggers`
|
|
13346
|
+
);
|
|
12449
13347
|
return value.map((item, index) => {
|
|
12450
13348
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
12451
13349
|
throw new Error(`trigger[${index}] must be an object`);
|
|
@@ -12458,6 +13356,7 @@ function strictTriggers(value) {
|
|
|
12458
13356
|
optionalType(record2[key], "boolean", `trigger[${index}].${key}`);
|
|
12459
13357
|
for (const key of ["dataLayerName", "trackEventName"])
|
|
12460
13358
|
optionalType(record2[key], "string", `trigger[${index}].${key}`);
|
|
13359
|
+
strictWhen(record2.when, `trigger[${index}].when`);
|
|
12461
13360
|
if (record2.trackRevenue !== void 0) {
|
|
12462
13361
|
const revenue = strictObject(
|
|
12463
13362
|
record2.trackRevenue,
|
|
@@ -12474,7 +13373,7 @@ function strictTriggers(value) {
|
|
|
12474
13373
|
);
|
|
12475
13374
|
optionalType(revenue.enabled, "boolean", `trigger[${index}].trackRevenue.enabled`);
|
|
12476
13375
|
for (const key of ["revenueFrom", "orderIdFrom", "currencyFrom", "costFrom", "profitFrom"])
|
|
12477
|
-
|
|
13376
|
+
strictSource(revenue[key], `trigger[${index}].trackRevenue.${key}`, false);
|
|
12478
13377
|
if (revenue.properties !== void 0) {
|
|
12479
13378
|
if (!Array.isArray(revenue.properties))
|
|
12480
13379
|
throw new Error(`trigger[${index}].trackRevenue.properties must be an array`);
|
|
@@ -12490,13 +13389,28 @@ function strictTriggers(value) {
|
|
|
12490
13389
|
(mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].mappings[${mappingIndex}]`, false)
|
|
12491
13390
|
);
|
|
12492
13391
|
}
|
|
13392
|
+
if (record2.properties !== void 0) {
|
|
13393
|
+
if (!Array.isArray(record2.properties))
|
|
13394
|
+
throw new Error(`trigger[${index}].properties must be an array`);
|
|
13395
|
+
record2.properties.forEach(
|
|
13396
|
+
(mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].properties[${mappingIndex}]`, true)
|
|
13397
|
+
);
|
|
13398
|
+
}
|
|
12493
13399
|
return record2;
|
|
12494
13400
|
});
|
|
12495
13401
|
}
|
|
12496
|
-
|
|
13402
|
+
function writeWarnings(result) {
|
|
13403
|
+
const warnings = result.warnings;
|
|
13404
|
+
if (!Array.isArray(warnings)) return;
|
|
13405
|
+
for (const warning of warnings) {
|
|
13406
|
+
if (typeof warning === "string") process.stderr.write(`WARNING: ${warning}
|
|
13407
|
+
`);
|
|
13408
|
+
}
|
|
13409
|
+
}
|
|
13410
|
+
var uuidPattern2 = /^[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
13411
|
async function identify(command, requestedShop) {
|
|
12498
13412
|
let credentials = requireCredentials();
|
|
12499
|
-
if (credentials.credentialKind === "user" && !
|
|
13413
|
+
if (credentials.credentialKind === "user" && !uuidPattern2.test(requestedShop)) {
|
|
12500
13414
|
const discoveryCredentials = {
|
|
12501
13415
|
...credentials,
|
|
12502
13416
|
resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
|
|
@@ -12569,7 +13483,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
|
|
|
12569
13483
|
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
13484
|
let triggers;
|
|
12571
13485
|
try {
|
|
12572
|
-
triggers = strictTriggers(JSON.parse(
|
|
13486
|
+
triggers = strictTriggers(JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.file), "utf8")));
|
|
12573
13487
|
} catch (error) {
|
|
12574
13488
|
fail(
|
|
12575
13489
|
this,
|
|
@@ -12584,20 +13498,19 @@ var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger arra
|
|
|
12584
13498
|
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12585
13499
|
...data
|
|
12586
13500
|
};
|
|
12587
|
-
output(
|
|
12588
|
-
|
|
12589
|
-
result,
|
|
12590
|
-
() => process.stdout.write(
|
|
13501
|
+
output(this, result, () => {
|
|
13502
|
+
process.stdout.write(
|
|
12591
13503
|
`TRIGGERS_SET shop_id=${identity.shop_id} domain=${identity.domain} updated_at=${result.updated_at} count=${triggers.length}
|
|
12592
13504
|
`
|
|
12593
|
-
)
|
|
12594
|
-
|
|
13505
|
+
);
|
|
13506
|
+
writeWarnings(result);
|
|
13507
|
+
});
|
|
12595
13508
|
});
|
|
12596
13509
|
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
13510
|
let proposals;
|
|
12598
13511
|
let report;
|
|
12599
13512
|
try {
|
|
12600
|
-
report = JSON.parse(
|
|
13513
|
+
report = JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.fromFile), "utf8"));
|
|
12601
13514
|
proposals = proposeTriggers(report);
|
|
12602
13515
|
} catch (error) {
|
|
12603
13516
|
fail(this, error instanceof Error ? error.message : String(error), 2);
|
|
@@ -12656,14 +13569,13 @@ var recipe = new Command("apply-recipe").argument("<recipeId>").requiredOption("
|
|
|
12656
13569
|
domain: identity.domain,
|
|
12657
13570
|
applied_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12658
13571
|
};
|
|
12659
|
-
output(
|
|
12660
|
-
|
|
12661
|
-
result,
|
|
12662
|
-
() => process.stdout.write(
|
|
13572
|
+
output(this, result, () => {
|
|
13573
|
+
process.stdout.write(
|
|
12663
13574
|
`TRIGGERS_APPLIED shop_id=${identity.shop_id} domain=${identity.domain} applied_at=${result.applied_at} recipe=${recipeId}
|
|
12664
13575
|
`
|
|
12665
|
-
)
|
|
12666
|
-
|
|
13576
|
+
);
|
|
13577
|
+
writeWarnings(result);
|
|
13578
|
+
});
|
|
12667
13579
|
});
|
|
12668
13580
|
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
13581
|
const hours = Number(opts.hours);
|
|
@@ -12750,11 +13662,11 @@ async function patchWithRetry(command, build) {
|
|
|
12750
13662
|
warn2(command, result.data.precompile);
|
|
12751
13663
|
return result.data;
|
|
12752
13664
|
}
|
|
12753
|
-
var list5 = new Command("list").alias("ls").description("List install domains").action(async function
|
|
13665
|
+
var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
|
|
12754
13666
|
const data = await load4(this);
|
|
12755
13667
|
output(this, data, () => printDomains(data));
|
|
12756
13668
|
});
|
|
12757
|
-
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function
|
|
13669
|
+
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
|
|
12758
13670
|
const added = domain.map(normalizeDomain);
|
|
12759
13671
|
const data = await patchWithRetry(
|
|
12760
13672
|
this,
|
|
@@ -12762,7 +13674,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
|
|
|
12762
13674
|
);
|
|
12763
13675
|
output(this, data, () => printDomains(data));
|
|
12764
13676
|
});
|
|
12765
|
-
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function
|
|
13677
|
+
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
|
|
12766
13678
|
const removeSet = new Set(domain.map(normalizeDomain));
|
|
12767
13679
|
const data = await patchWithRetry(
|
|
12768
13680
|
this,
|
|
@@ -12770,7 +13682,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
|
|
|
12770
13682
|
);
|
|
12771
13683
|
output(this, data, () => printDomains(data));
|
|
12772
13684
|
});
|
|
12773
|
-
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function
|
|
13685
|
+
var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
|
|
12774
13686
|
const replacement = domain.map(normalizeDomain);
|
|
12775
13687
|
const data = await patchWithRetry(this, () => replacement);
|
|
12776
13688
|
output(this, data, () => printDomains(data));
|
|
@@ -12862,14 +13774,14 @@ async function patchWithRetry2(command, build) {
|
|
|
12862
13774
|
);
|
|
12863
13775
|
return result.data;
|
|
12864
13776
|
}
|
|
12865
|
-
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function
|
|
13777
|
+
var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
|
|
12866
13778
|
const data = await load5(this);
|
|
12867
13779
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12868
13780
|
});
|
|
12869
13781
|
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
13782
|
"--signed",
|
|
12871
13783
|
"Require signed handover tokens (the default; unsigning is a platform-support action)"
|
|
12872
|
-
).action(async function
|
|
13784
|
+
).action(async function action53(opts) {
|
|
12873
13785
|
const domains = opts.domain.map(normalizeDomain2);
|
|
12874
13786
|
if (domains.some((domain) => !domain))
|
|
12875
13787
|
fail(
|
|
@@ -12889,14 +13801,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
|
|
|
12889
13801
|
}));
|
|
12890
13802
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12891
13803
|
});
|
|
12892
|
-
var off2 = new Command("off").description("Disable cross-domain handover").action(async function
|
|
13804
|
+
var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
|
|
12893
13805
|
const data = await patchWithRetry2(this, (current) => ({
|
|
12894
13806
|
externalTracking: { domains: [], signed: true },
|
|
12895
13807
|
ifRevision: current.revision
|
|
12896
13808
|
}));
|
|
12897
13809
|
output(this, data, () => printSettings(data.externalTracking));
|
|
12898
13810
|
});
|
|
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
|
|
13811
|
+
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
13812
|
const config = await load5(this);
|
|
12901
13813
|
const target = normalizeDomain2(opts.toDomain);
|
|
12902
13814
|
if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
|
|
@@ -13939,7 +14851,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
|
|
|
13939
14851
|
).option("--check <id>", "Run one check (for example purchase_capture)").option(
|
|
13940
14852
|
"--worker <url>",
|
|
13941
14853
|
`Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
|
|
13942
|
-
).action(async function
|
|
14854
|
+
).action(async function action56(options) {
|
|
13943
14855
|
const data = await runDoctor(options);
|
|
13944
14856
|
if (options.check && data.checks.length === 0)
|
|
13945
14857
|
fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
|
|
@@ -13961,7 +14873,7 @@ function printDoctorReport(checks) {
|
|
|
13961
14873
|
var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
|
|
13962
14874
|
"--since <date>",
|
|
13963
14875
|
"Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
|
|
13964
|
-
).action(async function
|
|
14876
|
+
).action(async function action57(opts) {
|
|
13965
14877
|
if (opts.since !== void 0 && parseDay(opts.since) === null)
|
|
13966
14878
|
fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
|
|
13967
14879
|
const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
|
|
@@ -14021,7 +14933,7 @@ function analyticsSubcommand(name, description, defaultDays) {
|
|
|
14021
14933
|
command.option("--planning-days <n>", "Planning window in days", "180");
|
|
14022
14934
|
command.option("--limit <n>", "Maximum page rows", "50");
|
|
14023
14935
|
}
|
|
14024
|
-
return command.action(async function
|
|
14936
|
+
return command.action(async function action71(opts) {
|
|
14025
14937
|
const result = await getAnalytics(requireCredentials(), name, {
|
|
14026
14938
|
days: positiveInteger(this, opts.days, "--days"),
|
|
14027
14939
|
planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
|
|
@@ -14041,7 +14953,7 @@ function urlsCommand() {
|
|
|
14041
14953
|
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
14954
|
"--match <regex>",
|
|
14043
14955
|
"Client-side regex filter; exhausts inventory pages before filtering"
|
|
14044
|
-
).option("--limit <n>", "Maximum rows to print", "50").action(async function
|
|
14956
|
+
).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
|
|
14045
14957
|
const creds = requireCredentials();
|
|
14046
14958
|
const days = positiveInteger(this, opts.days, "--days") ?? 14;
|
|
14047
14959
|
const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
|
|
@@ -14230,15 +15142,15 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
14230
15142
|
`Response from ${hostname} uses unsupported content encoding "${encoding}"`
|
|
14231
15143
|
)
|
|
14232
15144
|
);
|
|
14233
|
-
return new Promise((
|
|
15145
|
+
return new Promise((resolve2, reject) => {
|
|
14234
15146
|
const stream = decoderFor(encoding);
|
|
14235
15147
|
const chunks = [];
|
|
14236
15148
|
let bytes = 0;
|
|
14237
15149
|
let settled = false;
|
|
14238
|
-
const settle = (
|
|
15150
|
+
const settle = (action71) => {
|
|
14239
15151
|
if (settled) return;
|
|
14240
15152
|
settled = true;
|
|
14241
|
-
|
|
15153
|
+
action71();
|
|
14242
15154
|
};
|
|
14243
15155
|
stream.on("data", (chunk) => {
|
|
14244
15156
|
if (settled) return;
|
|
@@ -14250,7 +15162,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
14250
15162
|
}
|
|
14251
15163
|
chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
|
|
14252
15164
|
stream.destroy();
|
|
14253
|
-
settle(() =>
|
|
15165
|
+
settle(() => resolve2({ body: Buffer.concat(chunks), truncated: true }));
|
|
14254
15166
|
return;
|
|
14255
15167
|
}
|
|
14256
15168
|
chunks.push(chunk);
|
|
@@ -14260,7 +15172,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
14260
15172
|
"error",
|
|
14261
15173
|
(error) => settle(() => {
|
|
14262
15174
|
if (limits.onExceed === "truncate" && inputTruncated && chunks.length > 0)
|
|
14263
|
-
|
|
15175
|
+
resolve2({ body: Buffer.concat(chunks), truncated: true });
|
|
14264
15176
|
else
|
|
14265
15177
|
reject(
|
|
14266
15178
|
new BodyDecodeError(
|
|
@@ -14271,7 +15183,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
|
|
|
14271
15183
|
);
|
|
14272
15184
|
stream.on(
|
|
14273
15185
|
"end",
|
|
14274
|
-
() => settle(() =>
|
|
15186
|
+
() => settle(() => resolve2({ body: Buffer.concat(chunks), truncated: false }))
|
|
14275
15187
|
);
|
|
14276
15188
|
stream.end(raw);
|
|
14277
15189
|
});
|
|
@@ -14298,7 +15210,7 @@ function responseFromIncoming(res, body5, decoded) {
|
|
|
14298
15210
|
});
|
|
14299
15211
|
}
|
|
14300
15212
|
function pinnedFetch(url, addresses, limits) {
|
|
14301
|
-
return new Promise((
|
|
15213
|
+
return new Promise((resolve2, reject) => {
|
|
14302
15214
|
const send3 = url.protocol === "https:" ? import_node_https.request : import_node_http2.request;
|
|
14303
15215
|
const pinnedLookup = (_hostname, options, callback) => {
|
|
14304
15216
|
if (options?.all)
|
|
@@ -14309,11 +15221,11 @@ function pinnedFetch(url, addresses, limits) {
|
|
|
14309
15221
|
else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
|
|
14310
15222
|
};
|
|
14311
15223
|
let settled = false;
|
|
14312
|
-
const settle = (
|
|
15224
|
+
const settle = (action71) => {
|
|
14313
15225
|
if (settled) return;
|
|
14314
15226
|
settled = true;
|
|
14315
15227
|
clearTimeout(deadline);
|
|
14316
|
-
|
|
15228
|
+
action71();
|
|
14317
15229
|
};
|
|
14318
15230
|
const req = send3(
|
|
14319
15231
|
url,
|
|
@@ -14345,7 +15257,7 @@ function pinnedFetch(url, addresses, limits) {
|
|
|
14345
15257
|
chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
|
|
14346
15258
|
req.destroy();
|
|
14347
15259
|
settle(
|
|
14348
|
-
() => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(
|
|
15260
|
+
() => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve2, reject)
|
|
14349
15261
|
);
|
|
14350
15262
|
return;
|
|
14351
15263
|
}
|
|
@@ -14360,7 +15272,7 @@ function pinnedFetch(url, addresses, limits) {
|
|
|
14360
15272
|
reject(new Error(`No HTTP status from ${url.hostname}`));
|
|
14361
15273
|
return;
|
|
14362
15274
|
}
|
|
14363
|
-
finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(
|
|
15275
|
+
finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve2, reject);
|
|
14364
15276
|
})
|
|
14365
15277
|
);
|
|
14366
15278
|
}
|
|
@@ -14648,7 +15560,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
|
|
|
14648
15560
|
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
15561
|
"--worker-url <url>",
|
|
14650
15562
|
"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
|
|
15563
|
+
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
|
|
14652
15564
|
const creds = requireCredentials();
|
|
14653
15565
|
const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
|
|
14654
15566
|
if (!shopId)
|
|
@@ -14792,8 +15704,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
|
|
|
14792
15704
|
|
|
14793
15705
|
// src/commands/flags.ts
|
|
14794
15706
|
init_source();
|
|
14795
|
-
var
|
|
14796
|
-
var
|
|
15707
|
+
var import_node_fs17 = __toESM(require("node:fs"));
|
|
15708
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
14797
15709
|
|
|
14798
15710
|
// src/edge-status.ts
|
|
14799
15711
|
function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
|
|
@@ -14835,7 +15747,7 @@ function body(command, value, opts) {
|
|
|
14835
15747
|
let parsed = {};
|
|
14836
15748
|
if (value) {
|
|
14837
15749
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
14838
|
-
const text = value.startsWith("@") ||
|
|
15750
|
+
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;
|
|
14839
15751
|
try {
|
|
14840
15752
|
parsed = JSON.parse(text);
|
|
14841
15753
|
} catch (e) {
|
|
@@ -14948,11 +15860,11 @@ function write(name) {
|
|
|
14948
15860
|
});
|
|
14949
15861
|
return c;
|
|
14950
15862
|
}
|
|
14951
|
-
function lifecycle(
|
|
14952
|
-
return new Command(
|
|
14953
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind, id,
|
|
15863
|
+
function lifecycle(action71) {
|
|
15864
|
+
return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
|
|
15865
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
|
|
14954
15866
|
if (!r.ok || !r.data)
|
|
14955
|
-
fail(this, r.error ?? `Failed to ${
|
|
15867
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
14956
15868
|
output(this, { flag: r.data }, () => detail(r.data));
|
|
14957
15869
|
});
|
|
14958
15870
|
}
|
|
@@ -14968,8 +15880,8 @@ Examples:
|
|
|
14968
15880
|
);
|
|
14969
15881
|
|
|
14970
15882
|
// src/commands/backlog.ts
|
|
14971
|
-
var
|
|
14972
|
-
var
|
|
15883
|
+
var import_node_fs18 = __toESM(require("node:fs"));
|
|
15884
|
+
var import_node_path16 = __toESM(require("node:path"));
|
|
14973
15885
|
init_source();
|
|
14974
15886
|
var kind2 = "backlog";
|
|
14975
15887
|
var singular = "backlog item";
|
|
@@ -14977,7 +15889,7 @@ function parse(command, value) {
|
|
|
14977
15889
|
if (!value) return {};
|
|
14978
15890
|
try {
|
|
14979
15891
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
14980
|
-
const text = value.startsWith("@") ||
|
|
15892
|
+
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;
|
|
14981
15893
|
const body5 = JSON.parse(text);
|
|
14982
15894
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
|
|
14983
15895
|
throw new Error("body must be an object");
|
|
@@ -15027,24 +15939,24 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
|
|
|
15027
15939
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
15028
15940
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
15029
15941
|
});
|
|
15030
|
-
function write2(
|
|
15031
|
-
const c = new Command(
|
|
15032
|
-
`${
|
|
15942
|
+
function write2(action71) {
|
|
15943
|
+
const c = new Command(action71).description(
|
|
15944
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular}`
|
|
15033
15945
|
);
|
|
15034
|
-
if (
|
|
15946
|
+
if (action71 === "update") c.argument("<id>", `${singular} ID`);
|
|
15035
15947
|
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
15036
15948
|
"--file <path>",
|
|
15037
15949
|
"JSON body file"
|
|
15038
15950
|
);
|
|
15039
15951
|
c.action(async function(...args) {
|
|
15040
15952
|
const o = this.opts();
|
|
15041
|
-
const id =
|
|
15042
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
15953
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
15954
|
+
const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15043
15955
|
const body5 = parse(this, raw);
|
|
15044
15956
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
15045
|
-
const r =
|
|
15957
|
+
const r = action71 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
|
|
15046
15958
|
if (!r.ok || !r.data)
|
|
15047
|
-
fail(this, r.error ?? `Failed to ${
|
|
15959
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15048
15960
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
15049
15961
|
});
|
|
15050
15962
|
return c;
|
|
@@ -15077,7 +15989,7 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
|
|
|
15077
15989
|
const readline3 = await import("node:readline");
|
|
15078
15990
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
15079
15991
|
const answer = await new Promise(
|
|
15080
|
-
(
|
|
15992
|
+
(resolve2) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve2)
|
|
15081
15993
|
);
|
|
15082
15994
|
rl.close();
|
|
15083
15995
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -15093,8 +16005,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
|
|
|
15093
16005
|
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
16006
|
|
|
15095
16007
|
// src/commands/signals.ts
|
|
15096
|
-
var
|
|
15097
|
-
var
|
|
16008
|
+
var import_node_fs19 = __toESM(require("node:fs"));
|
|
16009
|
+
var import_node_path17 = __toESM(require("node:path"));
|
|
15098
16010
|
init_source();
|
|
15099
16011
|
var kind3 = "signals";
|
|
15100
16012
|
var singular2 = "signal";
|
|
@@ -15102,7 +16014,7 @@ function parse2(command, value) {
|
|
|
15102
16014
|
if (!value) return {};
|
|
15103
16015
|
try {
|
|
15104
16016
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15105
|
-
const text = value.startsWith("@") ||
|
|
16017
|
+
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;
|
|
15106
16018
|
const body5 = JSON.parse(text);
|
|
15107
16019
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
|
|
15108
16020
|
throw new Error("body must be an object");
|
|
@@ -15152,24 +16064,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
|
|
|
15152
16064
|
fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
15153
16065
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
15154
16066
|
});
|
|
15155
|
-
function write3(
|
|
15156
|
-
const c = new Command(
|
|
15157
|
-
`${
|
|
16067
|
+
function write3(action71) {
|
|
16068
|
+
const c = new Command(action71).description(
|
|
16069
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular2}`
|
|
15158
16070
|
);
|
|
15159
|
-
if (
|
|
16071
|
+
if (action71 === "update") c.argument("<id>", `${singular2} ID`);
|
|
15160
16072
|
c.argument("[json]", "Inline JSON, @file, or file path").option(
|
|
15161
16073
|
"--file <path>",
|
|
15162
16074
|
"JSON body file"
|
|
15163
16075
|
);
|
|
15164
16076
|
c.action(async function(...args) {
|
|
15165
16077
|
const o = this.opts();
|
|
15166
|
-
const id =
|
|
15167
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
16078
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
16079
|
+
const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15168
16080
|
const body5 = parse2(this, raw);
|
|
15169
16081
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
15170
|
-
const r =
|
|
16082
|
+
const r = action71 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
|
|
15171
16083
|
if (!r.ok || !r.data)
|
|
15172
|
-
fail(this, r.error ?? `Failed to ${
|
|
16084
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15173
16085
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
15174
16086
|
});
|
|
15175
16087
|
return c;
|
|
@@ -15184,14 +16096,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
|
|
|
15184
16096
|
|
|
15185
16097
|
// src/commands/personalizations.ts
|
|
15186
16098
|
init_source();
|
|
15187
|
-
var
|
|
15188
|
-
var
|
|
16099
|
+
var import_node_fs20 = __toESM(require("node:fs"));
|
|
16100
|
+
var import_node_path18 = __toESM(require("node:path"));
|
|
15189
16101
|
var kind4 = "personalizations";
|
|
15190
16102
|
function body2(command, value, opts) {
|
|
15191
16103
|
let parsed = {};
|
|
15192
16104
|
if (value) {
|
|
15193
16105
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15194
|
-
const text = value.startsWith("@") ||
|
|
16106
|
+
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;
|
|
15195
16107
|
try {
|
|
15196
16108
|
parsed = JSON.parse(text);
|
|
15197
16109
|
} catch (e) {
|
|
@@ -15261,11 +16173,11 @@ function write4(name) {
|
|
|
15261
16173
|
});
|
|
15262
16174
|
return c;
|
|
15263
16175
|
}
|
|
15264
|
-
function lifecycle2(
|
|
15265
|
-
return new Command(
|
|
15266
|
-
const r = await transitionManagedDelivery(requireCredentials(), kind4, id,
|
|
16176
|
+
function lifecycle2(action71) {
|
|
16177
|
+
return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
|
|
16178
|
+
const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
|
|
15267
16179
|
if (!r.ok || !r.data)
|
|
15268
|
-
fail(this, r.error ?? `Failed to ${
|
|
16180
|
+
fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
|
|
15269
16181
|
output(this, { personalization: r.data }, () => detail4(r.data));
|
|
15270
16182
|
});
|
|
15271
16183
|
}
|
|
@@ -15273,13 +16185,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
|
|
|
15273
16185
|
|
|
15274
16186
|
// src/commands/targeting-resources.ts
|
|
15275
16187
|
init_source();
|
|
15276
|
-
var
|
|
15277
|
-
var
|
|
16188
|
+
var import_node_fs21 = __toESM(require("node:fs"));
|
|
16189
|
+
var import_node_path19 = __toESM(require("node:path"));
|
|
15278
16190
|
function body3(command, value) {
|
|
15279
16191
|
if (!value) return {};
|
|
15280
16192
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
15281
16193
|
try {
|
|
15282
|
-
const text = value.startsWith("@") ||
|
|
16194
|
+
const text = value.startsWith("@") || import_node_fs21.default.existsSync(source) ? import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), source), "utf8") : value;
|
|
15283
16195
|
return JSON.parse(text);
|
|
15284
16196
|
} catch (error) {
|
|
15285
16197
|
fail(
|
|
@@ -15347,21 +16259,21 @@ function targetingResourceCommand(kind5) {
|
|
|
15347
16259
|
);
|
|
15348
16260
|
})
|
|
15349
16261
|
);
|
|
15350
|
-
for (const
|
|
15351
|
-
const command = new Command(
|
|
15352
|
-
`${
|
|
16262
|
+
for (const action71 of ["create", "update"]) {
|
|
16263
|
+
const command = new Command(action71).description(
|
|
16264
|
+
`${action71 === "create" ? "Create" : "Update"} a ${singular3}`
|
|
15353
16265
|
);
|
|
15354
|
-
if (
|
|
16266
|
+
if (action71 === "update") command.argument("<id>");
|
|
15355
16267
|
command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
|
|
15356
16268
|
const opts = this.opts();
|
|
15357
|
-
const id =
|
|
15358
|
-
const value = opts.file ? `@${opts.file}` : String(args[
|
|
16269
|
+
const id = action71 === "update" ? String(args[0]) : void 0;
|
|
16270
|
+
const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
|
|
15359
16271
|
const requestBody = body3(this, value);
|
|
15360
|
-
if (
|
|
16272
|
+
if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
|
|
15361
16273
|
fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
|
|
15362
16274
|
const response = await requestTargetingResource(
|
|
15363
16275
|
requireCredentials(),
|
|
15364
|
-
|
|
16276
|
+
action71 === "create" ? "POST" : "PATCH",
|
|
15365
16277
|
kind5,
|
|
15366
16278
|
id,
|
|
15367
16279
|
requestBody
|
|
@@ -15369,7 +16281,7 @@ function targetingResourceCommand(kind5) {
|
|
|
15369
16281
|
if (!response.ok || !response.data)
|
|
15370
16282
|
fail(
|
|
15371
16283
|
this,
|
|
15372
|
-
response.error ?? `Failed to ${
|
|
16284
|
+
response.error ?? `Failed to ${action71}`,
|
|
15373
16285
|
classifyStatus(response.status),
|
|
15374
16286
|
response.errorDetails
|
|
15375
16287
|
);
|
|
@@ -15378,7 +16290,7 @@ function targetingResourceCommand(kind5) {
|
|
|
15378
16290
|
{
|
|
15379
16291
|
[kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
|
|
15380
16292
|
},
|
|
15381
|
-
() => console.log(source_default.green(`${
|
|
16293
|
+
() => console.log(source_default.green(`${action71}d ${singular3}`))
|
|
15382
16294
|
);
|
|
15383
16295
|
});
|
|
15384
16296
|
root.addCommand(command);
|
|
@@ -15399,8 +16311,8 @@ function targetingResourceCommand(kind5) {
|
|
|
15399
16311
|
input: process.stdin,
|
|
15400
16312
|
output: process.stderr
|
|
15401
16313
|
});
|
|
15402
|
-
const answer = await new Promise((
|
|
15403
|
-
rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `),
|
|
16314
|
+
const answer = await new Promise((resolve2) => {
|
|
16315
|
+
rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve2);
|
|
15404
16316
|
});
|
|
15405
16317
|
rl.close();
|
|
15406
16318
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -15452,7 +16364,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
|
|
|
15452
16364
|
// src/commands/brand.ts
|
|
15453
16365
|
init_source();
|
|
15454
16366
|
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
|
|
16367
|
+
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
|
|
15456
16368
|
const creds = requireCredentials();
|
|
15457
16369
|
const res = await getBrandContext(creds);
|
|
15458
16370
|
if (res.status === 404) {
|
|
@@ -15485,7 +16397,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
|
|
|
15485
16397
|
|
|
15486
16398
|
// src/commands/targeting.ts
|
|
15487
16399
|
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
|
|
16400
|
+
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
16401
|
const creds = requireCredentials();
|
|
15490
16402
|
const res = await testTargeting(creds, url, { status: opts.status });
|
|
15491
16403
|
if (!res.ok || !res.data) {
|
|
@@ -15530,13 +16442,21 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
|
|
|
15530
16442
|
|
|
15531
16443
|
// src/commands/sos.ts
|
|
15532
16444
|
init_source();
|
|
15533
|
-
var
|
|
15534
|
-
var
|
|
15535
|
-
var SOS_TYPES = /* @__PURE__ */ new Set([
|
|
16445
|
+
var import_node_fs22 = __toESM(require("node:fs"));
|
|
16446
|
+
var import_node_path20 = __toESM(require("node:path"));
|
|
16447
|
+
var SOS_TYPES = /* @__PURE__ */ new Set([
|
|
16448
|
+
"full_handover",
|
|
16449
|
+
"design",
|
|
16450
|
+
"development",
|
|
16451
|
+
"qa",
|
|
16452
|
+
"config",
|
|
16453
|
+
"bug",
|
|
16454
|
+
"help"
|
|
16455
|
+
]);
|
|
15536
16456
|
function readText(value) {
|
|
15537
16457
|
if (!value) return void 0;
|
|
15538
16458
|
if (!value.startsWith("@")) return value;
|
|
15539
|
-
return
|
|
16459
|
+
return import_node_fs22.default.readFileSync(import_node_path20.default.resolve(process.cwd(), value.slice(1)), "utf8");
|
|
15540
16460
|
}
|
|
15541
16461
|
function printTicket(ticket, reused) {
|
|
15542
16462
|
console.log();
|
|
@@ -15547,11 +16467,26 @@ function printTicket(ticket, reused) {
|
|
|
15547
16467
|
console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
|
|
15548
16468
|
console.log();
|
|
15549
16469
|
}
|
|
15550
|
-
|
|
16470
|
+
function printCreatedTicket(result) {
|
|
16471
|
+
if ("reused" in result) {
|
|
16472
|
+
printTicket(result.ticket, result.reused);
|
|
16473
|
+
return;
|
|
16474
|
+
}
|
|
16475
|
+
console.log();
|
|
16476
|
+
console.log(source_default.bold(result.deduped ? "Reused customer ticket" : "Created customer ticket"));
|
|
16477
|
+
console.log(` Reference: ${source_default.dim(result.ticket.reference)}`);
|
|
16478
|
+
console.log(` Status: ${result.ticket.status}`);
|
|
16479
|
+
console.log();
|
|
16480
|
+
}
|
|
16481
|
+
var create4 = new Command("create").description("Create an Apex SOS ticket").option(
|
|
16482
|
+
"--type <type>",
|
|
16483
|
+
"full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
|
|
16484
|
+
"qa"
|
|
16485
|
+
).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
16486
|
if (!SOS_TYPES.has(opts.type)) {
|
|
15552
16487
|
fail(
|
|
15553
16488
|
this,
|
|
15554
|
-
"Invalid SOS type. Use full_handover, design, development, qa, config, or
|
|
16489
|
+
"Invalid SOS type. Use full_handover, design, development, qa, config, bug, or help. Internal types require a DRIP account.",
|
|
15555
16490
|
3
|
|
15556
16491
|
);
|
|
15557
16492
|
}
|
|
@@ -15572,9 +16507,9 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
15572
16507
|
res.errorDetails
|
|
15573
16508
|
);
|
|
15574
16509
|
}
|
|
15575
|
-
output(this, res.data, () =>
|
|
16510
|
+
output(this, res.data, () => printCreatedTicket(res.data));
|
|
15576
16511
|
});
|
|
15577
|
-
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function
|
|
16512
|
+
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
|
|
15578
16513
|
const creds = requireCredentials();
|
|
15579
16514
|
const res = await listMySos(creds);
|
|
15580
16515
|
if (!res.ok || !res.data) {
|
|
@@ -15655,13 +16590,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
|
|
|
15655
16590
|
var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
|
|
15656
16591
|
|
|
15657
16592
|
// src/commands/shops.ts
|
|
15658
|
-
var
|
|
15659
|
-
var
|
|
16593
|
+
var import_node_fs23 = __toESM(require("node:fs"));
|
|
16594
|
+
var import_node_path21 = __toESM(require("node:path"));
|
|
15660
16595
|
function body4(command, raw) {
|
|
15661
16596
|
if (!raw) return {};
|
|
15662
16597
|
try {
|
|
15663
16598
|
const source = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
15664
|
-
const text = raw.startsWith("@") ||
|
|
16599
|
+
const text = raw.startsWith("@") || import_node_fs23.default.existsSync(source) ? import_node_fs23.default.readFileSync(import_node_path21.default.resolve(source), "utf8") : raw;
|
|
15665
16600
|
const value = JSON.parse(text);
|
|
15666
16601
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15667
16602
|
throw new Error("body must be an object");
|
|
@@ -15889,7 +16824,7 @@ function commandSchema(command) {
|
|
|
15889
16824
|
};
|
|
15890
16825
|
}
|
|
15891
16826
|
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
|
|
16827
|
+
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action71() {
|
|
15893
16828
|
const schema = commandSchema(program3);
|
|
15894
16829
|
output(this, schema, () => {
|
|
15895
16830
|
process.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
@@ -16042,7 +16977,7 @@ function parseContainer(source, id, dataLayerName = "dataLayer") {
|
|
|
16042
16977
|
vtpPaths,
|
|
16043
16978
|
ecommerceFrom: [...ecommerce][0] ?? null,
|
|
16044
16979
|
serverContainerUrl: [...serverUrls][0] ?? null,
|
|
16045
|
-
piiPaths: vtpPaths.filter((
|
|
16980
|
+
piiPaths: vtpPaths.filter((path24) => /email|phone|address|name/i.test(path24))
|
|
16046
16981
|
}
|
|
16047
16982
|
};
|
|
16048
16983
|
}
|
|
@@ -16101,6 +17036,145 @@ function minutesValue(value) {
|
|
|
16101
17036
|
throw new CliError("--minutes must be an integer between 1 and 20160", EXIT_CODES.validation);
|
|
16102
17037
|
return parsed;
|
|
16103
17038
|
}
|
|
17039
|
+
function isStableField(field) {
|
|
17040
|
+
return field.types.length > 0 && !field.type_collision;
|
|
17041
|
+
}
|
|
17042
|
+
function baseFieldTypes(field) {
|
|
17043
|
+
return new Set(field.types.map((type) => type.split(":")[0]));
|
|
17044
|
+
}
|
|
17045
|
+
function isContainerField(field) {
|
|
17046
|
+
const types = baseFieldTypes(field);
|
|
17047
|
+
return types.has("obj") || types.has("arr");
|
|
17048
|
+
}
|
|
17049
|
+
function hasSyntheticPlaceholder(path24) {
|
|
17050
|
+
return path24.split(".").map((segment) => segment.replace(/\[\]$/, "")).some((segment) => /^<[^>]+>$/.test(segment));
|
|
17051
|
+
}
|
|
17052
|
+
function ambiguousDottedPrefix(event, path24) {
|
|
17053
|
+
const segments = path24.split(".");
|
|
17054
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
17055
|
+
const prefix = segments.slice(0, index).join(".");
|
|
17056
|
+
const parent = event.fields.find((field) => field.path === prefix);
|
|
17057
|
+
if (!parent || !isContainerField(parent)) return prefix;
|
|
17058
|
+
}
|
|
17059
|
+
return void 0;
|
|
17060
|
+
}
|
|
17061
|
+
function dimensionEligibility(event, field) {
|
|
17062
|
+
const path24 = field.path;
|
|
17063
|
+
if (hasSyntheticPlaceholder(path24)) {
|
|
17064
|
+
return {
|
|
17065
|
+
eligible: false,
|
|
17066
|
+
message: `Path ${JSON.stringify(path24)} is a synthetic placeholder and cannot create a dimension. Choose an observed scalar path.`
|
|
17067
|
+
};
|
|
17068
|
+
}
|
|
17069
|
+
if (isContainerField(field)) {
|
|
17070
|
+
return {
|
|
17071
|
+
eligible: false,
|
|
17072
|
+
message: `Path ${JSON.stringify(path24)} is container-typed. Choose an observed scalar path.`
|
|
17073
|
+
};
|
|
17074
|
+
}
|
|
17075
|
+
if (path24.includes("[]")) {
|
|
17076
|
+
return {
|
|
17077
|
+
eligible: false,
|
|
17078
|
+
message: `Path ${JSON.stringify(path24)} includes an array-element segment and cannot create a reliable dimension. Choose a scalar path outside an array.`
|
|
17079
|
+
};
|
|
17080
|
+
}
|
|
17081
|
+
if (!isStableField(field)) {
|
|
17082
|
+
return {
|
|
17083
|
+
eligible: false,
|
|
17084
|
+
message: `Path ${JSON.stringify(path24)} does not have a stable observed type. Choose a path without a type collision.`
|
|
17085
|
+
};
|
|
17086
|
+
}
|
|
17087
|
+
if (!event.event) {
|
|
17088
|
+
return {
|
|
17089
|
+
eligible: false,
|
|
17090
|
+
message: `Path ${JSON.stringify(path24)} has no observed event name. Choose a path from a named event.`
|
|
17091
|
+
};
|
|
17092
|
+
}
|
|
17093
|
+
const ambiguousPrefix = ambiguousDottedPrefix(event, path24);
|
|
17094
|
+
if (ambiguousPrefix) {
|
|
17095
|
+
return {
|
|
17096
|
+
eligible: false,
|
|
17097
|
+
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.`
|
|
17098
|
+
};
|
|
17099
|
+
}
|
|
17100
|
+
return { eligible: true };
|
|
17101
|
+
}
|
|
17102
|
+
function isNumericField(field) {
|
|
17103
|
+
const numericTypes = /* @__PURE__ */ new Set(["num", "numstr", "numstr_comma"]);
|
|
17104
|
+
return field.types.length > 0 && field.types.every((type) => numericTypes.has(type.split(":")[0]));
|
|
17105
|
+
}
|
|
17106
|
+
function defaultDimensionLabel(path24) {
|
|
17107
|
+
const words = path24.split(/[._-]+/).filter(Boolean).join(" ");
|
|
17108
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : path24;
|
|
17109
|
+
}
|
|
17110
|
+
function quoteShellArgument(value) {
|
|
17111
|
+
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
17112
|
+
}
|
|
17113
|
+
function createDimensionInvocation(path24, eventName) {
|
|
17114
|
+
return `apex discover --create-dimension ${quoteShellArgument(path24)} --event ${quoteShellArgument(eventName)}`;
|
|
17115
|
+
}
|
|
17116
|
+
function resolveDimensionObservation(command, report, path24, eventName) {
|
|
17117
|
+
const matches = report.data_layer.flatMap(
|
|
17118
|
+
(event) => event.fields.filter((field) => field.path === path24).map((field) => ({ event, field }))
|
|
17119
|
+
);
|
|
17120
|
+
if (!matches.length) {
|
|
17121
|
+
fail(
|
|
17122
|
+
command,
|
|
17123
|
+
`Path ${JSON.stringify(path24)} was not found in the current discovery report. Run apex discover and copy an observed path exactly.`,
|
|
17124
|
+
EXIT_CODES.validation
|
|
17125
|
+
);
|
|
17126
|
+
}
|
|
17127
|
+
let selected2;
|
|
17128
|
+
if (eventName !== void 0) {
|
|
17129
|
+
const eventMatches = matches.filter((match2) => match2.event.event === eventName);
|
|
17130
|
+
if (!eventMatches.length) {
|
|
17131
|
+
fail(
|
|
17132
|
+
command,
|
|
17133
|
+
`Path ${JSON.stringify(path24)} was not observed on event ${JSON.stringify(eventName)}. Run apex discover and copy the event name exactly.`,
|
|
17134
|
+
EXIT_CODES.validation
|
|
17135
|
+
);
|
|
17136
|
+
}
|
|
17137
|
+
const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
|
|
17138
|
+
if (layers.length > 1) {
|
|
17139
|
+
fail(
|
|
17140
|
+
command,
|
|
17141
|
+
`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.`,
|
|
17142
|
+
EXIT_CODES.validation
|
|
17143
|
+
);
|
|
17144
|
+
}
|
|
17145
|
+
selected2 = eventMatches[0];
|
|
17146
|
+
} else {
|
|
17147
|
+
const namedEvents = [
|
|
17148
|
+
...new Set(matches.map((match2) => match2.event.event).filter(Boolean))
|
|
17149
|
+
].sort();
|
|
17150
|
+
if (namedEvents.length > 1) {
|
|
17151
|
+
fail(
|
|
17152
|
+
command,
|
|
17153
|
+
`Path ${JSON.stringify(path24)} was observed on multiple events: ${namedEvents.join(", ")}. Retry with --event <name>.`,
|
|
17154
|
+
EXIT_CODES.validation
|
|
17155
|
+
);
|
|
17156
|
+
}
|
|
17157
|
+
const eventMatches = namedEvents.length ? matches.filter((match2) => match2.event.event === namedEvents[0]) : matches;
|
|
17158
|
+
const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
|
|
17159
|
+
if (layers.length > 1) {
|
|
17160
|
+
fail(
|
|
17161
|
+
command,
|
|
17162
|
+
`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.`,
|
|
17163
|
+
EXIT_CODES.validation
|
|
17164
|
+
);
|
|
17165
|
+
}
|
|
17166
|
+
selected2 = eventMatches[0];
|
|
17167
|
+
}
|
|
17168
|
+
if (!selected2)
|
|
17169
|
+
fail(command, "No matching dimension observation was selected", EXIT_CODES.validation);
|
|
17170
|
+
const eligibility = dimensionEligibility(selected2.event, selected2.field);
|
|
17171
|
+
if (!eligibility.eligible) fail(command, eligibility.message, EXIT_CODES.validation);
|
|
17172
|
+
return {
|
|
17173
|
+
eventName: selected2.event.event,
|
|
17174
|
+
dlName: selected2.event.dlName,
|
|
17175
|
+
field: selected2.field
|
|
17176
|
+
};
|
|
17177
|
+
}
|
|
16104
17178
|
function renderDiscovery(report) {
|
|
16105
17179
|
const lines = [
|
|
16106
17180
|
`Observed tracking schema (${report.window.minutes} minutes)`,
|
|
@@ -16120,14 +17194,18 @@ function renderDiscovery(report) {
|
|
|
16120
17194
|
`
|
|
16121
17195
|
${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
17196
|
);
|
|
16123
|
-
for (const
|
|
17197
|
+
for (const path24 of event.paths)
|
|
16124
17198
|
lines.push(
|
|
16125
|
-
` URL ${
|
|
17199
|
+
` URL ${path24.path}: ${path24.pageviews} pageviews, ${path24.count} pushes, ${path24.pspReturn_count} PSP returns`
|
|
16126
17200
|
);
|
|
16127
|
-
for (const field of event.fields)
|
|
17201
|
+
for (const field of event.fields) {
|
|
16128
17202
|
lines.push(
|
|
16129
17203
|
`${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
17204
|
);
|
|
17205
|
+
if (dimensionEligibility(event, field).eligible) {
|
|
17206
|
+
lines.push(` Create dimension: ${createDimensionInvocation(field.path, event.event)}`);
|
|
17207
|
+
}
|
|
17208
|
+
}
|
|
16131
17209
|
}
|
|
16132
17210
|
if (report.trigger_diagnoses.length) {
|
|
16133
17211
|
lines.push("\n\u26A0 Trigger diagnoses");
|
|
@@ -16161,16 +17239,30 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
|
|
|
16161
17239
|
return `${lines.join("\n")}
|
|
16162
17240
|
`;
|
|
16163
17241
|
}
|
|
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
|
|
17242
|
+
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
17243
|
if (opts.gtm && opts.gtmUrl)
|
|
16166
17244
|
fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
|
|
17245
|
+
if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
|
|
17246
|
+
fail(
|
|
17247
|
+
this,
|
|
17248
|
+
"GTM mining cannot be combined with --create-dimension. Run discovery and creation as separate commands.",
|
|
17249
|
+
EXIT_CODES.validation
|
|
17250
|
+
);
|
|
17251
|
+
}
|
|
17252
|
+
if (!opts.createDimension) {
|
|
17253
|
+
const createOnlyFlag = opts.event !== void 0 ? "--event" : opts.key !== void 0 ? "--key" : opts.label !== void 0 ? "--label" : void 0;
|
|
17254
|
+
if (createOnlyFlag)
|
|
17255
|
+
fail(this, `${createOnlyFlag} requires --create-dimension`, EXIT_CODES.validation);
|
|
17256
|
+
}
|
|
16167
17257
|
const gtmInput = opts.gtmUrl ?? opts.gtm;
|
|
16168
17258
|
const minutes = minutesValue(opts.minutes) ?? 10080;
|
|
16169
17259
|
let report;
|
|
16170
17260
|
let apiSucceeded = false;
|
|
17261
|
+
let discoveryCredentials;
|
|
16171
17262
|
let apiFailure;
|
|
16172
17263
|
try {
|
|
16173
|
-
|
|
17264
|
+
discoveryCredentials = requireCredentials();
|
|
17265
|
+
const result = await discoverTrackingSchema(discoveryCredentials, minutes);
|
|
16174
17266
|
if (result.ok && result.data) {
|
|
16175
17267
|
report = result.data;
|
|
16176
17268
|
apiSucceeded = true;
|
|
@@ -16186,7 +17278,7 @@ var discoverCommand = new Command("discover").description("Read back the trackin
|
|
|
16186
17278
|
...error instanceof CliError ? { details: error.details, cliError: error } : {}
|
|
16187
17279
|
};
|
|
16188
17280
|
}
|
|
16189
|
-
if (!report && !gtmInput) {
|
|
17281
|
+
if (!report && (!gtmInput || opts.createDimension)) {
|
|
16190
17282
|
if (apiFailure?.cliError) throw apiFailure.cliError;
|
|
16191
17283
|
fail(
|
|
16192
17284
|
this,
|
|
@@ -16241,20 +17333,92 @@ var discoverCommand = new Command("discover").description("Read back the trackin
|
|
|
16241
17333
|
report.gtm = gtm;
|
|
16242
17334
|
report.sources.gtm_static = gtm.containers.length > 0;
|
|
16243
17335
|
}
|
|
17336
|
+
if (opts.createDimension) {
|
|
17337
|
+
if (!apiSucceeded || !discoveryCredentials) {
|
|
17338
|
+
fail(
|
|
17339
|
+
this,
|
|
17340
|
+
apiFailure?.message ?? "Discovery API data is required to create a dimension",
|
|
17341
|
+
classifyStatus(apiFailure?.status ?? 0),
|
|
17342
|
+
apiFailure?.details
|
|
17343
|
+
);
|
|
17344
|
+
}
|
|
17345
|
+
const observation = resolveDimensionObservation(
|
|
17346
|
+
this,
|
|
17347
|
+
report,
|
|
17348
|
+
opts.createDimension,
|
|
17349
|
+
opts.event
|
|
17350
|
+
);
|
|
17351
|
+
const definition = {
|
|
17352
|
+
key: opts.key ?? opts.createDimension.toLowerCase(),
|
|
17353
|
+
label: opts.label ?? defaultDimensionLabel(opts.createDimension),
|
|
17354
|
+
value_type: isNumericField(observation.field) ? "number" : "string",
|
|
17355
|
+
capture: {
|
|
17356
|
+
mode: "data_layer",
|
|
17357
|
+
...observation.dlName !== "dataLayer" ? { dataLayerName: observation.dlName } : {},
|
|
17358
|
+
eventName: observation.eventName,
|
|
17359
|
+
from: opts.createDimension,
|
|
17360
|
+
persist: true
|
|
17361
|
+
},
|
|
17362
|
+
is_dimension: true
|
|
17363
|
+
};
|
|
17364
|
+
let created;
|
|
17365
|
+
try {
|
|
17366
|
+
created = await createInsight(discoveryCredentials, "signals", definition);
|
|
17367
|
+
} catch (error) {
|
|
17368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17369
|
+
fail(
|
|
17370
|
+
this,
|
|
17371
|
+
`Dimension was not created: ${message}`,
|
|
17372
|
+
error instanceof CliError ? error.exitCode : EXIT_CODES.general,
|
|
17373
|
+
error instanceof CliError ? error.details : void 0
|
|
17374
|
+
);
|
|
17375
|
+
}
|
|
17376
|
+
if (!created.ok || !created.data) {
|
|
17377
|
+
const reason = created.error ?? "The signals API rejected the request";
|
|
17378
|
+
if (created.status === 409) {
|
|
17379
|
+
fail(
|
|
17380
|
+
this,
|
|
17381
|
+
`Dimension was not created: ${reason} Choose another --key or archive an existing dimension in Audience > Attributes, then retry.`,
|
|
17382
|
+
classifyStatus(created.status),
|
|
17383
|
+
created.errorDetails
|
|
17384
|
+
);
|
|
17385
|
+
}
|
|
17386
|
+
if (created.status === 400) {
|
|
17387
|
+
fail(
|
|
17388
|
+
this,
|
|
17389
|
+
`Dimension was not created: ${reason} Check --create-dimension, --event, --key, and --label, then retry.`,
|
|
17390
|
+
classifyStatus(created.status),
|
|
17391
|
+
created.errorDetails
|
|
17392
|
+
);
|
|
17393
|
+
}
|
|
17394
|
+
fail(
|
|
17395
|
+
this,
|
|
17396
|
+
`Dimension was not created: ${reason}`,
|
|
17397
|
+
classifyStatus(created.status),
|
|
17398
|
+
created.errorDetails
|
|
17399
|
+
);
|
|
17400
|
+
}
|
|
17401
|
+
output(this, { signal: created.data }, () => {
|
|
17402
|
+
process.stdout.write(`Created dimension ${created.data.id}.
|
|
17403
|
+
`);
|
|
17404
|
+
process.stdout.write("Next: open Audience > Attributes in the Apex dashboard.\n");
|
|
17405
|
+
});
|
|
17406
|
+
return;
|
|
17407
|
+
}
|
|
16244
17408
|
output(this, report, () => process.stdout.write(renderDiscovery(report)));
|
|
16245
17409
|
});
|
|
16246
17410
|
|
|
16247
17411
|
// src/commands/skills.ts
|
|
16248
|
-
var
|
|
16249
|
-
var
|
|
17412
|
+
var import_node_fs24 = __toESM(require("node:fs"));
|
|
17413
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
16250
17414
|
function packageRoot() {
|
|
16251
17415
|
const candidates = [
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
17416
|
+
import_node_path22.default.resolve(__dirname, ".."),
|
|
17417
|
+
import_node_path22.default.resolve(import_node_path22.default.dirname(process.argv[1] ?? process.cwd()), ".."),
|
|
17418
|
+
import_node_path22.default.resolve(process.cwd(), "packages/cli")
|
|
16255
17419
|
];
|
|
16256
17420
|
for (const candidate of candidates) {
|
|
16257
|
-
if (
|
|
17421
|
+
if (import_node_fs24.default.existsSync(import_node_path22.default.join(candidate, "AGENTS.md"))) return candidate;
|
|
16258
17422
|
}
|
|
16259
17423
|
throw new Error(
|
|
16260
17424
|
`Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
|
|
@@ -16263,22 +17427,22 @@ function packageRoot() {
|
|
|
16263
17427
|
function skillPaths() {
|
|
16264
17428
|
const root = packageRoot();
|
|
16265
17429
|
return {
|
|
16266
|
-
agents:
|
|
16267
|
-
skill:
|
|
17430
|
+
agents: import_node_path22.default.join(root, "AGENTS.md"),
|
|
17431
|
+
skill: import_node_path22.default.join(root, "skills", "apex-cli", "SKILL.md")
|
|
16268
17432
|
};
|
|
16269
17433
|
}
|
|
16270
17434
|
function installSkill(paths, targetRoot) {
|
|
16271
|
-
const missing = [paths.skill, paths.agents].filter((filePath) => !
|
|
17435
|
+
const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs24.default.existsSync(filePath));
|
|
16272
17436
|
if (missing.length > 0) {
|
|
16273
17437
|
throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
|
|
16274
17438
|
}
|
|
16275
|
-
const target =
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
17439
|
+
const target = import_node_path22.default.resolve(targetRoot, "apex-cli");
|
|
17440
|
+
import_node_fs24.default.mkdirSync(target, { recursive: true });
|
|
17441
|
+
import_node_fs24.default.copyFileSync(paths.skill, import_node_path22.default.join(target, "SKILL.md"));
|
|
17442
|
+
import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
|
|
16279
17443
|
return target;
|
|
16280
17444
|
}
|
|
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
|
|
17445
|
+
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
17446
|
let paths;
|
|
16283
17447
|
try {
|
|
16284
17448
|
paths = skillPaths();
|
|
@@ -16308,7 +17472,7 @@ function switchValue(value) {
|
|
|
16308
17472
|
throw new Error("Use on or off");
|
|
16309
17473
|
}
|
|
16310
17474
|
var settingsGet = new Command("settings").description("Show platform operator settings").action(async function() {
|
|
16311
|
-
const res = await getAdminSettings(
|
|
17475
|
+
const res = await getAdminSettings(requireUserCredentials());
|
|
16312
17476
|
if (!res.ok || !res.data)
|
|
16313
17477
|
fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
|
|
16314
17478
|
output(
|
|
@@ -16340,14 +17504,14 @@ var set5 = new Command("set").description("Set platform kill switches").option("
|
|
|
16340
17504
|
`apex admin settings set ${switches} --confirm`
|
|
16341
17505
|
);
|
|
16342
17506
|
}
|
|
16343
|
-
const res = await setAdminSettings(
|
|
17507
|
+
const res = await setAdminSettings(requireUserCredentials(), body5);
|
|
16344
17508
|
if (!res.ok || !res.data)
|
|
16345
17509
|
fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
|
|
16346
17510
|
output(this, res.data, () => console.log("Platform settings updated."));
|
|
16347
17511
|
});
|
|
16348
17512
|
settingsGet.addCommand(set5);
|
|
16349
17513
|
var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
|
|
16350
|
-
const res = await getAdminQuarantine(
|
|
17514
|
+
const res = await getAdminQuarantine(requireUserCredentials());
|
|
16351
17515
|
if (!res.ok || !res.data)
|
|
16352
17516
|
fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
|
|
16353
17517
|
output(
|
|
@@ -16364,7 +17528,56 @@ var quarantine = new Command("quarantine").description("List tracking quarantine
|
|
|
16364
17528
|
)
|
|
16365
17529
|
);
|
|
16366
17530
|
});
|
|
16367
|
-
|
|
17531
|
+
async function confirmArchive(command, shopId, force) {
|
|
17532
|
+
if (force) return true;
|
|
17533
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
17534
|
+
confirmationRequired(
|
|
17535
|
+
command,
|
|
17536
|
+
"Archiving a shop requires confirmation in non-TTY mode.",
|
|
17537
|
+
`apex admin shops archive ${JSON.stringify(shopId)} --force`
|
|
17538
|
+
);
|
|
17539
|
+
}
|
|
17540
|
+
const readline3 = await import("node:readline");
|
|
17541
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
17542
|
+
const answer = await new Promise((resolve2) => {
|
|
17543
|
+
rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve2);
|
|
17544
|
+
});
|
|
17545
|
+
rl.close();
|
|
17546
|
+
if (answer.toLowerCase() !== "y") {
|
|
17547
|
+
console.error("Aborted.");
|
|
17548
|
+
return false;
|
|
17549
|
+
}
|
|
17550
|
+
return true;
|
|
17551
|
+
}
|
|
17552
|
+
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) {
|
|
17553
|
+
if (!await confirmArchive(this, shopId, opts.force)) return;
|
|
17554
|
+
const res = await archiveAdminShop(requireUserCredentials(), shopId);
|
|
17555
|
+
if (!res.ok || !res.data)
|
|
17556
|
+
fail(
|
|
17557
|
+
this,
|
|
17558
|
+
res.error ?? "Failed to archive shop",
|
|
17559
|
+
classifyStatus(res.status),
|
|
17560
|
+
res.errorDetails
|
|
17561
|
+
);
|
|
17562
|
+
output(this, res.data, () => console.log("Shop archived."));
|
|
17563
|
+
});
|
|
17564
|
+
var restore2 = new Command("restore").description("Restore an archived shop").argument("<shopId>", "Shop ID").action(async function(shopId) {
|
|
17565
|
+
const res = await restoreAdminShop(requireUserCredentials(), shopId);
|
|
17566
|
+
if (!res.ok || !res.data)
|
|
17567
|
+
fail(
|
|
17568
|
+
this,
|
|
17569
|
+
res.error ?? "Failed to restore shop",
|
|
17570
|
+
classifyStatus(res.status),
|
|
17571
|
+
res.errorDetails
|
|
17572
|
+
);
|
|
17573
|
+
output(
|
|
17574
|
+
this,
|
|
17575
|
+
res.data,
|
|
17576
|
+
() => console.log("Shop restored. Delivery remains controlled by the kill switch.")
|
|
17577
|
+
);
|
|
17578
|
+
});
|
|
17579
|
+
var shops = new Command("shops").description("Archive and restore shops on the platform admin plane").addCommand(archive2).addCommand(restore2);
|
|
17580
|
+
var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops);
|
|
16368
17581
|
|
|
16369
17582
|
// src/commands/membership.ts
|
|
16370
17583
|
init_source();
|
|
@@ -16381,7 +17594,7 @@ async function confirm(c, force, label, retry) {
|
|
|
16381
17594
|
const readline3 = await import("node:readline");
|
|
16382
17595
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
16383
17596
|
const answer = await new Promise(
|
|
16384
|
-
(
|
|
17597
|
+
(resolve2) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve2)
|
|
16385
17598
|
);
|
|
16386
17599
|
rl.close();
|
|
16387
17600
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -16408,7 +17621,12 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
|
|
|
16408
17621
|
orgCommand.command("members").action(async function() {
|
|
16409
17622
|
await run2(this, listOrgMembers(requireUserCredentials()), "list members");
|
|
16410
17623
|
});
|
|
16411
|
-
orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").
|
|
17624
|
+
orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
|
|
17625
|
+
new Option(
|
|
17626
|
+
"--default-shop-role <role>",
|
|
17627
|
+
"Default shop role for every brand in the workspace (workspace defaults are internal roles)"
|
|
17628
|
+
).choices(["editor"]).default("editor")
|
|
17629
|
+
).action(async function(email, o) {
|
|
16412
17630
|
if (o.role !== void 0) {
|
|
16413
17631
|
process.stderr.write(
|
|
16414
17632
|
"Warning: --role is deprecated for apex org invite; use --org-role instead. --role will be removed in v0.7.0.\n"
|
|
@@ -16452,7 +17670,12 @@ var teamCommand = new Command("team").description("Manage a shop team");
|
|
|
16452
17670
|
teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
|
|
16453
17671
|
await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
|
|
16454
17672
|
});
|
|
16455
|
-
teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").
|
|
17673
|
+
teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
|
|
17674
|
+
new Option(
|
|
17675
|
+
"--role <role>",
|
|
17676
|
+
"Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
|
|
17677
|
+
).choices(["editor", "client_viewer"]).makeOptionMandatory()
|
|
17678
|
+
).action(async function(email, o) {
|
|
16456
17679
|
await run2(
|
|
16457
17680
|
this,
|
|
16458
17681
|
inviteTeam(requireUserCredentials(), email, o.shop, o.role),
|
|
@@ -16466,7 +17689,12 @@ teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id
|
|
|
16466
17689
|
"revoke invitation"
|
|
16467
17690
|
);
|
|
16468
17691
|
});
|
|
16469
|
-
teamCommand.command("set-role").argument("<memberId>").
|
|
17692
|
+
teamCommand.command("set-role").argument("<memberId>").addArgument(
|
|
17693
|
+
new Argument(
|
|
17694
|
+
"<role>",
|
|
17695
|
+
"Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
|
|
17696
|
+
).choices(["editor", "client_viewer"])
|
|
17697
|
+
).requiredOption("--shop <id>").action(async function(id, role, o) {
|
|
16470
17698
|
await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
|
|
16471
17699
|
});
|
|
16472
17700
|
teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
|
|
@@ -16636,8 +17864,8 @@ async function fetchText(url) {
|
|
|
16636
17864
|
});
|
|
16637
17865
|
return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
|
|
16638
17866
|
}
|
|
16639
|
-
function abs(base,
|
|
16640
|
-
return new URL(
|
|
17867
|
+
function abs(base, path24) {
|
|
17868
|
+
return new URL(path24, base).toString();
|
|
16641
17869
|
}
|
|
16642
17870
|
function looksLikeShopifyCart(text) {
|
|
16643
17871
|
try {
|
|
@@ -16657,9 +17885,9 @@ async function collectSignals(url) {
|
|
|
16657
17885
|
const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
|
|
16658
17886
|
const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
|
|
16659
17887
|
if (!protectedHit) {
|
|
16660
|
-
for (const
|
|
17888
|
+
for (const path24 of ["/cart.js"]) {
|
|
16661
17889
|
try {
|
|
16662
|
-
const secondary = await fetchText(abs(primary.finalUrl || url,
|
|
17890
|
+
const secondary = await fetchText(abs(primary.finalUrl || url, path24));
|
|
16663
17891
|
if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
|
|
16664
17892
|
chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
|
|
16665
17893
|
}
|
|
@@ -16838,7 +18066,7 @@ function selectedEvents(opts) {
|
|
|
16838
18066
|
}
|
|
16839
18067
|
var list14 = new Command("list").alias("ls").description(
|
|
16840
18068
|
"List webhooks. JSON includes availableEvents; secrets are never returned after create."
|
|
16841
|
-
).action(async function
|
|
18069
|
+
).action(async function action65() {
|
|
16842
18070
|
const data = await loadWebhookIndex(this);
|
|
16843
18071
|
output(this, data, () => printWebhooks(data.data));
|
|
16844
18072
|
});
|
|
@@ -16847,7 +18075,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
16847
18075
|
"Webhook event to subscribe to; repeat for multiple events",
|
|
16848
18076
|
(value, previous = []) => [...previous, value],
|
|
16849
18077
|
[]
|
|
16850
|
-
).action(async function
|
|
18078
|
+
).action(async function action66(opts) {
|
|
16851
18079
|
const events = selectedEvents(opts);
|
|
16852
18080
|
if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
|
|
16853
18081
|
const index = await loadWebhookIndex(this);
|
|
@@ -16879,7 +18107,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
|
|
|
16879
18107
|
}
|
|
16880
18108
|
);
|
|
16881
18109
|
});
|
|
16882
|
-
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function
|
|
18110
|
+
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
|
|
16883
18111
|
requireShopSecretKey(this);
|
|
16884
18112
|
const result = handleResult(
|
|
16885
18113
|
this,
|
|
@@ -16888,7 +18116,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
|
|
|
16888
18116
|
);
|
|
16889
18117
|
output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
|
|
16890
18118
|
});
|
|
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
|
|
18119
|
+
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
18120
|
const limit = Number.parseInt(opts.limit, 10);
|
|
16893
18121
|
if (!Number.isInteger(limit) || limit < 1)
|
|
16894
18122
|
fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
|
|
@@ -16905,7 +18133,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
|
|
|
16905
18133
|
"Replacement event; repeat for multiple events",
|
|
16906
18134
|
(value, previous = []) => [...previous, value],
|
|
16907
18135
|
[]
|
|
16908
|
-
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function
|
|
18136
|
+
).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
|
|
16909
18137
|
if (opts.active && opts.inactive)
|
|
16910
18138
|
fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
|
|
16911
18139
|
const events = selectedEvents(opts);
|
|
@@ -16938,7 +18166,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
|
|
|
16938
18166
|
);
|
|
16939
18167
|
output(this, { webhook: result }, () => printWebhooks([result]));
|
|
16940
18168
|
});
|
|
16941
|
-
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function
|
|
18169
|
+
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
|
|
16942
18170
|
requireShopSecretKey(this);
|
|
16943
18171
|
const result = handleResult(
|
|
16944
18172
|
this,
|
|
@@ -16957,10 +18185,10 @@ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks"
|
|
|
16957
18185
|
|
|
16958
18186
|
// src/commands/simulate.ts
|
|
16959
18187
|
init_source();
|
|
16960
|
-
var
|
|
16961
|
-
var
|
|
18188
|
+
var import_node_fs25 = __toESM(require("node:fs"));
|
|
18189
|
+
var import_node_path23 = __toESM(require("node:path"));
|
|
16962
18190
|
function readJson2(file) {
|
|
16963
|
-
const text = file === "-" ?
|
|
18191
|
+
const text = file === "-" ? import_node_fs25.default.readFileSync(0, "utf8") : import_node_fs25.default.readFileSync(import_node_path23.default.resolve(file), "utf8");
|
|
16964
18192
|
try {
|
|
16965
18193
|
return JSON.parse(text);
|
|
16966
18194
|
} catch (error) {
|
|
@@ -17082,8 +18310,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
|
|
|
17082
18310
|
});
|
|
17083
18311
|
|
|
17084
18312
|
// src/commands/config-resources.ts
|
|
17085
|
-
var
|
|
17086
|
-
var
|
|
18313
|
+
var import_node_fs26 = __toESM(require("node:fs"));
|
|
18314
|
+
var import_node_path24 = __toESM(require("node:path"));
|
|
17087
18315
|
function show(resource) {
|
|
17088
18316
|
process.stdout.write(`${JSON.stringify(resource, null, 2)}
|
|
17089
18317
|
`);
|
|
@@ -17109,8 +18337,8 @@ async function confirmAssetDelete(id) {
|
|
|
17109
18337
|
input: process.stdin,
|
|
17110
18338
|
output: process.stderr
|
|
17111
18339
|
});
|
|
17112
|
-
const answer = await new Promise((
|
|
17113
|
-
rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `,
|
|
18340
|
+
const answer = await new Promise((resolve2) => {
|
|
18341
|
+
rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve2);
|
|
17114
18342
|
});
|
|
17115
18343
|
rl.close();
|
|
17116
18344
|
return answer.trim().toLowerCase() === "y";
|
|
@@ -17156,12 +18384,12 @@ function readFamily(family, label) {
|
|
|
17156
18384
|
}
|
|
17157
18385
|
var assetReads = readFamily("assets", "assets");
|
|
17158
18386
|
var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
|
|
17159
|
-
const absolute =
|
|
17160
|
-
if (!
|
|
17161
|
-
const fileStat =
|
|
18387
|
+
const absolute = import_node_path24.default.resolve(process.cwd(), file);
|
|
18388
|
+
if (!import_node_fs26.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
|
|
18389
|
+
const fileStat = import_node_fs26.default.statSync(absolute);
|
|
17162
18390
|
if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
|
|
17163
18391
|
if (fileStat.size > MAX_ASSET_SIZE_BYTES) fail(this, `${file} is larger than 10 MB`, 3);
|
|
17164
|
-
const mimeType = ASSET_MIME_TYPES[
|
|
18392
|
+
const mimeType = ASSET_MIME_TYPES[import_node_path24.default.extname(absolute).toLowerCase()];
|
|
17165
18393
|
if (!mimeType)
|
|
17166
18394
|
fail(
|
|
17167
18395
|
this,
|
|
@@ -17170,8 +18398,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
|
|
|
17170
18398
|
);
|
|
17171
18399
|
const r = await uploadAssets(requireCredentials(), [
|
|
17172
18400
|
{
|
|
17173
|
-
name:
|
|
17174
|
-
bytes:
|
|
18401
|
+
name: import_node_path24.default.basename(absolute),
|
|
18402
|
+
bytes: import_node_fs26.default.readFileSync(absolute),
|
|
17175
18403
|
type: mimeType
|
|
17176
18404
|
}
|
|
17177
18405
|
]);
|
|
@@ -17261,6 +18489,7 @@ program2.addCommand(funnelsCommand);
|
|
|
17261
18489
|
program2.addCommand(environmentsCommand);
|
|
17262
18490
|
program2.addCommand(consentCommand);
|
|
17263
18491
|
program2.addCommand(triggersCommand);
|
|
18492
|
+
program2.addCommand(projectCodeCommand);
|
|
17264
18493
|
program2.addCommand(domainsCommand);
|
|
17265
18494
|
program2.addCommand(handoverCommand);
|
|
17266
18495
|
program2.addCommand(analyticsCommand);
|