@drip-apex/apex-cli 0.2.1 → 0.3.0
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 +15 -1
- package/README.md +70 -2
- package/dist/cli.js +2040 -480
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +12 -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 path21 = require("node:path");
|
|
1163
|
+
var fs25 = 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 (fs25.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 = path21.resolve(baseDir, baseName);
|
|
2163
|
+
if (fs25.existsSync(localBin)) return localBin;
|
|
2164
|
+
if (sourceExt.includes(path21.extname(baseName))) return void 0;
|
|
2165
2165
|
const foundExt = sourceExt.find(
|
|
2166
|
-
(ext) =>
|
|
2166
|
+
(ext) => fs25.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 = fs25.realpathSync(this._scriptPath);
|
|
2179
2179
|
} catch {
|
|
2180
2180
|
resolvedScriptPath = this._scriptPath;
|
|
2181
2181
|
}
|
|
2182
|
-
executableDir =
|
|
2183
|
-
|
|
2182
|
+
executableDir = path21.resolve(
|
|
2183
|
+
path21.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 = path21.basename(
|
|
2191
2191
|
this._scriptPath,
|
|
2192
|
-
|
|
2192
|
+
path21.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(path21.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 = path21.basename(filename, path21.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(path22) {
|
|
3065
|
+
if (path22 === void 0) return this._executableDir;
|
|
3066
|
+
this._executableDir = path22;
|
|
3067
3067
|
return this;
|
|
3068
3068
|
}
|
|
3069
3069
|
/**
|
|
@@ -4588,15 +4588,15 @@ var init_mjs = __esm({
|
|
|
4588
4588
|
this.listeners[ev].push(fn);
|
|
4589
4589
|
}
|
|
4590
4590
|
removeListener(ev, fn) {
|
|
4591
|
-
const
|
|
4592
|
-
const i =
|
|
4591
|
+
const list15 = this.listeners[ev];
|
|
4592
|
+
const i = list15.indexOf(fn);
|
|
4593
4593
|
if (i === -1) {
|
|
4594
4594
|
return;
|
|
4595
4595
|
}
|
|
4596
|
-
if (i === 0 &&
|
|
4597
|
-
|
|
4596
|
+
if (i === 0 && list15.length === 1) {
|
|
4597
|
+
list15.length = 0;
|
|
4598
4598
|
} else {
|
|
4599
|
-
|
|
4599
|
+
list15.splice(i, 1);
|
|
4600
4600
|
}
|
|
4601
4601
|
}
|
|
4602
4602
|
emit(ev, code, signal) {
|
|
@@ -6743,16 +6743,16 @@ __export(ora_exports, {
|
|
|
6743
6743
|
function ora(options) {
|
|
6744
6744
|
return new Ora(options);
|
|
6745
6745
|
}
|
|
6746
|
-
async function oraPromise(
|
|
6747
|
-
const actionIsFunction = typeof
|
|
6748
|
-
const actionIsPromise = typeof
|
|
6746
|
+
async function oraPromise(action68, options) {
|
|
6747
|
+
const actionIsFunction = typeof action68 === "function";
|
|
6748
|
+
const actionIsPromise = typeof action68.then === "function";
|
|
6749
6749
|
if (!actionIsFunction && !actionIsPromise) {
|
|
6750
6750
|
throw new TypeError("Parameter `action` must be a Function or a Promise");
|
|
6751
6751
|
}
|
|
6752
6752
|
const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
|
|
6753
6753
|
const spinner = ora(options).start();
|
|
6754
6754
|
try {
|
|
6755
|
-
const promise = actionIsFunction ?
|
|
6755
|
+
const promise = actionIsFunction ? action68(spinner) : action68;
|
|
6756
6756
|
const result = await promise;
|
|
6757
6757
|
spinner.succeed(
|
|
6758
6758
|
successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
|
|
@@ -7126,6 +7126,7 @@ var commandName = "apex";
|
|
|
7126
7126
|
var wroteEnvelope = false;
|
|
7127
7127
|
var stdoutLines = [];
|
|
7128
7128
|
var originalConsoleLog = null;
|
|
7129
|
+
var deprecationMarker = null;
|
|
7129
7130
|
function findPackageVersion() {
|
|
7130
7131
|
const candidates = [
|
|
7131
7132
|
import_node_path.default.resolve(__dirname, "../package.json"),
|
|
@@ -7182,12 +7183,16 @@ function restoreConsoleLog() {
|
|
|
7182
7183
|
console.log = originalConsoleLog;
|
|
7183
7184
|
originalConsoleLog = null;
|
|
7184
7185
|
}
|
|
7186
|
+
function markDeprecatedInvocation(marker) {
|
|
7187
|
+
deprecationMarker = marker;
|
|
7188
|
+
}
|
|
7185
7189
|
function writeEnvelope(data, command = commandName) {
|
|
7186
7190
|
const envelope = {
|
|
7187
7191
|
ok: true,
|
|
7188
7192
|
version: CLI_VERSION,
|
|
7189
7193
|
command,
|
|
7190
|
-
data
|
|
7194
|
+
data,
|
|
7195
|
+
...deprecationMarker ? { deprecated: deprecationMarker } : {}
|
|
7191
7196
|
};
|
|
7192
7197
|
wroteEnvelope = true;
|
|
7193
7198
|
restoreConsoleLog();
|
|
@@ -7359,6 +7364,8 @@ function findConfigPath() {
|
|
|
7359
7364
|
var import_node_crypto = require("node:crypto");
|
|
7360
7365
|
var mockEnvironmentPatchAttempts = 0;
|
|
7361
7366
|
var mockEnvironmentReads = 0;
|
|
7367
|
+
var mockDomainsPatchAttempts = 0;
|
|
7368
|
+
var mockScreenshotPolls = 0;
|
|
7362
7369
|
function extractErrorMessage(json, status2) {
|
|
7363
7370
|
if (!json) return `HTTP ${status2}`;
|
|
7364
7371
|
const err = json.error;
|
|
@@ -7390,6 +7397,13 @@ function extractErrorDetails(json) {
|
|
|
7390
7397
|
return Object.keys(details).length > 0 ? details : void 0;
|
|
7391
7398
|
}
|
|
7392
7399
|
function mockExperiment() {
|
|
7400
|
+
const collidingVariation = process.env.APEX_CLI_MOCK_SCREENSHOTS === "collision" ? [{
|
|
7401
|
+
id: "deadbeef-0000-4000-8000-000000000002",
|
|
7402
|
+
name: "Trust-Badge",
|
|
7403
|
+
weight: 0,
|
|
7404
|
+
is_control: false,
|
|
7405
|
+
mutations: []
|
|
7406
|
+
}] : [];
|
|
7393
7407
|
return {
|
|
7394
7408
|
id: "00000000-0000-4000-8000-000000000001",
|
|
7395
7409
|
slug: "fixture-test",
|
|
@@ -7398,6 +7412,7 @@ function mockExperiment() {
|
|
|
7398
7412
|
traffic_allocation: 1,
|
|
7399
7413
|
store_url: "https://example.com/products/demo",
|
|
7400
7414
|
created_at: "2026-07-04T00:00:00.000Z",
|
|
7415
|
+
archived_at: null,
|
|
7401
7416
|
variations: [
|
|
7402
7417
|
{
|
|
7403
7418
|
id: "00000000-0000-4000-8000-0000000000c0",
|
|
@@ -7407,9 +7422,10 @@ function mockExperiment() {
|
|
|
7407
7422
|
is_control: true,
|
|
7408
7423
|
mutations: []
|
|
7409
7424
|
},
|
|
7425
|
+
...collidingVariation,
|
|
7410
7426
|
{
|
|
7411
7427
|
id: "00000000-0000-4000-8000-0000000000b1",
|
|
7412
|
-
slug: "badge",
|
|
7428
|
+
slug: process.env.APEX_CLI_MOCK_SCREENSHOTS === "collision" ? void 0 : "badge",
|
|
7413
7429
|
name: "Trust Badge",
|
|
7414
7430
|
weight: 0.5,
|
|
7415
7431
|
is_control: false,
|
|
@@ -7418,6 +7434,18 @@ function mockExperiment() {
|
|
|
7418
7434
|
]
|
|
7419
7435
|
};
|
|
7420
7436
|
}
|
|
7437
|
+
var SECOND_PAGE_GOAL_ID = "00000000-0000-4000-8000-0000000000f2";
|
|
7438
|
+
function mockFunnel() {
|
|
7439
|
+
return {
|
|
7440
|
+
id: "00000000-0000-4000-8000-0000000000aa",
|
|
7441
|
+
name: "Fixture funnel",
|
|
7442
|
+
steps: [
|
|
7443
|
+
{ order: 1, label: "Collection", url_pattern: "/collections/*" },
|
|
7444
|
+
{ order: 2, label: "Product", url_pattern: "/products/*" }
|
|
7445
|
+
],
|
|
7446
|
+
created_at: "2026-07-04T00:00:00.000Z"
|
|
7447
|
+
};
|
|
7448
|
+
}
|
|
7421
7449
|
function mockGoal() {
|
|
7422
7450
|
return {
|
|
7423
7451
|
id: "00000000-0000-4000-8000-0000000000f1",
|
|
@@ -7427,11 +7455,24 @@ function mockGoal() {
|
|
|
7427
7455
|
selector: null,
|
|
7428
7456
|
text_match: null,
|
|
7429
7457
|
url_pattern: null,
|
|
7458
|
+
page_id: null,
|
|
7430
7459
|
event_name: "purchase",
|
|
7431
7460
|
capture: null,
|
|
7432
7461
|
created_at: "2026-07-04T00:00:00.000Z"
|
|
7433
7462
|
};
|
|
7434
7463
|
}
|
|
7464
|
+
var PAGE_SCOPED_GOAL_ID = "00000000-0000-4000-8000-0000000000f3";
|
|
7465
|
+
function mockPageScopedGoal() {
|
|
7466
|
+
return {
|
|
7467
|
+
...mockGoal(),
|
|
7468
|
+
id: PAGE_SCOPED_GOAL_ID,
|
|
7469
|
+
name: "Fixture page-scoped pageview",
|
|
7470
|
+
type: "pageview",
|
|
7471
|
+
event_name: null,
|
|
7472
|
+
url_pattern: "bestellung-abgeschlossen",
|
|
7473
|
+
page_id: "00000000-0000-4000-8000-0000000000e1"
|
|
7474
|
+
};
|
|
7475
|
+
}
|
|
7435
7476
|
function mockBrandContext() {
|
|
7436
7477
|
if (process.env.APEX_CLI_MOCK_BRAND_PROFILE === "none") {
|
|
7437
7478
|
return {
|
|
@@ -7481,11 +7522,144 @@ function mockBrandContext() {
|
|
|
7481
7522
|
].join("\n")
|
|
7482
7523
|
};
|
|
7483
7524
|
}
|
|
7484
|
-
function mockResponse(method,
|
|
7525
|
+
function mockResponse(method, path21, body5) {
|
|
7485
7526
|
if (process.env.APEX_CLI_MOCK_API !== "1") return null;
|
|
7486
|
-
const url = new URL(
|
|
7527
|
+
const url = new URL(path21, "https://mock.apex.local");
|
|
7487
7528
|
const exp = mockExperiment();
|
|
7529
|
+
if (method === "POST" && url.pathname === `/api/v1/experiments/${exp.id}/screenshots`) {
|
|
7530
|
+
mockScreenshotPolls = 0;
|
|
7531
|
+
return { ok: true, status: 202, data: { job_id: "screenshot_job_1", status: "queued", poll_url: "/api/v1/screenshots/screenshot_job_1" } };
|
|
7532
|
+
}
|
|
7533
|
+
if (method === "GET" && url.pathname === "/api/v1/screenshots/screenshot_job_1") {
|
|
7534
|
+
mockScreenshotPolls += 1;
|
|
7535
|
+
if (process.env.APEX_CLI_MOCK_SCREENSHOTS === "transient" && mockScreenshotPolls === 1) {
|
|
7536
|
+
return { ok: false, status: 503, error: "Service temporarily unavailable" };
|
|
7537
|
+
}
|
|
7538
|
+
if (process.env.APEX_CLI_MOCK_SCREENSHOTS === "timeout" || process.env.APEX_CLI_MOCK_SCREENSHOTS === "deadline-processing" || mockScreenshotPolls === 1 && process.env.APEX_CLI_MOCK_SCREENSHOTS !== "deadline-completed") {
|
|
7539
|
+
return { ok: true, status: 200, data: { job_id: "screenshot_job_1", experiment_id: exp.id, status: "processing", devices: ["desktop_chrome"], variation_ids: exp.variations.map((variation) => variation.id) } };
|
|
7540
|
+
}
|
|
7541
|
+
const results2 = {
|
|
7542
|
+
[exp.variations[0].id]: { desktop_chrome: "data:image/png;base64,iVBORw0KGgo=" }
|
|
7543
|
+
};
|
|
7544
|
+
if (process.env.APEX_CLI_MOCK_SCREENSHOTS !== "partial") {
|
|
7545
|
+
for (const variation of exp.variations.slice(1)) {
|
|
7546
|
+
results2[variation.id] = { desktop_chrome: "data:image/png;base64,iVBORw0KGgo=" };
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
return { ok: true, status: 200, data: { job_id: "screenshot_job_1", experiment_id: exp.id, status: "completed", devices: ["desktop_chrome"], variation_ids: exp.variations.map((variation) => variation.id), results: results2 } };
|
|
7550
|
+
}
|
|
7551
|
+
if (url.pathname.startsWith("/api/v1/webhooks")) {
|
|
7552
|
+
const input = body5 ?? {};
|
|
7553
|
+
const webhook = {
|
|
7554
|
+
id: url.pathname.split("/")[4] || "webhook_1",
|
|
7555
|
+
url: String(input.url ?? "https://hooks.example.test/apex"),
|
|
7556
|
+
events: Array.isArray(input.events) ? input.events.map(String) : ["project.published"],
|
|
7557
|
+
active: input.active === void 0 ? true : input.active === true,
|
|
7558
|
+
hasSecret: true,
|
|
7559
|
+
consecutiveFailures: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? 3 : 1,
|
|
7560
|
+
disabledAt: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "2026-08-05T12:00:00.000Z" : null,
|
|
7561
|
+
created_at: "2026-08-05T00:00:00.000Z"
|
|
7562
|
+
};
|
|
7563
|
+
const availableEvents = ["experiment.started", "experiment.completed", "experiment.significant", "experiment.guardian_triggered", "experiment.guardian_paused", "variation.winning", "screenshots.completed", "project.published"];
|
|
7564
|
+
if (method === "GET" && url.pathname === "/api/v1/webhooks") return { ok: true, status: 200, data: { data: [webhook], availableEvents, pagination: { has_more: false, next_cursor: null } } };
|
|
7565
|
+
if (method === "POST" && url.pathname === "/api/v1/webhooks") return { ok: true, status: 201, data: { ...webhook, secret: "whsec_mock_secret" } };
|
|
7566
|
+
if (method === "PATCH") return { ok: true, status: 200, data: webhook };
|
|
7567
|
+
if (method === "DELETE") return { ok: true, status: 200, data: { deleted: true, id: webhook.id } };
|
|
7568
|
+
if (method === "GET" && url.pathname.endsWith("/deliveries")) return { ok: true, status: 200, data: { webhook: { id: webhook.id, active: webhook.active, consecutiveFailures: webhook.consecutiveFailures, disabledAt: webhook.disabledAt }, data: [{ id: "delivery_1", webhook_id: webhook.id, event: "project.published", status: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "dead" : "delivered", attempts: 1, response_status: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? 500 : 200, last_error: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "Webhook responded with 500" : null, next_attempt_at: null, processing_started_at: null, delivered_at: "2026-08-05T00:01:00.000Z", created_at: "2026-08-05T00:00:00.000Z", updated_at: "2026-08-05T00:01:00.000Z" }], pagination: { has_more: false, next_cursor: null } } };
|
|
7569
|
+
if (method === "POST" && url.pathname.endsWith("/test")) return { ok: true, status: 200, data: { webhook: { id: webhook.id, active: webhook.active, consecutiveFailures: webhook.consecutiveFailures, disabledAt: webhook.disabledAt }, delivery: { id: "delivery_test", webhook_id: webhook.id, event: "test", status: "delivered", attempts: 1, response_status: 200, last_error: null, next_attempt_at: null, processing_started_at: null, delivered_at: "2026-08-05T00:01:00.000Z", created_at: "2026-08-05T00:00:00.000Z", updated_at: "2026-08-05T00:01:00.000Z" } } };
|
|
7570
|
+
}
|
|
7571
|
+
const forcedStatus = Number(process.env.APEX_CLI_MOCK_ERROR_STATUS ?? 0);
|
|
7572
|
+
if (forcedStatus > 0) {
|
|
7573
|
+
return {
|
|
7574
|
+
ok: false,
|
|
7575
|
+
status: forcedStatus,
|
|
7576
|
+
error: forcedStatus === 401 ? "Invalid API key" : "Resource not found",
|
|
7577
|
+
errorDetails: { mocked: true }
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
if (method === "POST" && url.pathname.endsWith("/build") && process.env.APEX_CLI_MOCK_WORKSPACE_BUILD === "blocked-evidence") {
|
|
7581
|
+
return {
|
|
7582
|
+
ok: false,
|
|
7583
|
+
status: 400,
|
|
7584
|
+
error: "Generated custom JavaScript includes an external network or asset call.",
|
|
7585
|
+
data: {
|
|
7586
|
+
ok: false,
|
|
7587
|
+
error: "Generated custom JavaScript includes an external network or asset call.",
|
|
7588
|
+
blockingIssues: [
|
|
7589
|
+
{
|
|
7590
|
+
code: "external_network_call",
|
|
7591
|
+
severity: "blocking",
|
|
7592
|
+
field: "custom_js",
|
|
7593
|
+
message: "Generated custom JavaScript includes an external network or asset call.",
|
|
7594
|
+
evidence: {
|
|
7595
|
+
construct: "String-capable timer execution (fail-closed)",
|
|
7596
|
+
snippet: "setTimeout(unknownHandler, 10)",
|
|
7597
|
+
line: 7,
|
|
7598
|
+
column: 2
|
|
7599
|
+
}
|
|
7600
|
+
},
|
|
7601
|
+
{
|
|
7602
|
+
code: "external_network_call",
|
|
7603
|
+
severity: "blocking",
|
|
7604
|
+
field: "mutations[1].html",
|
|
7605
|
+
message: "Generated HTML includes an external network or asset URL.",
|
|
7606
|
+
evidence: {
|
|
7607
|
+
construct: "External HTML asset URL",
|
|
7608
|
+
url: "https://assets.example.test/pixel.png",
|
|
7609
|
+
matchIndex: 14
|
|
7610
|
+
}
|
|
7611
|
+
},
|
|
7612
|
+
{
|
|
7613
|
+
code: "invalid_custom_code_type",
|
|
7614
|
+
severity: "blocking",
|
|
7615
|
+
field: "custom_css",
|
|
7616
|
+
message: "custom_css must be a string."
|
|
7617
|
+
},
|
|
7618
|
+
{
|
|
7619
|
+
code: "price_node_mutation",
|
|
7620
|
+
severity: "blocking",
|
|
7621
|
+
field: ".product-price",
|
|
7622
|
+
message: "This mutation replaces a price-display node.",
|
|
7623
|
+
evidence: {
|
|
7624
|
+
selector: ".product-price"
|
|
7625
|
+
}
|
|
7626
|
+
},
|
|
7627
|
+
{
|
|
7628
|
+
code: "unknown_validation_family",
|
|
7629
|
+
severity: "blocking",
|
|
7630
|
+
field: "future_guardrail_field",
|
|
7631
|
+
message: "A future guardrail blocked this workspace."
|
|
7632
|
+
}
|
|
7633
|
+
]
|
|
7634
|
+
}
|
|
7635
|
+
};
|
|
7636
|
+
}
|
|
7637
|
+
if (method === "GET" && url.pathname === "/api/v1/roadmap/approved") {
|
|
7638
|
+
return {
|
|
7639
|
+
ok: true,
|
|
7640
|
+
status: 200,
|
|
7641
|
+
data: {
|
|
7642
|
+
data: [{ id: "roadmap_1", title: "Fixture roadmap idea", linked_experiments: [] }],
|
|
7643
|
+
pagination: { has_more: false, next_cursor: null }
|
|
7644
|
+
}
|
|
7645
|
+
};
|
|
7646
|
+
}
|
|
7647
|
+
if (method === "GET" && url.pathname === "/api/v1/roadmap/items/roadmap_1/handoff") {
|
|
7648
|
+
return {
|
|
7649
|
+
ok: true,
|
|
7650
|
+
status: 200,
|
|
7651
|
+
data: {
|
|
7652
|
+
id: "roadmap_1",
|
|
7653
|
+
title: "Fixture roadmap idea",
|
|
7654
|
+
hypothesis: "The fixture improves conversion",
|
|
7655
|
+
instructions: "Build the fixture"
|
|
7656
|
+
}
|
|
7657
|
+
};
|
|
7658
|
+
}
|
|
7488
7659
|
if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/filesystem`) {
|
|
7660
|
+
if (process.env.APEX_CLI_MOCK_FILESYSTEM_REFRESH === "failed") {
|
|
7661
|
+
throw new Error("Mock filesystem refresh failed");
|
|
7662
|
+
}
|
|
7489
7663
|
const fixtures = [
|
|
7490
7664
|
["experiment.json", '{"schemaVersion":1,"name":"Fixture Test"}\n'],
|
|
7491
7665
|
["targeting.json", '{"url":[],"segments":[]}\n'],
|
|
@@ -7497,6 +7671,7 @@ function mockResponse(method, path20, body5) {
|
|
|
7497
7671
|
["launch.json", '{"status":"draft"}\n'],
|
|
7498
7672
|
["variations/control/variation.json", '{"slug":"control","isControl":true}\n'],
|
|
7499
7673
|
["variations/control/mutations.json", "[]\n"],
|
|
7674
|
+
["variations/control/url-blocks.json", "[]\n"],
|
|
7500
7675
|
["variations/control/index.css", ".control { color: inherit; }\n"],
|
|
7501
7676
|
["variations/control/index.js", "window.__apexControl = true;\n"],
|
|
7502
7677
|
["variations/control/reset.js", "delete window.__apexControl;\n"],
|
|
@@ -7524,6 +7699,36 @@ function mockResponse(method, path20, body5) {
|
|
|
7524
7699
|
}
|
|
7525
7700
|
};
|
|
7526
7701
|
}
|
|
7702
|
+
const workspaceMatch = url.pathname.match(
|
|
7703
|
+
/^\/api\/v1\/experiments\/([^/]+)\/variations\/([^/]+)\/workspace$/
|
|
7704
|
+
);
|
|
7705
|
+
if (method === "GET" && workspaceMatch) {
|
|
7706
|
+
const content = "window.__apexOperator = true;\n";
|
|
7707
|
+
return {
|
|
7708
|
+
ok: true,
|
|
7709
|
+
status: 200,
|
|
7710
|
+
data: {
|
|
7711
|
+
workspace: {
|
|
7712
|
+
id: "workspace_1",
|
|
7713
|
+
schemaVersion: 1,
|
|
7714
|
+
status: "draft",
|
|
7715
|
+
currentRevisionId: "revision_1",
|
|
7716
|
+
updatedAt: "2026-08-05T00:00:00.000Z"
|
|
7717
|
+
},
|
|
7718
|
+
files: [
|
|
7719
|
+
{
|
|
7720
|
+
path: "src/index.js",
|
|
7721
|
+
kind: "custom_js",
|
|
7722
|
+
contentType: "application/javascript",
|
|
7723
|
+
content,
|
|
7724
|
+
sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex"),
|
|
7725
|
+
sizeBytes: Buffer.byteLength(content),
|
|
7726
|
+
updatedAt: "2026-08-05T00:00:00.000Z"
|
|
7727
|
+
}
|
|
7728
|
+
]
|
|
7729
|
+
}
|
|
7730
|
+
};
|
|
7731
|
+
}
|
|
7527
7732
|
const configMatch = url.pathname.match(
|
|
7528
7733
|
/^\/api\/v1\/(assets|landing-pages)(?:\/([^/]+))?$/
|
|
7529
7734
|
);
|
|
@@ -7703,16 +7908,16 @@ function mockResponse(method, path20, body5) {
|
|
|
7703
7908
|
/^\/api\/v1\/(flags|personalizations)(?:\/([^/]+))?(?:\/(activate|pause|archive|status))?$/
|
|
7704
7909
|
);
|
|
7705
7910
|
if (managedMatch) {
|
|
7706
|
-
const [, kind5, id = `${kind5.slice(0, -1)}_1`,
|
|
7911
|
+
const [, kind5, id = `${kind5.slice(0, -1)}_1`, action68] = managedMatch;
|
|
7707
7912
|
const input = body5 ?? {};
|
|
7708
7913
|
const item = {
|
|
7709
7914
|
id,
|
|
7710
7915
|
key: String(input.key ?? `fixture-${kind5.slice(0, -1)}`),
|
|
7711
7916
|
name: String(input.name ?? `Fixture ${kind5.slice(0, -1)}`),
|
|
7712
|
-
status:
|
|
7917
|
+
status: action68 === "activate" ? "active" : action68 === "pause" ? "paused" : action68 === "archive" ? "archived" : "draft",
|
|
7713
7918
|
updated_at: "2026-07-19T00:00:00.000Z"
|
|
7714
7919
|
};
|
|
7715
|
-
if (kind5 === "flags" &&
|
|
7920
|
+
if (kind5 === "flags" && action68 === "status")
|
|
7716
7921
|
return {
|
|
7717
7922
|
ok: true,
|
|
7718
7923
|
status: 200,
|
|
@@ -7745,11 +7950,11 @@ function mockResponse(method, path20, body5) {
|
|
|
7745
7950
|
/^\/api\/v1\/(backlog|signals)(?:\/([^/]+))?(?:\/(archive|restore))?$/
|
|
7746
7951
|
);
|
|
7747
7952
|
if (insightMatch) {
|
|
7748
|
-
const [, kind5, id = `${kind5}_1`,
|
|
7953
|
+
const [, kind5, id = `${kind5}_1`, action68] = insightMatch;
|
|
7749
7954
|
const input = body5 ?? {};
|
|
7750
|
-
if (
|
|
7751
|
-
return { ok: false, status: 405, error: `${
|
|
7752
|
-
const archived =
|
|
7955
|
+
if (action68 && (kind5 !== "backlog" || method !== "POST"))
|
|
7956
|
+
return { ok: false, status: 405, error: `${action68} requires POST` };
|
|
7957
|
+
const archived = action68 === "archive" || method === "DELETE" || url.searchParams.get("include_archived") === "true";
|
|
7753
7958
|
const item = {
|
|
7754
7959
|
id,
|
|
7755
7960
|
...kind5 === "backlog" ? {
|
|
@@ -7775,20 +7980,24 @@ function mockResponse(method, path20, body5) {
|
|
|
7775
7980
|
return { ok: true, status: 200, data: { ok: true } };
|
|
7776
7981
|
return {
|
|
7777
7982
|
ok: true,
|
|
7778
|
-
status: method === "POST" && !
|
|
7983
|
+
status: method === "POST" && !action68 ? 201 : 200,
|
|
7779
7984
|
data: item
|
|
7780
7985
|
};
|
|
7781
7986
|
}
|
|
7782
7987
|
if (method === "GET" && url.pathname === "/api/v1/experiments") {
|
|
7988
|
+
const experiment = url.searchParams.get("archived") === "1" ? { ...exp, archived_at: "2026-07-20T00:00:00.000Z" } : { ...exp, archived_at: null };
|
|
7783
7989
|
return {
|
|
7784
7990
|
ok: true,
|
|
7785
7991
|
status: 200,
|
|
7786
7992
|
data: {
|
|
7787
|
-
data: [
|
|
7993
|
+
data: [experiment],
|
|
7788
7994
|
pagination: { has_more: false, next_cursor: null }
|
|
7789
7995
|
}
|
|
7790
7996
|
};
|
|
7791
7997
|
}
|
|
7998
|
+
if (method === "POST" && url.pathname === `/api/v1/experiments/${exp.id}/start`) {
|
|
7999
|
+
return { ok: true, status: 200, data: { ...exp, status: "running" } };
|
|
8000
|
+
}
|
|
7792
8001
|
if (method === "POST" && url.pathname === "/api/v1/experiments") {
|
|
7793
8002
|
const input = body5 ?? {};
|
|
7794
8003
|
if (input.experiment_type === "redirect" && process.env.APEX_CLI_MOCK_REDIRECT_DISABLED === "1") {
|
|
@@ -7825,6 +8034,7 @@ function mockResponse(method, path20, body5) {
|
|
|
7825
8034
|
return { ok: true, status: 200, data: exp };
|
|
7826
8035
|
}
|
|
7827
8036
|
if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/results`) {
|
|
8037
|
+
const withheld = process.env.APEX_CLI_MOCK_RESULTS_STATE === "untrustworthy";
|
|
7828
8038
|
return {
|
|
7829
8039
|
ok: true,
|
|
7830
8040
|
status: 200,
|
|
@@ -7852,14 +8062,62 @@ function mockResponse(method, path20, body5) {
|
|
|
7852
8062
|
}
|
|
7853
8063
|
],
|
|
7854
8064
|
decision: {
|
|
7855
|
-
state: "collecting",
|
|
8065
|
+
state: withheld ? "untrustworthy" : "collecting",
|
|
7856
8066
|
treatmentName: "Trust Badge",
|
|
7857
8067
|
lift: 25,
|
|
7858
|
-
sequentialConfidence: 62.5
|
|
8068
|
+
sequentialConfidence: withheld ? 0 : 62.5,
|
|
8069
|
+
fixedConfidence: withheld ? 0 : 84,
|
|
8070
|
+
judgedConfidence: withheld ? 0 : 84,
|
|
8071
|
+
basis: withheld ? "sequential" : "fixed_horizon",
|
|
8072
|
+
blockingGates: withheld ? ["mixed-currency"] : []
|
|
8073
|
+
},
|
|
8074
|
+
effective_outcome: {
|
|
8075
|
+
outcome: withheld ? "needs_readout" : "winner",
|
|
8076
|
+
statisticsOutcome: withheld ? "needs_readout" : "collecting",
|
|
8077
|
+
provenance: withheld ? "statistics" : "manager_decided"
|
|
8078
|
+
},
|
|
8079
|
+
manager_decision: withheld ? null : {
|
|
8080
|
+
outcome: "winner",
|
|
8081
|
+
reason: "The business case is strong enough to ship.",
|
|
8082
|
+
decided_by: { id: "user_1", name: "Maya Manager" },
|
|
8083
|
+
decided_at: "2026-07-31T09:00:00.000Z",
|
|
8084
|
+
version: 1
|
|
7859
8085
|
},
|
|
7860
8086
|
metrics: [],
|
|
7861
8087
|
series: [],
|
|
7862
|
-
warnings: []
|
|
8088
|
+
warnings: withheld ? [{ code: "mixed-currency", message: "Mixed currencies block the verdict." }] : []
|
|
8089
|
+
}
|
|
8090
|
+
};
|
|
8091
|
+
}
|
|
8092
|
+
if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/qa/run`) {
|
|
8093
|
+
return {
|
|
8094
|
+
ok: true,
|
|
8095
|
+
status: 200,
|
|
8096
|
+
data: {
|
|
8097
|
+
run: {
|
|
8098
|
+
id: "qa_run_1",
|
|
8099
|
+
status: "in_review",
|
|
8100
|
+
checklist: [],
|
|
8101
|
+
urls: [],
|
|
8102
|
+
automatedEvidence: null,
|
|
8103
|
+
acceptanceCriteriaSummary: null,
|
|
8104
|
+
goalVerification: null,
|
|
8105
|
+
summary: {
|
|
8106
|
+
status: "in_review",
|
|
8107
|
+
required: 0,
|
|
8108
|
+
completedRequired: 0,
|
|
8109
|
+
failedRequired: 0,
|
|
8110
|
+
optional: 0,
|
|
8111
|
+
completedOptional: 0,
|
|
8112
|
+
blockers: []
|
|
8113
|
+
},
|
|
8114
|
+
notes: null,
|
|
8115
|
+
createdAt: "2026-07-04T00:00:00.000Z",
|
|
8116
|
+
updatedAt: "2026-07-04T00:00:00.000Z",
|
|
8117
|
+
approvedAt: null,
|
|
8118
|
+
approvedBy: null,
|
|
8119
|
+
completedAt: null
|
|
8120
|
+
}
|
|
7863
8121
|
}
|
|
7864
8122
|
};
|
|
7865
8123
|
}
|
|
@@ -7894,6 +8152,29 @@ function mockResponse(method, path20, body5) {
|
|
|
7894
8152
|
}
|
|
7895
8153
|
};
|
|
7896
8154
|
}
|
|
8155
|
+
if (process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "blocking-only") {
|
|
8156
|
+
return {
|
|
8157
|
+
ok: true,
|
|
8158
|
+
status: 200,
|
|
8159
|
+
data: {
|
|
8160
|
+
ok: false,
|
|
8161
|
+
experimentId: exp.id,
|
|
8162
|
+
status: "invalid",
|
|
8163
|
+
changes: [],
|
|
8164
|
+
errors: [],
|
|
8165
|
+
warnings: [],
|
|
8166
|
+
blockingIssues: [{
|
|
8167
|
+
code: "custom_js_parse_error",
|
|
8168
|
+
severity: "blocking",
|
|
8169
|
+
message: "Custom JavaScript does not parse.",
|
|
8170
|
+
path: "variations/control/index.js",
|
|
8171
|
+
variation: "control"
|
|
8172
|
+
}],
|
|
8173
|
+
invalidatesQa: false,
|
|
8174
|
+
precompileRequired: false
|
|
8175
|
+
}
|
|
8176
|
+
};
|
|
8177
|
+
}
|
|
7897
8178
|
const observedPaths = files.map((file) => file.path).filter((filePath) => typeof filePath === "string");
|
|
7898
8179
|
return {
|
|
7899
8180
|
ok: true,
|
|
@@ -7999,6 +8280,49 @@ function mockResponse(method, path20, body5) {
|
|
|
7999
8280
|
}
|
|
8000
8281
|
};
|
|
8001
8282
|
}
|
|
8283
|
+
if (url.pathname === "/api/v1/consent" && (method === "GET" || method === "PATCH")) {
|
|
8284
|
+
const patch = body5;
|
|
8285
|
+
const enabled = typeof patch?.consentMode?.enabled === "boolean" ? patch.consentMode.enabled : true;
|
|
8286
|
+
const provider = typeof patch?.consentMode?.provider === "string" ? patch.consentMode.provider : "cookiescript";
|
|
8287
|
+
const requiredGroup = typeof patch?.consentMode?.requiredGroup === "string" ? patch.consentMode.requiredGroup : "analytics";
|
|
8288
|
+
const getter = typeof patch?.consentMode?.getter === "string" ? patch.consentMode.getter : "";
|
|
8289
|
+
if (method === "PATCH" && patch?.applyCanonicalSplit === false) {
|
|
8290
|
+
return { ok: false, status: 403, error: "applyCanonicalSplit: false is not permitted with the consent:write scope \u2014 clear those prerequisites in the dashboard (shop:settings)." };
|
|
8291
|
+
}
|
|
8292
|
+
return { ok: true, status: 200, data: {
|
|
8293
|
+
consentMode: { enabled, provider, requiredGroup, getter },
|
|
8294
|
+
prerequisites: {
|
|
8295
|
+
showVariationsRule: "return true;",
|
|
8296
|
+
startTrackingRule: "return drip.get('trackingConsentEnabled');"
|
|
8297
|
+
},
|
|
8298
|
+
...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {}
|
|
8299
|
+
} };
|
|
8300
|
+
}
|
|
8301
|
+
if (url.pathname === "/api/v1/install-domains" && (method === "GET" || method === "PATCH")) {
|
|
8302
|
+
const conflictMode = process.env.APEX_CLI_MOCK_DOMAINS_CONFLICT;
|
|
8303
|
+
const serverDomains = conflictMode === "once" && mockDomainsPatchAttempts >= 1 ? ["shop.example.com", "peer.example.com"] : ["shop.example.com"];
|
|
8304
|
+
if (method === "PATCH") {
|
|
8305
|
+
mockDomainsPatchAttempts += 1;
|
|
8306
|
+
if (conflictMode === "always" || conflictMode === "once" && mockDomainsPatchAttempts === 1) {
|
|
8307
|
+
return { ok: false, status: 409, error: "Install domains changed since read \u2014 re-read and retry" };
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
8310
|
+
const domains = body5?.domains ?? serverDomains;
|
|
8311
|
+
const additionalDomains = method === "PATCH" ? domains : serverDomains;
|
|
8312
|
+
return { ok: true, status: 200, data: {
|
|
8313
|
+
domain: "example.com",
|
|
8314
|
+
additionalDomains,
|
|
8315
|
+
storeOrigins: ["https://example.com", "https://www.example.com", ...additionalDomains.map((domain) => `https://${domain}`)],
|
|
8316
|
+
revision: method === "PATCH" ? "rev_patched" : `rev_${serverDomains.length}`,
|
|
8317
|
+
...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {}
|
|
8318
|
+
} };
|
|
8319
|
+
}
|
|
8320
|
+
if (url.pathname === "/api/v1/external-tracking" && (method === "GET" || method === "PATCH")) {
|
|
8321
|
+
const current = { domains: ["www.example.com", "shop.example.com"], handoverQueryParam: "drip_bucket", handoverTtlSeconds: 120, handoverMintTimeoutMs: 250, signed: true };
|
|
8322
|
+
const patch = body5?.externalTracking;
|
|
8323
|
+
const next = { ...current, ...patch ?? {} };
|
|
8324
|
+
return { ok: true, status: 200, data: { externalTracking: next, revision: "2026-08-05T10:00:00.000Z", ...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {} } };
|
|
8325
|
+
}
|
|
8002
8326
|
const analyticsMatch = url.pathname.match(/^\/api\/v1\/analytics\/(stats|timeseries|traffic|pages|geo|sessions|web-vitals|realtime|entry-exit)$/);
|
|
8003
8327
|
if (method === "GET" && analyticsMatch) {
|
|
8004
8328
|
const fixtures = {
|
|
@@ -8014,12 +8338,119 @@ function mockResponse(method, path20, body5) {
|
|
|
8014
8338
|
};
|
|
8015
8339
|
return { ok: true, status: 200, data: { data: fixtures[analyticsMatch[1]] } };
|
|
8016
8340
|
}
|
|
8341
|
+
if (method === "GET" && url.pathname === "/api/v1/analytics/url-inventory") {
|
|
8342
|
+
return { ok: true, status: 200, data: { items: [
|
|
8343
|
+
{ host: "getkong.test", path: "/bestellung-abgeschlossen", pageviews: 12, visitors: 10 },
|
|
8344
|
+
{ host: "getkong.test", path: "/products/demo", pageviews: 7, visitors: 5 }
|
|
8345
|
+
], total: 2, truncated: false } };
|
|
8346
|
+
}
|
|
8347
|
+
if (method === "POST" && url.pathname === "/api/v1/analytics/suggest-goals") {
|
|
8348
|
+
return { ok: true, status: 200, data: { goals: [{ name: "Purchase", type: "pageview", urlPattern: "/bestellung-abgeschlossen", explanation: "Fixture" }], funnels: [], triggers: [] } };
|
|
8349
|
+
}
|
|
8350
|
+
if (method === "GET" && url.pathname === "/api/v1/funnels") {
|
|
8351
|
+
return { ok: true, status: 200, data: { data: [mockFunnel()] } };
|
|
8352
|
+
}
|
|
8353
|
+
if (method === "POST" && url.pathname === "/api/v1/funnels") {
|
|
8354
|
+
return { ok: true, status: 201, data: { ...mockFunnel(), ...body5 } };
|
|
8355
|
+
}
|
|
8356
|
+
if (method === "GET" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
|
|
8357
|
+
return { ok: true, status: 200, data: mockFunnel() };
|
|
8358
|
+
}
|
|
8359
|
+
if (method === "PATCH" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
|
|
8360
|
+
return { ok: true, status: 200, data: { ...mockFunnel(), ...body5 } };
|
|
8361
|
+
}
|
|
8362
|
+
if (method === "DELETE" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
|
|
8363
|
+
return { ok: true, status: 200, data: { ok: true } };
|
|
8364
|
+
}
|
|
8365
|
+
if (method === "GET" && url.pathname === `/api/v1/funnels/${mockFunnel().id}/run`) {
|
|
8366
|
+
return { ok: true, status: 200, data: { total_sessions_analyzed: 2, steps: [{ step: 1, label: "Collection", url_pattern: "/collections/*", visitors: 2, drop_off_rate: 0, matched_pageviews: 2 }] } };
|
|
8367
|
+
}
|
|
8368
|
+
if (method === "GET" && url.pathname.endsWith("/workspace")) {
|
|
8369
|
+
const files = [
|
|
8370
|
+
["experiment.config.json", JSON.stringify({ name: exp.name, storeUrl: exp.store_url, experimentSlug: exp.slug, variationSlug: "badge" }, null, 2) + "\n", "application/json", "config"],
|
|
8371
|
+
["metrics.json", '{"primary":null,"secondary":[]}\n', "application/json", "metrics"],
|
|
8372
|
+
["targeting.json", '{"url":[],"segments":[]}\n', "application/json", "targeting"],
|
|
8373
|
+
["src/index.js", "window.__apexOperatorFixture = true;\n", "application/javascript", "custom_js"],
|
|
8374
|
+
["src/index.scss", ".apex-operator-fixture { display: block; }\n", "text/x-scss", "custom_css"],
|
|
8375
|
+
["src/mutations.json", "[]\n", "application/json", "mutations"],
|
|
8376
|
+
["dev-doc.md", "# Fixture workspace\n", "text/markdown", "doc"],
|
|
8377
|
+
["brand.md", "# Fixture brand\n", "text/markdown", "brand"]
|
|
8378
|
+
];
|
|
8379
|
+
return {
|
|
8380
|
+
ok: true,
|
|
8381
|
+
status: 200,
|
|
8382
|
+
data: {
|
|
8383
|
+
workspace: {
|
|
8384
|
+
id: "workspace_1",
|
|
8385
|
+
schemaVersion: 1,
|
|
8386
|
+
status: "draft",
|
|
8387
|
+
currentRevisionId: "revision_1",
|
|
8388
|
+
updatedAt: "2026-07-30T00:00:00.000Z"
|
|
8389
|
+
},
|
|
8390
|
+
files: files.map(([filePath, content, contentType, kind5]) => ({
|
|
8391
|
+
path: filePath,
|
|
8392
|
+
kind: kind5,
|
|
8393
|
+
contentType,
|
|
8394
|
+
content,
|
|
8395
|
+
sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex"),
|
|
8396
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
8397
|
+
updatedAt: "2026-07-30T00:00:00.000Z"
|
|
8398
|
+
}))
|
|
8399
|
+
}
|
|
8400
|
+
};
|
|
8401
|
+
}
|
|
8402
|
+
if (method === "PUT" && url.pathname.includes("/variations/") && url.pathname.includes("/files/")) {
|
|
8403
|
+
return { ok: true, status: 200, data: { ok: true } };
|
|
8404
|
+
}
|
|
8405
|
+
if (method === "POST" && url.pathname.endsWith("/build")) {
|
|
8406
|
+
return {
|
|
8407
|
+
ok: true,
|
|
8408
|
+
status: 200,
|
|
8409
|
+
data: { ok: true, mutationCount: 0, customCssBytes: 42, customJsBytes: 58 }
|
|
8410
|
+
};
|
|
8411
|
+
}
|
|
8412
|
+
if (method === "POST" && url.pathname === "/api/v1/harness/sessions") {
|
|
8413
|
+
return { ok: true, status: 201, data: { sessionId: "harness_1", sessionToken: "token", experimentId: exp.id, variationId: exp.variations?.[1]?.id ?? "variation_1", expiresAt: "2026-07-30T01:00:00.000Z", previewUrl: "https://preview.drip-apex.com/api/preview?pst=harness_1" } };
|
|
8414
|
+
}
|
|
8415
|
+
if (method === "POST" && url.pathname === "/api/v1/harness/sessions/harness_1/versions") {
|
|
8416
|
+
return { ok: true, status: 201, data: { sessionId: "harness_1", version: 1, expiresAt: "2026-07-30T01:00:00.000Z", jsArtifactId: "asset_js", cssArtifactId: "asset_css" } };
|
|
8417
|
+
}
|
|
8418
|
+
if (method === "POST" && url.pathname === "/api/v1/harness/sessions/harness_1/publish") {
|
|
8419
|
+
return { ok: true, status: 200, data: { ok: true } };
|
|
8420
|
+
}
|
|
8017
8421
|
if (method === "GET" && url.pathname === "/api/v1/goals") {
|
|
8422
|
+
if (process.env.APEX_CLI_MOCK_GOAL_PAGES === "2") {
|
|
8423
|
+
if (!url.searchParams.get("cursor")) {
|
|
8424
|
+
return {
|
|
8425
|
+
ok: true,
|
|
8426
|
+
status: 200,
|
|
8427
|
+
data: {
|
|
8428
|
+
data: [mockGoal()],
|
|
8429
|
+
pagination: { has_more: true, next_cursor: "goals_page_2" }
|
|
8430
|
+
}
|
|
8431
|
+
};
|
|
8432
|
+
}
|
|
8433
|
+
return {
|
|
8434
|
+
ok: true,
|
|
8435
|
+
status: 200,
|
|
8436
|
+
data: {
|
|
8437
|
+
data: [{
|
|
8438
|
+
...mockGoal(),
|
|
8439
|
+
id: SECOND_PAGE_GOAL_ID,
|
|
8440
|
+
name: "Fixture pageview",
|
|
8441
|
+
type: "pageview",
|
|
8442
|
+
event_name: null,
|
|
8443
|
+
url_pattern: "bestellung-abgeschlossen"
|
|
8444
|
+
}],
|
|
8445
|
+
pagination: { has_more: false, next_cursor: null }
|
|
8446
|
+
}
|
|
8447
|
+
};
|
|
8448
|
+
}
|
|
8018
8449
|
return {
|
|
8019
8450
|
ok: true,
|
|
8020
8451
|
status: 200,
|
|
8021
8452
|
data: {
|
|
8022
|
-
data: [mockGoal()],
|
|
8453
|
+
data: [mockGoal(), mockPageScopedGoal()],
|
|
8023
8454
|
pagination: { has_more: false, next_cursor: null }
|
|
8024
8455
|
}
|
|
8025
8456
|
};
|
|
@@ -8195,8 +8626,8 @@ function getUserKeyIdentity(creds2) {
|
|
|
8195
8626
|
"/api/v1/me"
|
|
8196
8627
|
);
|
|
8197
8628
|
}
|
|
8198
|
-
function userRequest(creds2, method,
|
|
8199
|
-
return request({ ...creds2, apiKey: creds2.userApiKey ?? "", baseUrl: creds2.userBaseUrl ?? creds2.baseUrl }, method,
|
|
8629
|
+
function userRequest(creds2, method, path21, body5) {
|
|
8630
|
+
return request({ ...creds2, apiKey: creds2.userApiKey ?? "", baseUrl: creds2.userBaseUrl ?? creds2.baseUrl }, method, path21, body5);
|
|
8200
8631
|
}
|
|
8201
8632
|
function getAccountProfile(creds2) {
|
|
8202
8633
|
return userRequest(creds2, "GET", "/api/v1/account/profile");
|
|
@@ -8213,24 +8644,39 @@ function createUserManagedKey(creds2, body5) {
|
|
|
8213
8644
|
function revokeUserManagedKey(creds2, id, shopId) {
|
|
8214
8645
|
return userRequest(creds2, "DELETE", `/api/v1/keys/${encodeURIComponent(id)}?shop_id=${encodeURIComponent(shopId)}`);
|
|
8215
8646
|
}
|
|
8216
|
-
async function request(creds2, method,
|
|
8217
|
-
const mock = mockResponse(method,
|
|
8647
|
+
async function request(creds2, method, path21, body5, requestTimeoutMs) {
|
|
8648
|
+
const mock = mockResponse(method, path21, body5);
|
|
8218
8649
|
if (mock) return mock;
|
|
8219
|
-
const url = `${creds2.baseUrl}${
|
|
8650
|
+
const url = `${creds2.baseUrl}${path21}`;
|
|
8651
|
+
const configuredTimeout = Number(process.env.APEX_TIMEOUT_MS ?? "30000");
|
|
8652
|
+
const defaultTimeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 3e4;
|
|
8653
|
+
const timeoutMs = requestTimeoutMs !== void 0 ? Math.max(1, Math.min(defaultTimeoutMs, requestTimeoutMs)) : defaultTimeoutMs;
|
|
8220
8654
|
const headers = {
|
|
8221
8655
|
Authorization: `Bearer ${creds2.apiKey}`,
|
|
8222
8656
|
"Content-Type": "application/json"
|
|
8223
8657
|
};
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8658
|
+
let res;
|
|
8659
|
+
try {
|
|
8660
|
+
res = await fetch(url, {
|
|
8661
|
+
method,
|
|
8662
|
+
headers,
|
|
8663
|
+
body: body5 ? JSON.stringify(body5) : void 0,
|
|
8664
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
8665
|
+
});
|
|
8666
|
+
} catch (error) {
|
|
8667
|
+
const seconds = timeoutMs / 1e3;
|
|
8668
|
+
throw new CliError(
|
|
8669
|
+
`Could not reach ${creds2.baseUrl}: check DRIP_BASE_URL / network; server did not respond within ${seconds}s.`,
|
|
8670
|
+
EXIT_CODES.general,
|
|
8671
|
+
{ baseUrl: creds2.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) }
|
|
8672
|
+
);
|
|
8673
|
+
}
|
|
8229
8674
|
const json = await res.json().catch(() => null);
|
|
8230
8675
|
if (!res.ok) {
|
|
8231
8676
|
return {
|
|
8232
8677
|
ok: false,
|
|
8233
8678
|
status: res.status,
|
|
8679
|
+
data: json,
|
|
8234
8680
|
error: extractErrorMessage(json, res.status),
|
|
8235
8681
|
errorDetails: extractErrorDetails(json),
|
|
8236
8682
|
requestId: res.headers.get("x-request-id"),
|
|
@@ -8249,15 +8695,28 @@ async function request(creds2, method, path20, body5) {
|
|
|
8249
8695
|
data: json
|
|
8250
8696
|
};
|
|
8251
8697
|
}
|
|
8252
|
-
async function requestForm(creds2, method,
|
|
8253
|
-
const mock = mockResponse(method,
|
|
8698
|
+
async function requestForm(creds2, method, path21, body5) {
|
|
8699
|
+
const mock = mockResponse(method, path21, body5);
|
|
8254
8700
|
if (mock) return mock;
|
|
8255
|
-
const url = `${creds2.baseUrl}${
|
|
8256
|
-
const
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8701
|
+
const url = `${creds2.baseUrl}${path21}`;
|
|
8702
|
+
const configuredTimeout = Number(process.env.APEX_TIMEOUT_MS ?? "120000");
|
|
8703
|
+
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 12e4;
|
|
8704
|
+
let res;
|
|
8705
|
+
try {
|
|
8706
|
+
res = await fetch(url, {
|
|
8707
|
+
method,
|
|
8708
|
+
headers: { Authorization: `Bearer ${creds2.apiKey}` },
|
|
8709
|
+
body: body5,
|
|
8710
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
8711
|
+
});
|
|
8712
|
+
} catch (error) {
|
|
8713
|
+
const seconds = timeoutMs / 1e3;
|
|
8714
|
+
throw new CliError(
|
|
8715
|
+
`Could not reach ${creds2.baseUrl}: check DRIP_BASE_URL / network; server did not respond within ${seconds}s.`,
|
|
8716
|
+
EXIT_CODES.general,
|
|
8717
|
+
{ baseUrl: creds2.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) }
|
|
8718
|
+
);
|
|
8719
|
+
}
|
|
8261
8720
|
const json = await res.json().catch(() => null);
|
|
8262
8721
|
if (!res.ok) {
|
|
8263
8722
|
return {
|
|
@@ -8280,6 +8739,7 @@ function listExperiments(creds2, opts) {
|
|
|
8280
8739
|
if (opts?.status) params.set("status", opts.status);
|
|
8281
8740
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
8282
8741
|
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
8742
|
+
if (opts?.includeArchived) params.set("archived", "1");
|
|
8283
8743
|
const qs = params.toString();
|
|
8284
8744
|
return request(
|
|
8285
8745
|
creds2,
|
|
@@ -8341,6 +8801,15 @@ function getExperimentResults(creds2, id, opts) {
|
|
|
8341
8801
|
`/api/v1/experiments/${id}/results${qs ? `?${qs}` : ""}`
|
|
8342
8802
|
);
|
|
8343
8803
|
}
|
|
8804
|
+
function createExperimentScreenshots(creds2, id, body5) {
|
|
8805
|
+
return request(creds2, "POST", `/api/v1/experiments/${id}/screenshots`, body5);
|
|
8806
|
+
}
|
|
8807
|
+
async function getScreenshotJob(creds2, jobId, timeoutMs) {
|
|
8808
|
+
if (process.env.APEX_CLI_MOCK_API === "1" && ["deadline-processing", "deadline-completed"].includes(process.env.APEX_CLI_MOCK_SCREENSHOTS ?? "")) {
|
|
8809
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
8810
|
+
}
|
|
8811
|
+
return request(creds2, "GET", `/api/v1/screenshots/${jobId}`, void 0, timeoutMs);
|
|
8812
|
+
}
|
|
8344
8813
|
function planExperimentFilesystem(creds2, id, body5) {
|
|
8345
8814
|
return request(
|
|
8346
8815
|
creds2,
|
|
@@ -8417,13 +8886,32 @@ function putOperatorWorkspaceFile(creds2, experimentId, variationId, filePath, b
|
|
|
8417
8886
|
body5
|
|
8418
8887
|
);
|
|
8419
8888
|
}
|
|
8420
|
-
function buildOperatorWorkspace(creds2, experimentId, variationId) {
|
|
8889
|
+
function buildOperatorWorkspace(creds2, experimentId, variationId, body5) {
|
|
8421
8890
|
return request(
|
|
8422
8891
|
creds2,
|
|
8423
8892
|
"POST",
|
|
8424
|
-
`/api/v1/experiments/${experimentId}/variations/${variationId}/build
|
|
8893
|
+
`/api/v1/experiments/${experimentId}/variations/${variationId}/build`,
|
|
8894
|
+
body5
|
|
8425
8895
|
);
|
|
8426
8896
|
}
|
|
8897
|
+
function listFunnels(creds2) {
|
|
8898
|
+
return request(creds2, "GET", "/api/v1/funnels");
|
|
8899
|
+
}
|
|
8900
|
+
function createFunnel(creds2, body5) {
|
|
8901
|
+
return request(creds2, "POST", "/api/v1/funnels", body5);
|
|
8902
|
+
}
|
|
8903
|
+
function updateFunnel(creds2, id, body5) {
|
|
8904
|
+
return request(creds2, "PATCH", `/api/v1/funnels/${encodeURIComponent(id)}`, body5);
|
|
8905
|
+
}
|
|
8906
|
+
function deleteFunnel(creds2, id) {
|
|
8907
|
+
return request(creds2, "DELETE", `/api/v1/funnels/${encodeURIComponent(id)}`);
|
|
8908
|
+
}
|
|
8909
|
+
function runFunnel(creds2, id, days) {
|
|
8910
|
+
const params = new URLSearchParams();
|
|
8911
|
+
if (days) params.set("days", String(days));
|
|
8912
|
+
const qs = params.toString();
|
|
8913
|
+
return request(creds2, "GET", `/api/v1/funnels/${encodeURIComponent(id)}/run${qs ? `?${qs}` : ""}`);
|
|
8914
|
+
}
|
|
8427
8915
|
function listGoals(creds2, opts) {
|
|
8428
8916
|
const params = new URLSearchParams();
|
|
8429
8917
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
@@ -8442,6 +8930,14 @@ function updateGoal(creds2, id, body5) {
|
|
|
8442
8930
|
body5
|
|
8443
8931
|
);
|
|
8444
8932
|
}
|
|
8933
|
+
function getUrlInventory(creds2, opts) {
|
|
8934
|
+
const params = new URLSearchParams();
|
|
8935
|
+
for (const [key, value] of Object.entries(opts)) if (value !== void 0) params.set(key, String(value));
|
|
8936
|
+
return request(creds2, "GET", `/api/v1/analytics/url-inventory?${params.toString()}`);
|
|
8937
|
+
}
|
|
8938
|
+
function suggestGoals(creds2) {
|
|
8939
|
+
return request(creds2, "POST", "/api/v1/analytics/suggest-goals", {});
|
|
8940
|
+
}
|
|
8445
8941
|
function getBrandContext(creds2) {
|
|
8446
8942
|
return request(creds2, "GET", "/api/v1/brand-context");
|
|
8447
8943
|
}
|
|
@@ -8456,6 +8952,24 @@ function updateEnvironments(creds2, body5) {
|
|
|
8456
8952
|
body5
|
|
8457
8953
|
);
|
|
8458
8954
|
}
|
|
8955
|
+
function getConsent(creds2) {
|
|
8956
|
+
return request(creds2, "GET", "/api/v1/consent");
|
|
8957
|
+
}
|
|
8958
|
+
function updateConsent(creds2, body5) {
|
|
8959
|
+
return request(creds2, "PATCH", "/api/v1/consent", body5);
|
|
8960
|
+
}
|
|
8961
|
+
function getInstallDomains(creds2) {
|
|
8962
|
+
return request(creds2, "GET", "/api/v1/install-domains");
|
|
8963
|
+
}
|
|
8964
|
+
function updateInstallDomains(creds2, body5) {
|
|
8965
|
+
return request(creds2, "PATCH", "/api/v1/install-domains", body5);
|
|
8966
|
+
}
|
|
8967
|
+
function getExternalTracking(creds2) {
|
|
8968
|
+
return request(creds2, "GET", "/api/v1/external-tracking");
|
|
8969
|
+
}
|
|
8970
|
+
function updateExternalTracking(creds2, body5) {
|
|
8971
|
+
return request(creds2, "PATCH", "/api/v1/external-tracking", body5);
|
|
8972
|
+
}
|
|
8459
8973
|
function verifyTracking(creds2, opts) {
|
|
8460
8974
|
const params = new URLSearchParams();
|
|
8461
8975
|
if (opts?.minutes) params.set("minutes", String(opts.minutes));
|
|
@@ -8521,7 +9035,22 @@ function listAuditLogs(creds2, opts) {
|
|
|
8521
9035
|
if (opts.entityId) p.set("entity_id", opts.entityId);
|
|
8522
9036
|
if (opts.limit) p.set("limit", String(opts.limit));
|
|
8523
9037
|
if (opts.offset) p.set("offset", String(opts.offset));
|
|
8524
|
-
|
|
9038
|
+
const query = p.size ? `?${p}` : "";
|
|
9039
|
+
return request(creds2, "GET", `/api/v1/audit-logs${query}`).then(async (shopResult) => {
|
|
9040
|
+
if (shopResult.ok || shopResult.status !== 401 && shopResult.status !== 403) return shopResult;
|
|
9041
|
+
const shopFailure = `Shop key failed: missing audit_logs:read; create a secret shop key with audit_logs:read.`;
|
|
9042
|
+
if (!creds2.userApiKey) return { ...shopResult, error: shopFailure };
|
|
9043
|
+
if (!opts.shopId) return { ...shopResult, error: `${shopFailure} User key was not tried: pass --shop <shop-id> to select an authorized shop.` };
|
|
9044
|
+
const userResult = await userRequest(creds2, "GET", `/api/v1/shops/${encodeURIComponent(opts.shopId)}/audit-logs${query}`);
|
|
9045
|
+
if (userResult.ok) return userResult;
|
|
9046
|
+
const userPathFailure = `User-key shop audit-log path failed: ${userResult.error}`;
|
|
9047
|
+
if (userResult.status === 403 || userResult.status === 404) {
|
|
9048
|
+
return { ...userResult, error: `${shopFailure} ${userPathFailure} Re-run \`apex login --user\`, select the audit_logs:read scope, and use a member of shop ${opts.shopId}.` };
|
|
9049
|
+
}
|
|
9050
|
+
const staleKeyGuidance = userResult.status === 401 ? " Re-run `apex login --user` and select the audit_logs:read scope." : "";
|
|
9051
|
+
const separator = userResult.status === 401 && !/[.!?]$/.test(userPathFailure) ? "." : "";
|
|
9052
|
+
return { ...userResult, error: `${userPathFailure}${separator}${staleKeyGuidance}` };
|
|
9053
|
+
});
|
|
8525
9054
|
}
|
|
8526
9055
|
function listInbox(creds2, experimentId) {
|
|
8527
9056
|
const q = experimentId ? `?experiment_id=${encodeURIComponent(experimentId)}` : "";
|
|
@@ -8568,11 +9097,11 @@ function requestTargetingResource(creds2, method, kind5, id, body5, limit, curso
|
|
|
8568
9097
|
if (limit) params.set("limit", String(limit));
|
|
8569
9098
|
if (cursor) params.set("cursor", cursor);
|
|
8570
9099
|
const query = params.toString();
|
|
8571
|
-
const
|
|
8572
|
-
return request(creds2, method,
|
|
9100
|
+
const path21 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
|
|
9101
|
+
return request(creds2, method, path21, body5);
|
|
8573
9102
|
}
|
|
8574
|
-
function managedPath(kind5, id,
|
|
8575
|
-
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${
|
|
9103
|
+
function managedPath(kind5, id, action68) {
|
|
9104
|
+
return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action68 ? `/${action68}` : ""}`;
|
|
8576
9105
|
}
|
|
8577
9106
|
function listManagedDeliveries(creds2, kind5, limit) {
|
|
8578
9107
|
const q = limit ? `?limit=${limit}` : "";
|
|
@@ -8594,8 +9123,8 @@ function createManagedDelivery(creds2, kind5, body5) {
|
|
|
8594
9123
|
function updateManagedDelivery(creds2, kind5, id, body5) {
|
|
8595
9124
|
return request(creds2, "PATCH", managedPath(kind5, id), body5);
|
|
8596
9125
|
}
|
|
8597
|
-
function transitionManagedDelivery(creds2, kind5, id,
|
|
8598
|
-
return request(creds2, "POST", managedPath(kind5, id,
|
|
9126
|
+
function transitionManagedDelivery(creds2, kind5, id, action68) {
|
|
9127
|
+
return request(creds2, "POST", managedPath(kind5, id, action68));
|
|
8599
9128
|
}
|
|
8600
9129
|
function listConfigResources(creds2, family, opts) {
|
|
8601
9130
|
const params = new URLSearchParams();
|
|
@@ -8763,6 +9292,24 @@ var postShopArea = (c, id, area, body5) => userRequest(
|
|
|
8763
9292
|
`/api/v1/shops/${id}/${area}`,
|
|
8764
9293
|
body5
|
|
8765
9294
|
);
|
|
9295
|
+
function listWebhooks(creds2) {
|
|
9296
|
+
return request(creds2, "GET", "/api/v1/webhooks");
|
|
9297
|
+
}
|
|
9298
|
+
function createWebhook(creds2, body5) {
|
|
9299
|
+
return request(creds2, "POST", "/api/v1/webhooks", body5);
|
|
9300
|
+
}
|
|
9301
|
+
function updateWebhook(creds2, id, body5) {
|
|
9302
|
+
return request(creds2, "PATCH", `/api/v1/webhooks/${encodeURIComponent(id)}`, body5);
|
|
9303
|
+
}
|
|
9304
|
+
function deleteWebhook(creds2, id) {
|
|
9305
|
+
return request(creds2, "DELETE", `/api/v1/webhooks/${encodeURIComponent(id)}`);
|
|
9306
|
+
}
|
|
9307
|
+
function getWebhookDeliveries(creds2, id, limit) {
|
|
9308
|
+
return request(creds2, "GET", `/api/v1/webhooks/${encodeURIComponent(id)}/deliveries?limit=${encodeURIComponent(String(limit))}`);
|
|
9309
|
+
}
|
|
9310
|
+
function testWebhook(creds2, id) {
|
|
9311
|
+
return request(creds2, "POST", `/api/v1/webhooks/${encodeURIComponent(id)}/test`);
|
|
9312
|
+
}
|
|
8766
9313
|
|
|
8767
9314
|
// src/ui.ts
|
|
8768
9315
|
init_source();
|
|
@@ -8835,8 +9382,8 @@ function prompt(question, hide = false) {
|
|
|
8835
9382
|
}
|
|
8836
9383
|
});
|
|
8837
9384
|
}
|
|
8838
|
-
function printValidationFailure(res) {
|
|
8839
|
-
printError(`${res.error ?? `HTTP ${res.status}`} (HTTP ${res.status})`);
|
|
9385
|
+
function printValidationFailure(res, remedy = "") {
|
|
9386
|
+
printError(`${res.error ?? `HTTP ${res.status}`} (HTTP ${res.status})${remedy}`);
|
|
8840
9387
|
if (res.status === 429) {
|
|
8841
9388
|
printWarning(
|
|
8842
9389
|
"The API key reached Apex, but the validation request was rate-limited. If this happens on first login, check the server-side rate-limit backend/configuration."
|
|
@@ -9021,7 +9568,7 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
|
|
|
9021
9568
|
spinner.fail(
|
|
9022
9569
|
res.status === 401 ? "Authentication failed" : "API key validation failed"
|
|
9023
9570
|
);
|
|
9024
|
-
printValidationFailure(res);
|
|
9571
|
+
printValidationFailure(res, " Run `apex login`.");
|
|
9025
9572
|
fail(this, res.error ?? "API key validation failed", classifyStatus(res.status), res.errorDetails);
|
|
9026
9573
|
}
|
|
9027
9574
|
spinner.succeed("Authenticated successfully");
|
|
@@ -9029,7 +9576,13 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
|
|
|
9029
9576
|
output(this, { baseUrl, credentialsFile: "~/.drip/credentials.json" }, () => {
|
|
9030
9577
|
printSuccess(`Credentials saved to ~/.drip/credentials.json`);
|
|
9031
9578
|
});
|
|
9032
|
-
})
|
|
9579
|
+
}).addHelpText("after", `
|
|
9580
|
+
Examples:
|
|
9581
|
+
apex login
|
|
9582
|
+
apex login --user
|
|
9583
|
+
apex login --api-key apx_your_key_here
|
|
9584
|
+
apex login --base-url https://app.drip-apex.com
|
|
9585
|
+
`);
|
|
9033
9586
|
var logoutCommand = new Command("logout").description("Remove stored credentials").option("--user", "Remove only the stored user API key").action(function action2(opts) {
|
|
9034
9587
|
const existing = getStoredCredentials();
|
|
9035
9588
|
if (!existing) {
|
|
@@ -9072,7 +9625,7 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
|
|
|
9072
9625
|
if (res?.ok) {
|
|
9073
9626
|
printSuccess("Key is valid");
|
|
9074
9627
|
} else {
|
|
9075
|
-
if (res) printValidationFailure(res);
|
|
9628
|
+
if (res) printValidationFailure(res, " Run `apex login`.");
|
|
9076
9629
|
}
|
|
9077
9630
|
} else {
|
|
9078
9631
|
console.log("API Key: none (user credential only)");
|
|
@@ -9080,7 +9633,7 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
|
|
|
9080
9633
|
if (creds2.userApiKey) {
|
|
9081
9634
|
console.log(`User Key: ${source_default.dim(creds2.userApiKey.slice(0, 11) + "..." + creds2.userApiKey.slice(-4))}`);
|
|
9082
9635
|
if (userRes?.ok) console.log(`User Org: ${source_default.cyan(userRes.data.data.org_id)} (${userRes.data.data.org_role})`);
|
|
9083
|
-
else printWarning("User key could not be verified.");
|
|
9636
|
+
else printWarning("User key could not be verified. Run `apex login --user`.");
|
|
9084
9637
|
}
|
|
9085
9638
|
});
|
|
9086
9639
|
});
|
|
@@ -9119,22 +9672,40 @@ function printExperiment(exp) {
|
|
|
9119
9672
|
function formatPct(value, digits = 1) {
|
|
9120
9673
|
return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}%` : "-";
|
|
9121
9674
|
}
|
|
9675
|
+
function confidenceBasisLabel(value) {
|
|
9676
|
+
if (value === "fixed_horizon") return "fixed-horizon";
|
|
9677
|
+
if (value === "sequential") return "always-valid";
|
|
9678
|
+
return "unknown";
|
|
9679
|
+
}
|
|
9680
|
+
function statisticsStateLabel(value) {
|
|
9681
|
+
return value === "untrustworthy" ? "verdict withheld" : String(value ?? "unknown");
|
|
9682
|
+
}
|
|
9122
9683
|
function printExperimentResults(results2) {
|
|
9123
9684
|
const experiment = results2.experiment ?? {};
|
|
9124
9685
|
const totals = results2.totals ?? {};
|
|
9125
9686
|
const decision = results2.decision ?? {};
|
|
9687
|
+
const effectiveOutcome = results2.effective_outcome ?? {};
|
|
9688
|
+
const outcomeProvenance = effectiveOutcome.provenance === "manager_decided" ? " (manager decided)" : "";
|
|
9689
|
+
const verdictWithheld = decision.state === "untrustworthy";
|
|
9690
|
+
const statisticsState = verdictWithheld ? decision.state : effectiveOutcome.statisticsOutcome ?? decision.state ?? "unknown";
|
|
9691
|
+
const presentedOutcome = verdictWithheld && effectiveOutcome.provenance !== "manager_decided" ? decision.state : effectiveOutcome.outcome ?? decision.state ?? "unknown";
|
|
9126
9692
|
console.log();
|
|
9127
9693
|
console.log(source_default.bold(String(experiment.name ?? experiment.id ?? "Experiment results")));
|
|
9128
9694
|
console.log(` Status: ${statusBadge(String(experiment.status ?? "unknown"))}`);
|
|
9129
9695
|
console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
|
|
9130
9696
|
console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
|
|
9131
9697
|
console.log(` Revenue: ${String(totals.revenue ?? 0)}`);
|
|
9132
|
-
console.log(`
|
|
9698
|
+
console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
|
|
9699
|
+
console.log(
|
|
9700
|
+
` Statistics: ${statisticsStateLabel(statisticsState)}`
|
|
9701
|
+
);
|
|
9133
9702
|
if (decision.treatmentName) {
|
|
9134
9703
|
console.log(` Treatment: ${String(decision.treatmentName)}`);
|
|
9135
9704
|
}
|
|
9136
|
-
console.log(`
|
|
9137
|
-
console.log(
|
|
9705
|
+
console.log(` Stats lift: ${formatPct(decision.lift)}`);
|
|
9706
|
+
console.log(
|
|
9707
|
+
` Confidence (${confidenceBasisLabel(decision.basis)}): ${verdictWithheld ? "withheld \u2014 not decision-grade" : formatPct(decision.judgedConfidence)}`
|
|
9708
|
+
);
|
|
9138
9709
|
const arms = Array.isArray(results2.arms) ? results2.arms : [];
|
|
9139
9710
|
if (arms.length > 0) {
|
|
9140
9711
|
console.log();
|
|
@@ -9164,6 +9735,7 @@ var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
|
|
|
9164
9735
|
variation: [
|
|
9165
9736
|
"variation.json",
|
|
9166
9737
|
"mutations.json",
|
|
9738
|
+
"url-blocks.json",
|
|
9167
9739
|
"index.css",
|
|
9168
9740
|
"index.js",
|
|
9169
9741
|
"reset.js",
|
|
@@ -9204,6 +9776,10 @@ function writeFilesystemSession(folder, session) {
|
|
|
9204
9776
|
function readFilesystemSession(folder) {
|
|
9205
9777
|
const filePath = filesystemSessionPath(folder);
|
|
9206
9778
|
if (!import_node_fs6.default.existsSync(filePath)) {
|
|
9779
|
+
const workspaceMarker = import_node_path4.default.join(filesystemApexDir(folder), "session.json");
|
|
9780
|
+
if (import_node_fs6.default.existsSync(workspaceMarker)) {
|
|
9781
|
+
throw new Error("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.");
|
|
9782
|
+
}
|
|
9207
9783
|
throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
|
|
9208
9784
|
}
|
|
9209
9785
|
return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
|
|
@@ -9269,47 +9845,50 @@ function printFilesystemPlan(plan) {
|
|
|
9269
9845
|
console.log(source_default.yellow("\nWarnings"));
|
|
9270
9846
|
plan.warnings.forEach((warning) => console.log(` - ${warning}`));
|
|
9271
9847
|
}
|
|
9848
|
+
if ((plan.blockingIssues?.length ?? 0) > 0) {
|
|
9849
|
+
console.log(source_default.red("\nBlocking issues"));
|
|
9850
|
+
(plan.blockingIssues ?? []).forEach((issue) => {
|
|
9851
|
+
const context = [issue.path, issue.variation ? `variation: ${issue.variation}` : null].filter(Boolean).join(", ");
|
|
9852
|
+
console.log(` - [${issue.code}] ${issue.message}${context ? ` (${context})` : ""}`);
|
|
9853
|
+
});
|
|
9854
|
+
}
|
|
9272
9855
|
console.log(source_default.bold("\nChanges"));
|
|
9273
9856
|
if (plan.changes.length === 0) {
|
|
9274
9857
|
console.log(source_default.dim(" No changes."));
|
|
9275
9858
|
} else {
|
|
9276
9859
|
plan.changes.forEach((change) => {
|
|
9277
9860
|
const pathLabel = change.path ? source_default.dim(` ${change.path}`) : "";
|
|
9278
|
-
const
|
|
9279
|
-
console.log(` ${source_default.cyan(change.action)} ${change.entity}${pathLabel} \u2014 ${change.summary}${
|
|
9861
|
+
const qa3 = change.invalidatesQa ? source_default.yellow(" invalidates QA") : "";
|
|
9862
|
+
console.log(` ${source_default.cyan(change.action)} ${change.entity}${pathLabel} \u2014 ${change.summary}${qa3}`);
|
|
9280
9863
|
});
|
|
9281
9864
|
}
|
|
9282
9865
|
console.log();
|
|
9283
9866
|
console.log(`QA invalidated: ${plan.invalidatesQa ? source_default.yellow("yes") : "no"}`);
|
|
9284
9867
|
console.log(`Precompile: ${plan.precompileRequired ? "yes" : "no"}`);
|
|
9285
9868
|
}
|
|
9286
|
-
function printQaRun(
|
|
9287
|
-
if (json) {
|
|
9288
|
-
console.log(JSON.stringify({ run: run2 }, null, 2));
|
|
9289
|
-
return;
|
|
9290
|
-
}
|
|
9869
|
+
function printQaRun(run3) {
|
|
9291
9870
|
console.log();
|
|
9292
|
-
console.log(source_default.bold(`QA run ${
|
|
9293
|
-
console.log(` Status: ${statusBadge(
|
|
9871
|
+
console.log(source_default.bold(`QA run ${run3.id}`));
|
|
9872
|
+
console.log(` Status: ${statusBadge(run3.summary.status)}`);
|
|
9294
9873
|
console.log(
|
|
9295
|
-
` Required: ${
|
|
9874
|
+
` Required: ${run3.summary.completedRequired}/${run3.summary.required}`
|
|
9296
9875
|
);
|
|
9297
|
-
if (
|
|
9298
|
-
if (
|
|
9299
|
-
if (
|
|
9876
|
+
if (run3.approvedAt) console.log(` Approved: ${run3.approvedAt}`);
|
|
9877
|
+
if (run3.completedAt) console.log(` Complete: ${run3.completedAt}`);
|
|
9878
|
+
if (run3.summary.blockers.length > 0) {
|
|
9300
9879
|
console.log(source_default.yellow("\nBlockers"));
|
|
9301
|
-
|
|
9880
|
+
run3.summary.blockers.forEach((blocker) => console.log(` - ${blocker}`));
|
|
9302
9881
|
}
|
|
9303
9882
|
console.log(source_default.bold("\nChecklist"));
|
|
9304
|
-
for (const item of
|
|
9883
|
+
for (const item of run3.checklist) {
|
|
9305
9884
|
const required = item.required ? source_default.yellow(" required") : "";
|
|
9306
9885
|
const notes = item.notes ? source_default.dim(` \u2014 ${item.notes}`) : "";
|
|
9307
9886
|
console.log(` ${statusBadge(item.status)} ${item.id}${required}`);
|
|
9308
9887
|
console.log(` ${item.title}${notes}`);
|
|
9309
9888
|
}
|
|
9310
|
-
if (
|
|
9889
|
+
if (run3.urls.length > 0) {
|
|
9311
9890
|
console.log(source_default.bold("\nQA URLs"));
|
|
9312
|
-
|
|
9891
|
+
run3.urls.forEach((url) => {
|
|
9313
9892
|
console.log(` - ${url.label}: ${source_default.cyan(url.url)}`);
|
|
9314
9893
|
});
|
|
9315
9894
|
}
|
|
@@ -9363,18 +9942,21 @@ async function resolveExperimentForFilesystemOrId(value) {
|
|
|
9363
9942
|
}
|
|
9364
9943
|
async function refreshExperimentFilesystemFolderIfNeeded(creds2, folder, experimentId) {
|
|
9365
9944
|
if (!folder) return;
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9945
|
+
try {
|
|
9946
|
+
const refreshed = await getExperimentFilesystem(creds2, experimentId);
|
|
9947
|
+
if (refreshed.ok && refreshed.data) {
|
|
9948
|
+
writeExperimentFilesystemFiles(folder, refreshed.data.files);
|
|
9949
|
+
writeFilesystemSession(folder, {
|
|
9950
|
+
schemaVersion: 1,
|
|
9951
|
+
baseUrl: creds2.baseUrl,
|
|
9952
|
+
experimentId,
|
|
9953
|
+
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9954
|
+
files: Object.fromEntries(
|
|
9955
|
+
refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
|
|
9956
|
+
)
|
|
9957
|
+
});
|
|
9958
|
+
}
|
|
9959
|
+
} catch {
|
|
9378
9960
|
}
|
|
9379
9961
|
}
|
|
9380
9962
|
async function resolveExperiment(idOrSlug) {
|
|
@@ -9382,20 +9964,29 @@ async function resolveExperiment(idOrSlug) {
|
|
|
9382
9964
|
if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
|
|
9383
9965
|
const res2 = await getExperiment(creds2, idOrSlug);
|
|
9384
9966
|
if (res2.ok) return res2.data;
|
|
9385
|
-
|
|
9967
|
+
throw new CliError(
|
|
9968
|
+
res2.error ?? `Experiment "${idOrSlug}" not found`,
|
|
9969
|
+
classifyStatus(res2.status),
|
|
9970
|
+
res2.errorDetails
|
|
9971
|
+
);
|
|
9386
9972
|
}
|
|
9387
9973
|
const slug = idOrSlug.startsWith("slug:") ? idOrSlug.slice(5) : idOrSlug;
|
|
9388
9974
|
const res = await getExperimentBySlug(creds2, slug);
|
|
9389
9975
|
if (res.ok) return res.data;
|
|
9390
|
-
|
|
9976
|
+
throw new CliError(
|
|
9977
|
+
res.error ?? `Experiment "${idOrSlug}" not found`,
|
|
9978
|
+
classifyStatus(res.status),
|
|
9979
|
+
res.errorDetails
|
|
9980
|
+
);
|
|
9391
9981
|
}
|
|
9392
|
-
var list = new Command("list").alias("ls").description("List experiments").option("-s, --status <status>", "Filter by status (draft|running|paused|completed)").option("-n, --limit <n>", "Max results", "20").action(async function action4(opts) {
|
|
9982
|
+
var list = new Command("list").alias("ls").description("List experiments (archived experiments are hidden by default)").option("-s, --status <status>", "Filter by status (draft|running|paused|completed)").option("-n, --limit <n>", "Max results", "20").option("--include-archived", "Include archived experiments").action(async function action4(opts) {
|
|
9393
9983
|
const creds2 = requireCredentials();
|
|
9394
9984
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9395
9985
|
const spinner = process.stderr.isTTY ? ora2("Fetching experiments...").start() : null;
|
|
9396
9986
|
const res = await listExperiments(creds2, {
|
|
9397
9987
|
status: opts.status,
|
|
9398
|
-
limit: parseInt(opts.limit, 10)
|
|
9988
|
+
limit: parseInt(opts.limit, 10),
|
|
9989
|
+
includeArchived: opts.includeArchived
|
|
9399
9990
|
});
|
|
9400
9991
|
if (!res.ok) {
|
|
9401
9992
|
spinner?.fail("Failed to fetch experiments");
|
|
@@ -9409,15 +10000,16 @@ var list = new Command("list").alias("ls").description("List experiments").optio
|
|
|
9409
10000
|
return;
|
|
9410
10001
|
}
|
|
9411
10002
|
printTable(
|
|
9412
|
-
["NAME", "SLUG", "STATUS", "TRAFFIC", "CREATED"],
|
|
10003
|
+
["NAME", "SLUG", "STATUS", "ARCHIVED", "TRAFFIC", "CREATED"],
|
|
9413
10004
|
experiments.map((e) => [
|
|
9414
10005
|
e.name.slice(0, 40),
|
|
9415
10006
|
e.slug ?? source_default.dim("-"),
|
|
9416
10007
|
statusBadge(e.status),
|
|
10008
|
+
e.archived_at ? formatDate(e.archived_at) : source_default.dim("-"),
|
|
9417
10009
|
`${(e.traffic_allocation * 100).toFixed(0)}%`,
|
|
9418
10010
|
formatDate(e.created_at)
|
|
9419
10011
|
]),
|
|
9420
|
-
[40, 25, 12, 8, 15]
|
|
10012
|
+
[40, 25, 12, 15, 8, 15]
|
|
9421
10013
|
);
|
|
9422
10014
|
if (res.data.pagination.has_more) {
|
|
9423
10015
|
console.log(source_default.dim(`
|
|
@@ -9587,66 +10179,66 @@ var create = new Command("create").description("Create a new experiment").requir
|
|
|
9587
10179
|
spinner?.succeed("Experiment created");
|
|
9588
10180
|
output(this, { experiment: res.data }, () => printExperiment(res.data));
|
|
9589
10181
|
});
|
|
9590
|
-
var start = new Command("start").description("Start an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async (idOrSlug)
|
|
10182
|
+
var start = new Command("start").description("Start an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
|
|
9591
10183
|
const creds2 = requireCredentials();
|
|
9592
10184
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9593
10185
|
const spinner = ora2("Starting experiment...").start();
|
|
9594
10186
|
const exp = await resolveExperiment(idOrSlug);
|
|
9595
10187
|
if (!exp) {
|
|
9596
10188
|
spinner.fail(`Experiment "${idOrSlug}" not found`);
|
|
9597
|
-
|
|
10189
|
+
fail(this, `Experiment "${idOrSlug}" not found`);
|
|
9598
10190
|
}
|
|
9599
10191
|
const res = await startExperiment(creds2, exp.id);
|
|
9600
10192
|
if (!res.ok) {
|
|
9601
10193
|
spinner.fail("Failed to start experiment");
|
|
9602
|
-
|
|
9603
|
-
process.exit(1);
|
|
10194
|
+
fail(this, res.error ?? "Failed to start experiment", classifyStatus(res.status), res.errorDetails);
|
|
9604
10195
|
}
|
|
9605
|
-
|
|
10196
|
+
output(this, { experiment: { id: exp.id, name: exp.name, status: "running" } }, () => {
|
|
10197
|
+
spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("running")}`);
|
|
10198
|
+
});
|
|
9606
10199
|
});
|
|
9607
|
-
var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async (idOrSlug)
|
|
10200
|
+
var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action9(idOrSlug) {
|
|
9608
10201
|
const creds2 = requireCredentials();
|
|
9609
10202
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9610
10203
|
const spinner = ora2("Pausing experiment...").start();
|
|
9611
10204
|
const exp = await resolveExperiment(idOrSlug);
|
|
9612
10205
|
if (!exp) {
|
|
9613
10206
|
spinner.fail(`Experiment "${idOrSlug}" not found`);
|
|
9614
|
-
|
|
10207
|
+
fail(this, `Experiment "${idOrSlug}" not found`);
|
|
9615
10208
|
}
|
|
9616
10209
|
const res = await pauseExperiment(creds2, exp.id);
|
|
9617
10210
|
if (!res.ok) {
|
|
9618
10211
|
spinner.fail("Failed to pause experiment");
|
|
9619
|
-
|
|
9620
|
-
process.exit(1);
|
|
10212
|
+
fail(this, res.error ?? "Failed to pause experiment", classifyStatus(res.status), res.errorDetails);
|
|
9621
10213
|
}
|
|
9622
|
-
|
|
10214
|
+
output(this, { experiment: { id: exp.id, name: exp.name, status: "paused" } }, () => {
|
|
10215
|
+
spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
|
|
10216
|
+
});
|
|
9623
10217
|
});
|
|
9624
|
-
var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async (idOrSlug)
|
|
10218
|
+
var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action10(idOrSlug) {
|
|
9625
10219
|
const creds2 = requireCredentials();
|
|
9626
10220
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9627
10221
|
const spinner = ora2("Completing experiment...").start();
|
|
9628
10222
|
const exp = await resolveExperiment(idOrSlug);
|
|
9629
10223
|
if (!exp) {
|
|
9630
10224
|
spinner.fail(`Experiment "${idOrSlug}" not found`);
|
|
9631
|
-
|
|
10225
|
+
fail(this, `Experiment "${idOrSlug}" not found`);
|
|
9632
10226
|
}
|
|
9633
10227
|
const res = await completeExperiment(creds2, exp.id);
|
|
9634
10228
|
if (!res.ok) {
|
|
9635
10229
|
spinner.fail("Failed to complete experiment");
|
|
9636
|
-
|
|
9637
|
-
process.exit(1);
|
|
10230
|
+
fail(this, res.error ?? "Failed to complete experiment", classifyStatus(res.status), res.errorDetails);
|
|
9638
10231
|
}
|
|
9639
|
-
|
|
9640
|
-
`Experiment "${exp.name}" is now ${statusBadge("completed")}`
|
|
9641
|
-
);
|
|
10232
|
+
output(this, { experiment: { id: exp.id, name: exp.name, status: "completed" } }, () => {
|
|
10233
|
+
spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
|
|
10234
|
+
});
|
|
9642
10235
|
});
|
|
9643
|
-
var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", "Experiment ID or slug").option("-f, --force", "Skip confirmation").action(async function
|
|
10236
|
+
var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", "Experiment ID or slug").option("-f, --force", "Skip confirmation").action(async function action11(idOrSlug, opts) {
|
|
9644
10237
|
const creds2 = requireCredentials();
|
|
9645
10238
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9646
10239
|
const exp = await resolveExperiment(idOrSlug);
|
|
9647
10240
|
if (!exp) {
|
|
9648
|
-
|
|
9649
|
-
process.exit(1);
|
|
10241
|
+
fail(this, `Experiment "${idOrSlug}" not found.`);
|
|
9650
10242
|
}
|
|
9651
10243
|
if (!opts.force) {
|
|
9652
10244
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -9679,21 +10271,20 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
|
|
|
9679
10271
|
const res = await deleteExperiment(creds2, exp.id);
|
|
9680
10272
|
if (!res.ok) {
|
|
9681
10273
|
spinner.fail("Failed to delete experiment");
|
|
9682
|
-
|
|
9683
|
-
process.exit(1);
|
|
10274
|
+
fail(this, res.error ?? "Failed to delete experiment", classifyStatus(res.status), res.errorDetails);
|
|
9684
10275
|
}
|
|
9685
|
-
spinner.succeed(`Experiment "${exp.name}" deleted.`);
|
|
10276
|
+
output(this, { experiment: { id: exp.id, name: exp.name }, deleted: true }, () => spinner.succeed(`Experiment "${exp.name}" deleted.`));
|
|
9686
10277
|
});
|
|
9687
|
-
var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--
|
|
10278
|
+
var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action12(idOrSlug, opts) {
|
|
9688
10279
|
const creds2 = requireCredentials();
|
|
9689
10280
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9690
10281
|
const spinner = ora2("Pulling experiment filesystem...").start();
|
|
9691
10282
|
try {
|
|
9692
10283
|
const exp = await resolveExperiment(idOrSlug);
|
|
9693
|
-
if (!exp) throw new
|
|
10284
|
+
if (!exp) throw new CliError(`Experiment "${idOrSlug}" not found.`);
|
|
9694
10285
|
const res = await getExperimentFilesystem(creds2, exp.id);
|
|
9695
|
-
if (!res.ok || !res.data) throw new
|
|
9696
|
-
const folder = folderPath(opts.dir ?? res.data.rootName);
|
|
10286
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to pull experiment filesystem", classifyStatus(res.status), res.errorDetails);
|
|
10287
|
+
const folder = folderPath(opts.out ?? opts.dir ?? res.data.rootName);
|
|
9697
10288
|
writeExperimentFilesystemFiles(folder, res.data.files);
|
|
9698
10289
|
writeFilesystemSession(folder, {
|
|
9699
10290
|
schemaVersion: 1,
|
|
@@ -9704,15 +10295,16 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
|
|
|
9704
10295
|
res.data.files.map((file) => [file.path, { sha256: file.sha256 }])
|
|
9705
10296
|
)
|
|
9706
10297
|
});
|
|
9707
|
-
|
|
9708
|
-
|
|
10298
|
+
output(this, { folder, experimentId: exp.id, files: res.data.files.map((file) => import_node_path4.default.join(folder, file.path)) }, () => {
|
|
10299
|
+
spinner.succeed(`Pulled ${exp.name}`);
|
|
10300
|
+
console.log(source_default.cyan(folder));
|
|
10301
|
+
});
|
|
9709
10302
|
} catch (error) {
|
|
9710
10303
|
spinner.fail("Failed to pull experiment filesystem");
|
|
9711
|
-
|
|
9712
|
-
process.exit(1);
|
|
10304
|
+
throw error;
|
|
9713
10305
|
}
|
|
9714
10306
|
});
|
|
9715
|
-
var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function
|
|
10307
|
+
var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action13(folderArg) {
|
|
9716
10308
|
const creds2 = requireCredentials();
|
|
9717
10309
|
try {
|
|
9718
10310
|
const folder = folderPath(folderArg);
|
|
@@ -9723,7 +10315,9 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9723
10315
|
fail(this, res.error ?? "Failed to plan experiment filesystem", classifyStatus(res.status), res.errorDetails);
|
|
9724
10316
|
}
|
|
9725
10317
|
if (!res.data.ok) {
|
|
9726
|
-
|
|
10318
|
+
if (!shouldUseJson(this)) printFilesystemPlan(res.data);
|
|
10319
|
+
const reasons = res.data.blockingIssues?.map((issue) => issue.message) ?? [];
|
|
10320
|
+
fail(this, ["Experiment filesystem plan has errors.", ...reasons].join(" "), EXIT_CODES.validation, {
|
|
9727
10321
|
errors: res.data.errors,
|
|
9728
10322
|
plan: res.data
|
|
9729
10323
|
});
|
|
@@ -9734,7 +10328,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
9734
10328
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
9735
10329
|
}
|
|
9736
10330
|
});
|
|
9737
|
-
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
|
|
10331
|
+
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 action14(folderArg, opts) {
|
|
9738
10332
|
const creds2 = requireCredentials();
|
|
9739
10333
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9740
10334
|
try {
|
|
@@ -9746,7 +10340,9 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
9746
10340
|
fail(this, planRes.error ?? "Failed to plan experiment filesystem", classifyStatus(planRes.status), planRes.errorDetails);
|
|
9747
10341
|
}
|
|
9748
10342
|
if (!planRes.data.ok) {
|
|
9749
|
-
|
|
10343
|
+
if (!shouldUseJson(this)) printFilesystemPlan(planRes.data);
|
|
10344
|
+
const reasons = planRes.data.blockingIssues?.map((issue) => issue.message) ?? [];
|
|
10345
|
+
fail(this, ["Experiment filesystem plan has errors.", ...reasons].join(" "), EXIT_CODES.validation, {
|
|
9750
10346
|
errors: planRes.data.errors,
|
|
9751
10347
|
plan: planRes.data
|
|
9752
10348
|
});
|
|
@@ -9785,19 +10381,11 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
9785
10381
|
});
|
|
9786
10382
|
}
|
|
9787
10383
|
spinner?.succeed("Applied experiment filesystem");
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
baseUrl: creds2.baseUrl,
|
|
9794
|
-
experimentId: session.experimentId,
|
|
9795
|
-
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9796
|
-
files: Object.fromEntries(
|
|
9797
|
-
refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
|
|
9798
|
-
)
|
|
9799
|
-
});
|
|
9800
|
-
}
|
|
10384
|
+
await refreshExperimentFilesystemFolderIfNeeded(
|
|
10385
|
+
creds2,
|
|
10386
|
+
folder,
|
|
10387
|
+
session.experimentId
|
|
10388
|
+
);
|
|
9801
10389
|
output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data }, () => {
|
|
9802
10390
|
printFilesystemPlan(planRes.data);
|
|
9803
10391
|
});
|
|
@@ -9806,29 +10394,28 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
|
|
|
9806
10394
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
9807
10395
|
}
|
|
9808
10396
|
});
|
|
9809
|
-
var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--json", "Print
|
|
10397
|
+
var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--json", "Print the QA run in the standard JSON envelope").action(async function action15(target) {
|
|
9810
10398
|
const creds2 = requireCredentials();
|
|
9811
10399
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9812
10400
|
const spinner = ora2("Fetching QA run...").start();
|
|
9813
10401
|
try {
|
|
9814
10402
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
9815
10403
|
const res = await getExperimentQaRun(creds2, resolved.experimentId);
|
|
9816
|
-
if (!res.ok || !res.data) throw new
|
|
10404
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to fetch QA run", classifyStatus(res.status), res.errorDetails);
|
|
9817
10405
|
spinner.stop();
|
|
9818
|
-
printQaRun(res.data.run, opts.json === true);
|
|
9819
10406
|
await refreshExperimentFilesystemFolderIfNeeded(
|
|
9820
10407
|
creds2,
|
|
9821
10408
|
resolved.folder,
|
|
9822
10409
|
resolved.experimentId
|
|
9823
10410
|
);
|
|
10411
|
+
output(this, { run: res.data.run }, () => printQaRun(res.data.run));
|
|
9824
10412
|
} catch (error) {
|
|
9825
10413
|
spinner.fail("Failed to fetch QA run");
|
|
9826
|
-
|
|
9827
|
-
process.exit(1);
|
|
10414
|
+
throw error;
|
|
9828
10415
|
}
|
|
9829
10416
|
});
|
|
9830
10417
|
var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").requiredOption("--item <id>", "Checklist item id").requiredOption("--status <status>", "pending|pass|warning|fail|accepted|skipped").option("--notes <text>", "Checklist notes").option("--evidence <json-or-path>", "Evidence JSON object or @path/to/evidence.json").option("--json", "Print raw QA run JSON").action(
|
|
9831
|
-
async (target, opts)
|
|
10418
|
+
async function action16(target, opts) {
|
|
9832
10419
|
const creds2 = requireCredentials();
|
|
9833
10420
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9834
10421
|
const spinner = ora2("Updating QA checklist...").start();
|
|
@@ -9849,23 +10436,25 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
|
|
|
9849
10436
|
notes: opts.notes,
|
|
9850
10437
|
evidence
|
|
9851
10438
|
});
|
|
9852
|
-
if (!res.ok || !res.data) throw new
|
|
9853
|
-
|
|
9854
|
-
printQaRun(res.data.run, opts.json === true);
|
|
10439
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to update QA run", classifyStatus(res.status), res.errorDetails);
|
|
10440
|
+
const run3 = res.data.run;
|
|
9855
10441
|
await refreshExperimentFilesystemFolderIfNeeded(
|
|
9856
10442
|
creds2,
|
|
9857
10443
|
resolved.folder,
|
|
9858
10444
|
resolved.experimentId
|
|
9859
10445
|
);
|
|
10446
|
+
output(this, { run: run3 }, () => {
|
|
10447
|
+
spinner.succeed("Updated QA checklist");
|
|
10448
|
+
printQaRun(run3);
|
|
10449
|
+
});
|
|
9860
10450
|
} catch (error) {
|
|
9861
10451
|
spinner.fail("Failed to update QA checklist");
|
|
9862
|
-
|
|
9863
|
-
process.exit(1);
|
|
10452
|
+
throw error;
|
|
9864
10453
|
}
|
|
9865
10454
|
}
|
|
9866
10455
|
);
|
|
9867
10456
|
var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--notes <text>", "QA run notes").option("--acceptance <text>", "Acceptance criteria summary").option("--json", "Print raw QA run JSON").action(
|
|
9868
|
-
async (target, opts)
|
|
10457
|
+
async function action17(target, opts) {
|
|
9869
10458
|
const creds2 = requireCredentials();
|
|
9870
10459
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9871
10460
|
const spinner = ora2("Updating QA notes...").start();
|
|
@@ -9883,64 +10472,175 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
|
|
|
9883
10472
|
acceptanceCriteriaSummary: opts.acceptance ?? null
|
|
9884
10473
|
};
|
|
9885
10474
|
const res = await updateExperimentQaRun(creds2, resolved.experimentId, body5);
|
|
9886
|
-
if (!res.ok || !res.data) throw new
|
|
9887
|
-
|
|
9888
|
-
printQaRun(res.data.run, opts.json === true);
|
|
10475
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to update QA notes", classifyStatus(res.status), res.errorDetails);
|
|
10476
|
+
const run3 = res.data.run;
|
|
9889
10477
|
await refreshExperimentFilesystemFolderIfNeeded(
|
|
9890
10478
|
creds2,
|
|
9891
10479
|
resolved.folder,
|
|
9892
10480
|
resolved.experimentId
|
|
9893
10481
|
);
|
|
10482
|
+
output(this, { run: run3 }, () => {
|
|
10483
|
+
spinner.succeed("Updated QA notes");
|
|
10484
|
+
printQaRun(run3);
|
|
10485
|
+
});
|
|
9894
10486
|
} catch (error) {
|
|
9895
10487
|
spinner.fail("Failed to update QA notes");
|
|
9896
|
-
|
|
9897
|
-
process.exit(1);
|
|
10488
|
+
throw error;
|
|
9898
10489
|
}
|
|
9899
10490
|
}
|
|
9900
10491
|
);
|
|
9901
|
-
var qaApprove = new Command("approve").description("Mark QA done after the checklist gates are ready").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async (target)
|
|
10492
|
+
var qaApprove = new Command("approve").description("Mark QA done after the checklist gates are ready").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action18(target) {
|
|
9902
10493
|
const creds2 = requireCredentials();
|
|
9903
10494
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9904
10495
|
const spinner = ora2("Approving QA...").start();
|
|
9905
10496
|
try {
|
|
9906
10497
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
9907
10498
|
const res = await approveExperimentQa(creds2, resolved.experimentId);
|
|
9908
|
-
if (!res.ok) throw new
|
|
9909
|
-
spinner.succeed(`QA marked done for ${resolved.label}`);
|
|
10499
|
+
if (!res.ok) throw new CliError(res.error ?? "Failed to approve QA", classifyStatus(res.status), res.errorDetails);
|
|
9910
10500
|
await refreshExperimentFilesystemFolderIfNeeded(
|
|
9911
10501
|
creds2,
|
|
9912
10502
|
resolved.folder,
|
|
9913
10503
|
resolved.experimentId
|
|
9914
10504
|
);
|
|
10505
|
+
output(this, { experimentId: resolved.experimentId, qaStatus: "done" }, () => spinner.succeed(`QA marked done for ${resolved.label}`));
|
|
9915
10506
|
} catch (error) {
|
|
9916
10507
|
spinner.fail("Failed to approve QA");
|
|
9917
|
-
|
|
9918
|
-
process.exit(1);
|
|
10508
|
+
throw error;
|
|
9919
10509
|
}
|
|
9920
10510
|
});
|
|
9921
|
-
var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async (target)
|
|
10511
|
+
var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action19(target) {
|
|
9922
10512
|
const creds2 = requireCredentials();
|
|
9923
10513
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
9924
10514
|
const spinner = ora2("Reopening QA...").start();
|
|
9925
10515
|
try {
|
|
9926
10516
|
const resolved = await resolveExperimentForFilesystemOrId(target);
|
|
9927
10517
|
const res = await reopenExperimentQa(creds2, resolved.experimentId);
|
|
9928
|
-
if (!res.ok) throw new
|
|
9929
|
-
spinner.succeed(`QA reopened for ${resolved.label}`);
|
|
10518
|
+
if (!res.ok) throw new CliError(res.error ?? "Failed to reopen QA", classifyStatus(res.status), res.errorDetails);
|
|
9930
10519
|
await refreshExperimentFilesystemFolderIfNeeded(
|
|
9931
10520
|
creds2,
|
|
9932
10521
|
resolved.folder,
|
|
9933
10522
|
resolved.experimentId
|
|
9934
10523
|
);
|
|
10524
|
+
output(this, { experimentId: resolved.experimentId, qaStatus: "open" }, () => spinner.succeed(`QA reopened for ${resolved.label}`));
|
|
9935
10525
|
} catch (error) {
|
|
9936
10526
|
spinner.fail("Failed to reopen QA");
|
|
9937
|
-
|
|
9938
|
-
process.exit(1);
|
|
10527
|
+
throw error;
|
|
9939
10528
|
}
|
|
9940
10529
|
});
|
|
9941
10530
|
var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen);
|
|
9942
|
-
var
|
|
9943
|
-
|
|
10531
|
+
var SCREENSHOT_DEVICES = ["desktop_chrome", "iphone_15_pro", "galaxy_s24", "ipad_pro", "desktop_safari"];
|
|
10532
|
+
var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
|
|
10533
|
+
var commaList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
10534
|
+
var screenshots = new Command("screenshots").description("Force-assign every capture, including control, to its variation server-side for deterministic screenshots").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("--variations <list>", "Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned").option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--timeout <seconds>", "Maximum wait for completion; finished results are always downloaded", "300").action(async function action20(experimentRef, opts) {
|
|
10535
|
+
if (opts.url) fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
|
|
10536
|
+
const timeoutSeconds = Number(opts.timeout);
|
|
10537
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
|
|
10538
|
+
const experiment = await resolveExperiment(experimentRef);
|
|
10539
|
+
if (!experiment?.variations?.length) fail(this, "Experiment has no variations to capture.", EXIT_CODES.validation);
|
|
10540
|
+
const devices = opts.devices ? commaList(opts.devices) : SCREENSHOT_DEVICES;
|
|
10541
|
+
if (!devices.length) fail(this, "--devices must contain at least one device.", EXIT_CODES.validation);
|
|
10542
|
+
const unknownDevices = devices.filter((device) => !SCREENSHOT_DEVICES.includes(device));
|
|
10543
|
+
if (unknownDevices.length) fail(this, `Unknown screenshot device(s): ${unknownDevices.join(", ")}`, EXIT_CODES.validation);
|
|
10544
|
+
const requested = opts.variations ? commaList(opts.variations) : [];
|
|
10545
|
+
const control = experiment.variations.find((variation) => variation.is_control);
|
|
10546
|
+
if (!control) fail(this, "Experiment has no control variation.", EXIT_CODES.validation);
|
|
10547
|
+
const matchedById = /* @__PURE__ */ new Map();
|
|
10548
|
+
const unmatched = [];
|
|
10549
|
+
for (const ref of requested) {
|
|
10550
|
+
const variation = experiment.variations.find((candidate) => ref === candidate.id || ref === candidate.slug || ref.toLowerCase() === candidate.name.toLowerCase());
|
|
10551
|
+
if (variation) matchedById.set(variation.id, variation);
|
|
10552
|
+
else unmatched.push(ref);
|
|
10553
|
+
}
|
|
10554
|
+
if (unmatched.length) fail(this, `Unknown --variations value(s): ${unmatched.join(", ")}`, EXIT_CODES.validation);
|
|
10555
|
+
const selected = requested.length ? [...matchedById.values()] : experiment.variations;
|
|
10556
|
+
const variationIds = [.../* @__PURE__ */ new Set([control.id, ...selected.map((variation) => variation.id)])];
|
|
10557
|
+
const creds2 = requireCredentials();
|
|
10558
|
+
const created = await createExperimentScreenshots(creds2, experiment.id, { devices, variation_ids: variationIds });
|
|
10559
|
+
if (!created.ok || !created.data) fail(this, created.error ?? "Failed to create screenshot job", classifyStatus(created.status), created.errorDetails);
|
|
10560
|
+
let job = created.data;
|
|
10561
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
10562
|
+
let delayMs = process.env.APEX_CLI_MOCK_API === "1" ? 1 : 1e3;
|
|
10563
|
+
let consecutiveTransientFailures = 0;
|
|
10564
|
+
while (job.status === "queued" || job.status === "processing") {
|
|
10565
|
+
if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
10566
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - Date.now()))));
|
|
10567
|
+
if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
10568
|
+
let polled;
|
|
10569
|
+
try {
|
|
10570
|
+
polled = await getScreenshotJob(creds2, job.job_id, Math.max(1, deadline - Date.now()));
|
|
10571
|
+
} catch (error) {
|
|
10572
|
+
if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
10573
|
+
consecutiveTransientFailures += 1;
|
|
10574
|
+
if (consecutiveTransientFailures > 3) throw error;
|
|
10575
|
+
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
10576
|
+
continue;
|
|
10577
|
+
}
|
|
10578
|
+
const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
|
|
10579
|
+
if (!polledToTerminalState && Date.now() >= deadline) {
|
|
10580
|
+
fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
|
|
10581
|
+
}
|
|
10582
|
+
if (!polled.ok || !polled.data) {
|
|
10583
|
+
const transient = polled.status === 429 || polled.status >= 500;
|
|
10584
|
+
if (!transient || ++consecutiveTransientFailures > 3) {
|
|
10585
|
+
fail(this, polled.error ?? "Failed to poll screenshot job", classifyStatus(polled.status), polled.errorDetails);
|
|
10586
|
+
}
|
|
10587
|
+
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
10588
|
+
continue;
|
|
10589
|
+
}
|
|
10590
|
+
consecutiveTransientFailures = 0;
|
|
10591
|
+
job = polled.data;
|
|
10592
|
+
delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
|
|
10593
|
+
}
|
|
10594
|
+
if (job.status === "failed") fail(this, `Screenshot job failed: ${job.error ?? "unknown error"}`);
|
|
10595
|
+
const outputDir = import_node_path4.default.resolve(process.cwd(), opts.out);
|
|
10596
|
+
import_node_fs6.default.mkdirSync(outputDir, { recursive: true });
|
|
10597
|
+
const rows = [];
|
|
10598
|
+
const errors = [];
|
|
10599
|
+
const usedFilenames = /* @__PURE__ */ new Set();
|
|
10600
|
+
for (const variationId of variationIds) {
|
|
10601
|
+
const variation = experiment.variations.find((item) => item.id === variationId);
|
|
10602
|
+
for (const device of devices) {
|
|
10603
|
+
const url = job.results?.[variationId]?.[device];
|
|
10604
|
+
if (!url) {
|
|
10605
|
+
errors.push(`${variation.name}/${device}: no screenshot returned`);
|
|
10606
|
+
continue;
|
|
10607
|
+
}
|
|
10608
|
+
const baseFilename = `${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
|
|
10609
|
+
let filename = `${baseFilename}.png`;
|
|
10610
|
+
if (usedFilenames.has(filename)) {
|
|
10611
|
+
const suffix = variation.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
|
|
10612
|
+
filename = `${baseFilename}-${suffix}.png`;
|
|
10613
|
+
let collisionIndex = 2;
|
|
10614
|
+
while (usedFilenames.has(filename)) filename = `${baseFilename}-${suffix}-${collisionIndex++}.png`;
|
|
10615
|
+
}
|
|
10616
|
+
usedFilenames.add(filename);
|
|
10617
|
+
try {
|
|
10618
|
+
const response = await fetch(url);
|
|
10619
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
10620
|
+
import_node_fs6.default.writeFileSync(import_node_path4.default.join(outputDir, filename), Buffer.from(await response.arrayBuffer()));
|
|
10621
|
+
rows.push([variation.name, device, filename]);
|
|
10622
|
+
} catch (error) {
|
|
10623
|
+
errors.push(`${variation.name}/${device}: ${error instanceof Error ? error.message : String(error)}`);
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
}
|
|
10627
|
+
if (!rows.length) fail(this, `No screenshots succeeded. ${errors.join("; ")}`);
|
|
10628
|
+
if (errors.length) {
|
|
10629
|
+
console.error(source_default.yellow(`Partial result: ${rows.length} saved, ${errors.length} failed.`));
|
|
10630
|
+
errors.forEach((error) => console.error(` - ${error}`));
|
|
10631
|
+
}
|
|
10632
|
+
output(this, job, () => {
|
|
10633
|
+
printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
|
|
10634
|
+
});
|
|
10635
|
+
});
|
|
10636
|
+
var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(create).addCommand(start).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText("after", `
|
|
10637
|
+
Examples:
|
|
10638
|
+
apex experiments list --status running
|
|
10639
|
+
apex experiments get checkout-banner
|
|
10640
|
+
apex experiments pull checkout-banner --out ./checkout-banner
|
|
10641
|
+
apex experiments plan ./checkout-banner --json
|
|
10642
|
+
`);
|
|
10643
|
+
|
|
9944
10644
|
// src/commands/push.ts
|
|
9945
10645
|
init_source();
|
|
9946
10646
|
var import_node_fs7 = __toESM(require("node:fs"));
|
|
@@ -10014,7 +10714,7 @@ function isRecord(value) {
|
|
|
10014
10714
|
}
|
|
10015
10715
|
var pushCommand = new Command("push").description(
|
|
10016
10716
|
"Push experiment definitions from config file to the API (idempotent by slug)"
|
|
10017
|
-
).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function
|
|
10717
|
+
).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action21(opts) {
|
|
10018
10718
|
const creds2 = requireCredentials();
|
|
10019
10719
|
const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
|
|
10020
10720
|
const configPath = opts.config ?? findConfigPath();
|
|
@@ -10154,7 +10854,7 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10154
10854
|
}
|
|
10155
10855
|
const data = created.data;
|
|
10156
10856
|
output(previewCommand, data, () => console.log(source_default.cyan(data.preview_url)));
|
|
10157
|
-
if (opts.open !== false) {
|
|
10857
|
+
if (opts.open !== false && !shouldUseJson(previewCommand)) {
|
|
10158
10858
|
try {
|
|
10159
10859
|
const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
10160
10860
|
await open2(data.preview_url);
|
|
@@ -10170,7 +10870,7 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10170
10870
|
const res = await getExperiment(creds2, idOrSlug);
|
|
10171
10871
|
if (!res.ok) {
|
|
10172
10872
|
spinner.fail(`Experiment "${idOrSlug}" not found`);
|
|
10173
|
-
|
|
10873
|
+
fail(previewCommand, res.error ?? `Experiment "${idOrSlug}" not found`, classifyStatus(res.status), res.errorDetails);
|
|
10174
10874
|
}
|
|
10175
10875
|
exp = res.data;
|
|
10176
10876
|
} else {
|
|
@@ -10178,13 +10878,13 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10178
10878
|
const res = await getExperimentBySlug(creds2, slug);
|
|
10179
10879
|
if (!res.ok) {
|
|
10180
10880
|
spinner.fail(`Experiment "${slug}" not found`);
|
|
10181
|
-
|
|
10881
|
+
fail(previewCommand, res.error ?? `Experiment "${slug}" not found`, classifyStatus(res.status), res.errorDetails);
|
|
10182
10882
|
}
|
|
10183
10883
|
exp = res.data;
|
|
10184
10884
|
}
|
|
10185
10885
|
if (!exp.variations || exp.variations.length === 0) {
|
|
10186
10886
|
spinner.fail("Experiment has no variations");
|
|
10187
|
-
|
|
10887
|
+
fail(previewCommand, "Experiment has no variations");
|
|
10188
10888
|
}
|
|
10189
10889
|
let variation;
|
|
10190
10890
|
if (variationId) {
|
|
@@ -10193,12 +10893,9 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10193
10893
|
);
|
|
10194
10894
|
if (!variation) {
|
|
10195
10895
|
spinner.fail(`Variation "${variationId}" not found`);
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
)
|
|
10200
|
-
);
|
|
10201
|
-
process.exit(1);
|
|
10896
|
+
fail(previewCommand, `Variation "${variationId}" not found`, void 0, {
|
|
10897
|
+
availableVariations: exp.variations.map((v) => ({ id: v.id, name: v.name, slug: v.slug }))
|
|
10898
|
+
});
|
|
10202
10899
|
}
|
|
10203
10900
|
} else {
|
|
10204
10901
|
variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
|
|
@@ -10206,30 +10903,28 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
|
|
|
10206
10903
|
spinner.stop();
|
|
10207
10904
|
const storeUrl = exp.store_url ?? opts?.storeUrl;
|
|
10208
10905
|
if (!storeUrl) {
|
|
10209
|
-
|
|
10210
|
-
'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.'
|
|
10211
|
-
);
|
|
10212
|
-
process.exit(1);
|
|
10906
|
+
fail(previewCommand, 'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.');
|
|
10213
10907
|
}
|
|
10214
10908
|
let previewUrl;
|
|
10215
10909
|
try {
|
|
10216
10910
|
previewUrl = new URL(storeUrl);
|
|
10217
10911
|
} catch {
|
|
10218
|
-
|
|
10219
|
-
process.exit(1);
|
|
10912
|
+
fail(previewCommand, 'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".');
|
|
10220
10913
|
}
|
|
10221
10914
|
previewUrl.searchParams.set("drip_force", `${exp.id}:${variation.id}`);
|
|
10222
10915
|
previewUrl.searchParams.set("drip_experiment_id", exp.id);
|
|
10223
10916
|
previewUrl.searchParams.set("drip_variation_id", variation.id);
|
|
10224
10917
|
previewUrl.searchParams.set("drip_qa", "1");
|
|
10225
10918
|
previewUrl.searchParams.set("drip_debug", "1");
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10919
|
+
output(previewCommand, { experimentId: exp.id, variationId: variation.id, previewUrl: previewUrl.toString() }, () => {
|
|
10920
|
+
console.log();
|
|
10921
|
+
console.log(source_default.bold(`Preview: ${exp.name}`));
|
|
10922
|
+
console.log(`Variation: ${variation.name}${variation.is_control ? " (control)" : ""}`);
|
|
10923
|
+
console.log();
|
|
10924
|
+
console.log(source_default.cyan(previewUrl.toString()));
|
|
10925
|
+
console.log();
|
|
10926
|
+
});
|
|
10927
|
+
if (opts?.open !== false && !shouldUseJson(previewCommand)) {
|
|
10233
10928
|
try {
|
|
10234
10929
|
const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
10235
10930
|
await open2(previewUrl.toString());
|
|
@@ -10344,7 +11039,7 @@ async function uploadVersion(params) {
|
|
|
10344
11039
|
})
|
|
10345
11040
|
);
|
|
10346
11041
|
const res = await uploadHarnessVersion(creds2, params.session.sessionId, form);
|
|
10347
|
-
if (!res.ok) throw new
|
|
11042
|
+
if (!res.ok) throw new CliError(res.error ?? "Failed to upload harness version", classifyStatus(res.status), res.errorDetails);
|
|
10348
11043
|
return res.data;
|
|
10349
11044
|
}
|
|
10350
11045
|
async function createSession(folder) {
|
|
@@ -10362,40 +11057,43 @@ async function createSession(folder) {
|
|
|
10362
11057
|
resetJs: cleanupResetJs(experiment),
|
|
10363
11058
|
antiFlickerSelectors: apex.antiFlickerSelectors ?? []
|
|
10364
11059
|
});
|
|
10365
|
-
if (!res.ok) throw new
|
|
11060
|
+
if (!res.ok) throw new CliError(res.error ?? "Failed to create harness session", classifyStatus(res.status), res.errorDetails);
|
|
10366
11061
|
return { session: res.data, apex, experiment };
|
|
10367
11062
|
}
|
|
10368
11063
|
var harnessCommand = new Command("harness").description("Develop AB Test Harness projects through Apex live QA sessions");
|
|
10369
|
-
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action((folderArg, opts)
|
|
11064
|
+
harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action22(folderArg, opts) {
|
|
10370
11065
|
const folder = resolveFolder(folderArg);
|
|
10371
11066
|
const targetPath = apexConfigPath(folder);
|
|
10372
11067
|
if (import_node_fs8.default.existsSync(targetPath) && !opts.force) {
|
|
10373
|
-
|
|
10374
|
-
process.exit(1);
|
|
11068
|
+
fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
|
|
10375
11069
|
}
|
|
10376
11070
|
const { apex } = loadHarnessConfig(folder);
|
|
10377
11071
|
import_node_fs8.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
|
|
10378
11072
|
`, "utf8");
|
|
10379
|
-
printSuccess(`Created ${targetPath}`);
|
|
11073
|
+
output(this, { folder, configPath: targetPath, experiment: apex.experimentSlug, variation: apex.variationSlug }, () => printSuccess(`Created ${targetPath}`));
|
|
10380
11074
|
});
|
|
10381
|
-
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async (folderArg, opts)
|
|
11075
|
+
harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action23(folderArg, opts) {
|
|
10382
11076
|
const folder = resolveFolder(folderArg);
|
|
10383
11077
|
const created = await createSession(folder);
|
|
10384
11078
|
await runAbHarness(["build", folder], folder);
|
|
10385
11079
|
const version = await uploadVersion({ ...created, folder });
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
11080
|
+
output(this, { folder, sessionId: created.session.sessionId, experimentId: created.session.experimentId, variationId: created.session.variationId, version: version.version, previewUrl: created.session.previewUrl }, () => {
|
|
11081
|
+
console.log(source_default.bold("\nApex Harness Session"));
|
|
11082
|
+
console.log(`Experiment: ${created.session.experimentId}`);
|
|
11083
|
+
console.log(`Variation: ${created.session.variationId}`);
|
|
11084
|
+
console.log(`Version: ${version.version}`);
|
|
11085
|
+
console.log(source_default.cyan(created.session.previewUrl));
|
|
11086
|
+
console.log();
|
|
11087
|
+
});
|
|
10392
11088
|
if (opts.open !== false) {
|
|
10393
11089
|
const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
10394
11090
|
await open2(created.session.previewUrl);
|
|
10395
11091
|
}
|
|
10396
11092
|
let uploading = false;
|
|
10397
11093
|
let queued = false;
|
|
11094
|
+
let closing = false;
|
|
10398
11095
|
const rebuild = async () => {
|
|
11096
|
+
if (closing) return;
|
|
10399
11097
|
if (uploading) {
|
|
10400
11098
|
queued = true;
|
|
10401
11099
|
return;
|
|
@@ -10404,12 +11102,12 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
|
|
|
10404
11102
|
try {
|
|
10405
11103
|
await runAbHarness(["build", folder], folder);
|
|
10406
11104
|
const next = await uploadVersion({ ...created, folder });
|
|
10407
|
-
console.
|
|
11105
|
+
console.error(source_default.dim(`[harness] uploaded version ${next.version}`));
|
|
10408
11106
|
} catch (error) {
|
|
10409
11107
|
printWarning(error instanceof Error ? error.message : String(error));
|
|
10410
11108
|
} finally {
|
|
10411
11109
|
uploading = false;
|
|
10412
|
-
if (queued) {
|
|
11110
|
+
if (queued && !closing) {
|
|
10413
11111
|
queued = false;
|
|
10414
11112
|
void rebuild();
|
|
10415
11113
|
}
|
|
@@ -10423,19 +11121,22 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
|
|
|
10423
11121
|
debounce = setTimeout(() => void rebuild(), 300);
|
|
10424
11122
|
});
|
|
10425
11123
|
process.on("SIGINT", () => {
|
|
11124
|
+
closing = true;
|
|
11125
|
+
queued = false;
|
|
11126
|
+
if (debounce) clearTimeout(debounce);
|
|
10426
11127
|
watcher.close();
|
|
10427
|
-
process.
|
|
11128
|
+
process.exitCode = 0;
|
|
10428
11129
|
});
|
|
10429
11130
|
});
|
|
10430
|
-
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async (folderArg)
|
|
11131
|
+
harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action24(folderArg) {
|
|
10431
11132
|
const folder = resolveFolder(folderArg);
|
|
10432
11133
|
const created = await createSession(folder);
|
|
10433
11134
|
await runAbHarness(["verify", folder], folder);
|
|
10434
11135
|
await runAbHarness(["build", folder], folder);
|
|
10435
11136
|
await uploadVersion({ ...created, folder });
|
|
10436
11137
|
const res = await publishHarnessSession(requireCredentials(), created.session.sessionId);
|
|
10437
|
-
if (!res.ok) throw new
|
|
10438
|
-
printSuccess("Published latest harness artifact to the Apex draft variation.");
|
|
11138
|
+
if (!res.ok) throw new CliError(res.error ?? "Failed to publish harness session", classifyStatus(res.status), res.errorDetails);
|
|
11139
|
+
output(this, { folder, sessionId: created.session.sessionId, experimentId: created.session.experimentId, variationId: created.session.variationId, published: true }, () => printSuccess("Published latest harness artifact to the Apex draft variation."));
|
|
10439
11140
|
});
|
|
10440
11141
|
|
|
10441
11142
|
// src/commands/operator.ts
|
|
@@ -10444,6 +11145,47 @@ var import_node_crypto4 = __toESM(require("node:crypto"));
|
|
|
10444
11145
|
var import_node_fs9 = __toESM(require("node:fs"));
|
|
10445
11146
|
var import_node_path7 = __toESM(require("node:path"));
|
|
10446
11147
|
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)$/;
|
|
11148
|
+
function workspaceIssueFile(field, evidence) {
|
|
11149
|
+
if (field === "custom_js" || field === "reset_js") return "src/index.js";
|
|
11150
|
+
if (field === "custom_css") return "src/index.css";
|
|
11151
|
+
if (field === "mutations" || field.startsWith("mutations[")) {
|
|
11152
|
+
return "src/mutations.json";
|
|
11153
|
+
}
|
|
11154
|
+
if (typeof evidence.selector === "string" && evidence.selector.trim()) {
|
|
11155
|
+
return "src/mutations.json";
|
|
11156
|
+
}
|
|
11157
|
+
if (field === "anti_flicker_selectors") return "experiment.config.json";
|
|
11158
|
+
return null;
|
|
11159
|
+
}
|
|
11160
|
+
function formatWorkspaceBlockingIssues(value) {
|
|
11161
|
+
if (!value || typeof value !== "object") return null;
|
|
11162
|
+
const issues = value.blockingIssues;
|
|
11163
|
+
if (!Array.isArray(issues) || issues.length === 0) return null;
|
|
11164
|
+
return issues.map((rawIssue, index) => {
|
|
11165
|
+
const issue = rawIssue && typeof rawIssue === "object" ? rawIssue : {};
|
|
11166
|
+
const evidence = issue.evidence && typeof issue.evidence === "object" ? issue.evidence : {};
|
|
11167
|
+
const field = typeof issue.field === "string" ? issue.field : "";
|
|
11168
|
+
const file = workspaceIssueFile(field, evidence);
|
|
11169
|
+
const selector = typeof evidence.selector === "string" && evidence.selector.trim() ? evidence.selector.trim() : null;
|
|
11170
|
+
const lines = [
|
|
11171
|
+
`[${index + 1}] ${typeof issue.message === "string" ? issue.message : "Workspace build blocked."}`,
|
|
11172
|
+
file ? ` File: ${file}${field && !selector ? ` (${field})` : ""}` : ` Field: ${field || "workspace"}`
|
|
11173
|
+
];
|
|
11174
|
+
if (selector) lines.push(` Selector: ${selector}`);
|
|
11175
|
+
const construct = typeof evidence.construct === "string" ? evidence.construct : null;
|
|
11176
|
+
const snippet2 = typeof evidence.snippet === "string" ? evidence.snippet : typeof evidence.url === "string" ? evidence.url : null;
|
|
11177
|
+
if (construct) lines.push(` Match: ${construct}`);
|
|
11178
|
+
if (snippet2) lines.push(` Evidence: ${snippet2}`);
|
|
11179
|
+
if (typeof evidence.line === "number") {
|
|
11180
|
+
lines.push(
|
|
11181
|
+
` Location: line ${evidence.line}, column ${typeof evidence.column === "number" ? evidence.column : 0}`
|
|
11182
|
+
);
|
|
11183
|
+
} else if (typeof evidence.matchIndex === "number") {
|
|
11184
|
+
lines.push(` Location: mutation character ${evidence.matchIndex}`);
|
|
11185
|
+
}
|
|
11186
|
+
return lines.join("\n");
|
|
11187
|
+
}).join("\n");
|
|
11188
|
+
}
|
|
10447
11189
|
function sha256Text2(value) {
|
|
10448
11190
|
return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
|
|
10449
11191
|
}
|
|
@@ -10475,9 +11217,11 @@ function writeJsonFile(filePath, value) {
|
|
|
10475
11217
|
function readSession(folder) {
|
|
10476
11218
|
const session = readJsonFile(sessionPath(folder));
|
|
10477
11219
|
if (!session) {
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
11220
|
+
const filesystemMarker = import_node_path7.default.join(apexDir(folder), "experiment-filesystem.json");
|
|
11221
|
+
if (import_node_fs9.default.existsSync(filesystemMarker)) {
|
|
11222
|
+
throw new Error("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.");
|
|
11223
|
+
}
|
|
11224
|
+
throw new Error(`Missing ${sessionPath(folder)}. Run apex workspace checkout first.`);
|
|
10481
11225
|
}
|
|
10482
11226
|
return session;
|
|
10483
11227
|
}
|
|
@@ -10611,7 +11355,7 @@ async function createExactHarnessSession(folder, session) {
|
|
|
10611
11355
|
targetUrl,
|
|
10612
11356
|
antiFlickerSelectors: []
|
|
10613
11357
|
});
|
|
10614
|
-
if (!res.ok || !res.data) throw new
|
|
11358
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to create harness session", classifyStatus(res.status), res.errorDetails);
|
|
10615
11359
|
return res.data;
|
|
10616
11360
|
}
|
|
10617
11361
|
async function uploadCurrentArtifactVersion(folder, sessionId) {
|
|
@@ -10638,7 +11382,7 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
|
|
|
10638
11382
|
}
|
|
10639
11383
|
form.append("manifest", JSON.stringify(manifest));
|
|
10640
11384
|
const res = await uploadHarnessVersion(creds2, sessionId, form);
|
|
10641
|
-
if (!res.ok || !res.data) throw new
|
|
11385
|
+
if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to upload harness artifact", classifyStatus(res.status), res.errorDetails);
|
|
10642
11386
|
return res.data;
|
|
10643
11387
|
}
|
|
10644
11388
|
async function syncChangedFiles(folder, session) {
|
|
@@ -10662,18 +11406,28 @@ async function syncChangedFiles(folder, session) {
|
|
|
10662
11406
|
title: `CLI sync ${file.path}`
|
|
10663
11407
|
}
|
|
10664
11408
|
);
|
|
10665
|
-
if (!res.ok) throw new
|
|
11409
|
+
if (!res.ok) throw new CliError(res.error ?? `Failed to upload ${file.path}`, classifyStatus(res.status), res.errorDetails);
|
|
10666
11410
|
}
|
|
10667
11411
|
return { localFiles, changed };
|
|
10668
11412
|
}
|
|
10669
|
-
async function runPublish(folderArg) {
|
|
11413
|
+
async function runPublish(folderArg, opts = {}) {
|
|
10670
11414
|
const folder = folderPath2(folderArg);
|
|
10671
11415
|
const session = readSession(folder);
|
|
10672
11416
|
const { localFiles, changed } = await syncChangedFiles(folder, session);
|
|
10673
11417
|
const creds2 = requireCredentials();
|
|
10674
|
-
const build = await buildOperatorWorkspace(
|
|
11418
|
+
const build = await buildOperatorWorkspace(
|
|
11419
|
+
creds2,
|
|
11420
|
+
session.experimentId,
|
|
11421
|
+
session.variationId,
|
|
11422
|
+
opts.allowGoalWrites ? { allowGoalWrites: true } : void 0
|
|
11423
|
+
);
|
|
10675
11424
|
if (!build.ok || !build.data?.ok) {
|
|
10676
|
-
|
|
11425
|
+
const issueDetails = formatWorkspaceBlockingIssues(build.data);
|
|
11426
|
+
throw new CliError(
|
|
11427
|
+
issueDetails ?? build.error ?? JSON.stringify(build.data ?? { error: "Workspace build failed" }, null, 2),
|
|
11428
|
+
classifyStatus(build.status),
|
|
11429
|
+
build.errorDetails
|
|
11430
|
+
);
|
|
10677
11431
|
}
|
|
10678
11432
|
let external = null;
|
|
10679
11433
|
if (shouldUseExternalDelivery(folder)) {
|
|
@@ -10698,32 +11452,50 @@ async function runPublish(folderArg) {
|
|
|
10698
11452
|
}
|
|
10699
11453
|
return { build: build.data, changed, external };
|
|
10700
11454
|
}
|
|
10701
|
-
var
|
|
10702
|
-
operatorCommand
|
|
11455
|
+
var WORKSPACE_CHECKOUT_DIR_REMOVAL_VERSION = "0.4.0";
|
|
11456
|
+
var operatorCommand = new Command("workspace").alias("operator").description("Check out, preview, and publish Apex draft workspace files").addHelpText("after", `
|
|
11457
|
+
Examples:
|
|
11458
|
+
apex workspace checkout <experiment-id> --variation treatment --out ./workspace
|
|
11459
|
+
apex workspace status ./workspace --json
|
|
11460
|
+
apex workspace dev ./workspace --no-open
|
|
11461
|
+
apex workspace publish ./workspace
|
|
11462
|
+
`);
|
|
11463
|
+
operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action25(experimentId, opts) {
|
|
10703
11464
|
try {
|
|
10704
11465
|
const creds2 = requireCredentials();
|
|
10705
11466
|
const expRes = await getExperiment(creds2, experimentId);
|
|
10706
|
-
if (!expRes.ok || !expRes.data) throw new
|
|
10707
|
-
const
|
|
10708
|
-
const
|
|
10709
|
-
|
|
10710
|
-
|
|
11467
|
+
if (!expRes.ok || !expRes.data) throw new CliError(expRes.error ?? "Experiment not found", classifyStatus(expRes.status), expRes.errorDetails);
|
|
11468
|
+
const experiment = expRes.data;
|
|
11469
|
+
const variation = resolveVariation(experiment, opts.variation);
|
|
11470
|
+
if (opts.dir !== void 0) {
|
|
11471
|
+
process.stderr.write(`Warning: --dir is deprecated for apex workspace checkout; use --out instead. --dir will be removed in v${WORKSPACE_CHECKOUT_DIR_REMOVAL_VERSION}.
|
|
11472
|
+
`);
|
|
11473
|
+
markDeprecatedInvocation({
|
|
11474
|
+
option: "--dir",
|
|
11475
|
+
replacement: "--out",
|
|
11476
|
+
removalVersion: WORKSPACE_CHECKOUT_DIR_REMOVAL_VERSION
|
|
11477
|
+
});
|
|
11478
|
+
}
|
|
11479
|
+
const folder = folderPath2(opts.out ?? opts.dir ?? slugify2(`${experiment.name}-${variation.name}`, "apex-operator"));
|
|
11480
|
+
const workspace = await getOperatorWorkspace(creds2, experiment.id, variation.id);
|
|
11481
|
+
if (!workspace.ok || !workspace.data) throw new CliError(workspace.error ?? "Could not load workspace", classifyStatus(workspace.status), workspace.errorDetails);
|
|
10711
11482
|
import_node_fs9.default.mkdirSync(folder, { recursive: true });
|
|
10712
11483
|
writeWorkspaceFiles(folder, workspace.data);
|
|
10713
11484
|
writeSession(folder, sessionFromWorkspace({
|
|
10714
11485
|
credsBaseUrl: creds2.baseUrl,
|
|
10715
|
-
experimentId:
|
|
11486
|
+
experimentId: experiment.id,
|
|
10716
11487
|
variationId: variation.id,
|
|
10717
11488
|
workspace: workspace.data
|
|
10718
11489
|
}));
|
|
10719
|
-
|
|
10720
|
-
|
|
11490
|
+
output(this, { folder, experimentId: experiment.id, variationId: variation.id, files: workspace.data.files.map((file) => file.path) }, () => {
|
|
11491
|
+
printSuccess(`Checked out ${experiment.name} / ${variation.name}`);
|
|
11492
|
+
console.log(source_default.cyan(folder));
|
|
11493
|
+
});
|
|
10721
11494
|
} catch (error) {
|
|
10722
|
-
|
|
10723
|
-
process.exit(1);
|
|
11495
|
+
throw error;
|
|
10724
11496
|
}
|
|
10725
11497
|
});
|
|
10726
|
-
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function
|
|
11498
|
+
operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action26(folderArg) {
|
|
10727
11499
|
try {
|
|
10728
11500
|
const folder = folderPath2(folderArg);
|
|
10729
11501
|
const session = readSession(folder);
|
|
@@ -10751,18 +11523,12 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
|
|
|
10751
11523
|
fail(this, error instanceof Error ? error.message : String(error));
|
|
10752
11524
|
}
|
|
10753
11525
|
});
|
|
10754
|
-
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 (folderArg, opts)
|
|
11526
|
+
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 action27(folderArg, opts) {
|
|
10755
11527
|
try {
|
|
10756
11528
|
const folder = folderPath2(folderArg);
|
|
10757
11529
|
const session = readSession(folder);
|
|
10758
11530
|
const harnessSession = await createExactHarnessSession(folder, session);
|
|
10759
11531
|
const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
|
|
10760
|
-
console.log(source_default.bold("\nApex Workspace Preview"));
|
|
10761
|
-
console.log(`Experiment: ${harnessSession.experimentId}`);
|
|
10762
|
-
console.log(`Variation: ${harnessSession.variationId}`);
|
|
10763
|
-
console.log(`Version: ${version.version}`);
|
|
10764
|
-
console.log(source_default.cyan(harnessSession.previewUrl));
|
|
10765
|
-
console.log();
|
|
10766
11532
|
import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
|
|
10767
11533
|
writeJsonFile(import_node_path7.default.join(apexDir(folder), "harness-session.json"), {
|
|
10768
11534
|
sessionId: harnessSession.sessionId,
|
|
@@ -10772,6 +11538,14 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
10772
11538
|
previewUrl: harnessSession.previewUrl,
|
|
10773
11539
|
lastVersion: version.version
|
|
10774
11540
|
});
|
|
11541
|
+
output(this, { folder, experimentId: harnessSession.experimentId, variationId: harnessSession.variationId, version: version.version, previewUrl: harnessSession.previewUrl }, () => {
|
|
11542
|
+
console.log(source_default.bold("\nApex Workspace Preview"));
|
|
11543
|
+
console.log(`Experiment: ${harnessSession.experimentId}`);
|
|
11544
|
+
console.log(`Variation: ${harnessSession.variationId}`);
|
|
11545
|
+
console.log(`Version: ${version.version}`);
|
|
11546
|
+
console.log(source_default.cyan(harnessSession.previewUrl));
|
|
11547
|
+
console.log();
|
|
11548
|
+
});
|
|
10775
11549
|
if (opts.open !== false) {
|
|
10776
11550
|
const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
10777
11551
|
await open2(harnessSession.previewUrl);
|
|
@@ -10779,7 +11553,9 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
10779
11553
|
if (opts.once) return;
|
|
10780
11554
|
let uploading = false;
|
|
10781
11555
|
let queued = false;
|
|
11556
|
+
let closing = false;
|
|
10782
11557
|
const upload2 = async () => {
|
|
11558
|
+
if (closing) return;
|
|
10783
11559
|
if (uploading) {
|
|
10784
11560
|
queued = true;
|
|
10785
11561
|
return;
|
|
@@ -10787,12 +11563,12 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
10787
11563
|
uploading = true;
|
|
10788
11564
|
try {
|
|
10789
11565
|
const next = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
|
|
10790
|
-
console.
|
|
11566
|
+
console.error(source_default.dim(`[operator] uploaded preview version ${next.version}`));
|
|
10791
11567
|
} catch (error) {
|
|
10792
11568
|
printWarning(error instanceof Error ? error.message : String(error));
|
|
10793
11569
|
} finally {
|
|
10794
11570
|
uploading = false;
|
|
10795
|
-
if (queued) {
|
|
11571
|
+
if (queued && !closing) {
|
|
10796
11572
|
queued = false;
|
|
10797
11573
|
void upload2();
|
|
10798
11574
|
}
|
|
@@ -10806,28 +11582,29 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
|
|
|
10806
11582
|
timer = setTimeout(() => void upload2(), 300);
|
|
10807
11583
|
});
|
|
10808
11584
|
process.on("SIGINT", () => {
|
|
11585
|
+
closing = true;
|
|
11586
|
+
queued = false;
|
|
11587
|
+
if (timer) clearTimeout(timer);
|
|
10809
11588
|
watcher.close();
|
|
10810
|
-
process.
|
|
11589
|
+
process.exitCode = 0;
|
|
10811
11590
|
});
|
|
10812
11591
|
} catch (error) {
|
|
10813
|
-
|
|
10814
|
-
process.exit(1);
|
|
11592
|
+
throw error;
|
|
10815
11593
|
}
|
|
10816
11594
|
});
|
|
10817
|
-
operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").action(async (folderArg)
|
|
11595
|
+
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 action28(folderArg, opts) {
|
|
10818
11596
|
try {
|
|
10819
|
-
const result = await runPublish(folderArg);
|
|
10820
|
-
printSuccess(`Published ${result.changed.length} changed file(s) to Apex`);
|
|
11597
|
+
const result = await runPublish(folderArg, opts);
|
|
10821
11598
|
const build = result.build;
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
console.log(`
|
|
10827
|
-
|
|
11599
|
+
output(this, { changed: result.changed, build, external: result.external ?? null }, () => {
|
|
11600
|
+
printSuccess(`Published ${result.changed.length} changed file(s) to Apex`);
|
|
11601
|
+
console.log(`Mutations: ${build.mutationCount ?? 0}`);
|
|
11602
|
+
console.log(`CSS bytes: ${build.customCssBytes ?? 0}`);
|
|
11603
|
+
console.log(`JS bytes: ${build.customJsBytes ?? 0}`);
|
|
11604
|
+
if (result.external) console.log(`External artifacts: version ${result.external.version}`);
|
|
11605
|
+
});
|
|
10828
11606
|
} catch (error) {
|
|
10829
|
-
|
|
10830
|
-
process.exit(1);
|
|
11607
|
+
throw error;
|
|
10831
11608
|
}
|
|
10832
11609
|
});
|
|
10833
11610
|
|
|
@@ -10858,7 +11635,7 @@ function briefLine(item) {
|
|
|
10858
11635
|
].filter(Boolean);
|
|
10859
11636
|
return parts.join("\n");
|
|
10860
11637
|
}
|
|
10861
|
-
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 (opts)
|
|
11638
|
+
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 action29(opts) {
|
|
10862
11639
|
const creds2 = requireCredentials();
|
|
10863
11640
|
const limit = parseInt(opts.limit, 10);
|
|
10864
11641
|
const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
|
|
@@ -10868,63 +11645,56 @@ var approved = new Command("approved").description("List approved roadmap ideas
|
|
|
10868
11645
|
q: opts.query,
|
|
10869
11646
|
hasExperiment
|
|
10870
11647
|
});
|
|
10871
|
-
if (!res.ok)
|
|
10872
|
-
printError(res.error ?? "Failed to fetch approved roadmap items");
|
|
10873
|
-
process.exit(1);
|
|
10874
|
-
}
|
|
10875
|
-
if (opts.json) {
|
|
10876
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
10877
|
-
return;
|
|
10878
|
-
}
|
|
11648
|
+
if (!res.ok) fail(this, res.error ?? "Failed to fetch approved roadmap items", classifyStatus(res.status), res.errorDetails);
|
|
10879
11649
|
const items = res.data?.data ?? [];
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
item
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
for (const item of items) {
|
|
10898
|
-
console.log(source_default.bold(item.title));
|
|
10899
|
-
const detail5 = briefLine(item);
|
|
10900
|
-
if (detail5) console.log(detail5);
|
|
10901
|
-
console.log(source_default.dim(`apex roadmap handoff ${item.id}`));
|
|
11650
|
+
output(this, res.data ?? { data: [], pagination: { has_more: false, next_cursor: null } }, () => {
|
|
11651
|
+
if (items.length === 0) {
|
|
11652
|
+
console.log(source_default.dim("No approved roadmap items found."));
|
|
11653
|
+
return;
|
|
11654
|
+
}
|
|
11655
|
+
printTable(
|
|
11656
|
+
["ID", "SCORE", "TYPE", "EXPERIMENT", "TITLE"],
|
|
11657
|
+
items.map((item) => [
|
|
11658
|
+
item.id,
|
|
11659
|
+
score(item.computed_score),
|
|
11660
|
+
truncate(item.action_type, 14),
|
|
11661
|
+
truncate(experimentState(item), 18),
|
|
11662
|
+
truncate(item.title, 54)
|
|
11663
|
+
]),
|
|
11664
|
+
[36, 6, 14, 18, 54]
|
|
11665
|
+
);
|
|
11666
|
+
if (opts.brief) {
|
|
10902
11667
|
console.log();
|
|
11668
|
+
for (const item of items) {
|
|
11669
|
+
console.log(source_default.bold(item.title));
|
|
11670
|
+
const detail5 = briefLine(item);
|
|
11671
|
+
if (detail5) console.log(detail5);
|
|
11672
|
+
console.log(source_default.dim(`apex roadmap handoff ${item.id}`));
|
|
11673
|
+
console.log();
|
|
11674
|
+
}
|
|
10903
11675
|
}
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
console.log(source_default.dim(`
|
|
11676
|
+
if (res.data?.pagination.has_more) {
|
|
11677
|
+
console.log(source_default.dim(`
|
|
10907
11678
|
More results: --cursor ${res.data.pagination.next_cursor}`));
|
|
10908
|
-
|
|
11679
|
+
}
|
|
11680
|
+
});
|
|
10909
11681
|
});
|
|
10910
|
-
var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("
|
|
11682
|
+
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 action30(id, opts) {
|
|
10911
11683
|
const creds2 = requireCredentials();
|
|
10912
11684
|
const res = await getRoadmapItemHandoff(creds2, id);
|
|
10913
|
-
if (!res.ok)
|
|
10914
|
-
printError(res.error ?? "Failed to fetch roadmap handoff");
|
|
10915
|
-
process.exit(1);
|
|
10916
|
-
}
|
|
11685
|
+
if (!res.ok) fail(this, res.error ?? "Failed to fetch roadmap handoff", classifyStatus(res.status), res.errorDetails);
|
|
10917
11686
|
const payload = res.data ?? {};
|
|
10918
|
-
const
|
|
11687
|
+
const rendered = opts.instructions ? String(payload.instructions ?? "") : `${JSON.stringify(payload, null, 2)}
|
|
10919
11688
|
`;
|
|
10920
|
-
|
|
10921
|
-
|
|
11689
|
+
const outputData = opts.instructions ? { roadmapItemId: id, instructions: String(payload.instructions ?? "") } : payload;
|
|
11690
|
+
if (opts.out || opts.output) {
|
|
11691
|
+
const target = import_node_path8.default.resolve(process.cwd(), opts.out ?? opts.output);
|
|
10922
11692
|
import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(target), { recursive: true });
|
|
10923
|
-
import_node_fs10.default.writeFileSync(target,
|
|
10924
|
-
printSuccess(`Wrote ${target}`);
|
|
11693
|
+
import_node_fs10.default.writeFileSync(target, rendered, "utf8");
|
|
11694
|
+
output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
|
|
10925
11695
|
return;
|
|
10926
11696
|
}
|
|
10927
|
-
console.log(
|
|
11697
|
+
output(this, outputData, () => console.log(rendered));
|
|
10928
11698
|
});
|
|
10929
11699
|
var roadmapCommand = new Command("roadmap").description("Work with approved roadmap ideas and build handoffs");
|
|
10930
11700
|
roadmapCommand.addCommand(approved);
|
|
@@ -11104,7 +11874,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
|
|
|
11104
11874
|
console.log(`${index + 1}. ${check}`);
|
|
11105
11875
|
});
|
|
11106
11876
|
});
|
|
11107
|
-
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
|
|
11877
|
+
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 action31(opts) {
|
|
11108
11878
|
const creds2 = requireCredentials();
|
|
11109
11879
|
const res = await verifyTracking(creds2, {
|
|
11110
11880
|
minutes: Number.parseInt(opts.minutes, 10),
|
|
@@ -11144,6 +11914,151 @@ trackingCommand.command("verify").description("Verify live tracking events for t
|
|
|
11144
11914
|
init_source();
|
|
11145
11915
|
var import_node_fs12 = __toESM(require("node:fs"));
|
|
11146
11916
|
var import_node_path10 = __toESM(require("node:path"));
|
|
11917
|
+
|
|
11918
|
+
// src/lib/sdk-url-match.ts
|
|
11919
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
11920
|
+
var MAX_CACHE_ENTRIES = 256;
|
|
11921
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
11922
|
+
function cacheRegex(pattern, compiled) {
|
|
11923
|
+
if (!regexCache.has(pattern) && regexCache.size >= MAX_CACHE_ENTRIES) {
|
|
11924
|
+
const oldest = regexCache.keys().next().value;
|
|
11925
|
+
if (oldest) regexCache.delete(oldest);
|
|
11926
|
+
}
|
|
11927
|
+
regexCache.set(pattern, compiled);
|
|
11928
|
+
}
|
|
11929
|
+
function safeRegexTest(pattern, input) {
|
|
11930
|
+
if (!pattern || pattern.length > MAX_PATTERN_LENGTH) return false;
|
|
11931
|
+
if (regexCache.has(pattern)) {
|
|
11932
|
+
const cached = regexCache.get(pattern);
|
|
11933
|
+
return cached ? cached.test(input) : false;
|
|
11934
|
+
}
|
|
11935
|
+
try {
|
|
11936
|
+
const compiled = new RegExp(pattern);
|
|
11937
|
+
cacheRegex(pattern, compiled);
|
|
11938
|
+
return compiled.test(input);
|
|
11939
|
+
} catch {
|
|
11940
|
+
cacheRegex(pattern, null);
|
|
11941
|
+
return false;
|
|
11942
|
+
}
|
|
11943
|
+
}
|
|
11944
|
+
|
|
11945
|
+
// src/lib/sdk-goal-url-match.ts
|
|
11946
|
+
function sdkPageviewGoalMatch(urlPattern, href) {
|
|
11947
|
+
if (!urlPattern) return true;
|
|
11948
|
+
return safeRegexTest(urlPattern, href);
|
|
11949
|
+
}
|
|
11950
|
+
|
|
11951
|
+
// src/commands/goals.ts
|
|
11952
|
+
var INVENTORY_PAGE_SIZE = 100;
|
|
11953
|
+
var GOALS_PAGE_SIZE = 100;
|
|
11954
|
+
var MAX_GOAL_PAGES = 100;
|
|
11955
|
+
var NON_QUANTIFIABLE = /* @__PURE__ */ new Set(["(", "|", "^", "$"]);
|
|
11956
|
+
function urlPatternDependence(pattern) {
|
|
11957
|
+
let fragment = false;
|
|
11958
|
+
let inClass = false;
|
|
11959
|
+
let quantifiable = false;
|
|
11960
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
11961
|
+
const ch = pattern[i];
|
|
11962
|
+
if (ch === "\\") {
|
|
11963
|
+
const next = pattern[i + 1];
|
|
11964
|
+
i += 1;
|
|
11965
|
+
if (next === void 0) break;
|
|
11966
|
+
if (next === "?" || next === "&") return "query";
|
|
11967
|
+
if (next === "#") fragment = true;
|
|
11968
|
+
quantifiable = true;
|
|
11969
|
+
continue;
|
|
11970
|
+
}
|
|
11971
|
+
if (inClass) {
|
|
11972
|
+
if (ch === "]") {
|
|
11973
|
+
inClass = false;
|
|
11974
|
+
quantifiable = true;
|
|
11975
|
+
continue;
|
|
11976
|
+
}
|
|
11977
|
+
if (ch === "?" || ch === "&") return "query";
|
|
11978
|
+
if (ch === "#") fragment = true;
|
|
11979
|
+
continue;
|
|
11980
|
+
}
|
|
11981
|
+
if (ch === "[") {
|
|
11982
|
+
inClass = true;
|
|
11983
|
+
quantifiable = false;
|
|
11984
|
+
continue;
|
|
11985
|
+
}
|
|
11986
|
+
if (ch === "&") return "query";
|
|
11987
|
+
if (ch === "#") {
|
|
11988
|
+
fragment = true;
|
|
11989
|
+
quantifiable = true;
|
|
11990
|
+
continue;
|
|
11991
|
+
}
|
|
11992
|
+
if (ch === "?") {
|
|
11993
|
+
if (!quantifiable && pattern[i - 1] !== "(") return "query";
|
|
11994
|
+
quantifiable = true;
|
|
11995
|
+
continue;
|
|
11996
|
+
}
|
|
11997
|
+
quantifiable = !NON_QUANTIFIABLE.has(ch);
|
|
11998
|
+
}
|
|
11999
|
+
return fragment ? "fragment" : "none";
|
|
12000
|
+
}
|
|
12001
|
+
var PAGE_SCOPED_REASON = "page-scoped goal: matching is decided by the assigned Page, not url_pattern \u2014 inventory dry-run does not apply";
|
|
12002
|
+
function goalPageId(goal) {
|
|
12003
|
+
const raw = goal.page_id ?? goal.pageId ?? null;
|
|
12004
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
12005
|
+
}
|
|
12006
|
+
var DEPENDENCE_REASON = {
|
|
12007
|
+
query: "URL inventory is path-level only, so query-string-dependent patterns cannot be counted safely.",
|
|
12008
|
+
fragment: "URL fragments are never sent to the server, so fragment-dependent patterns cannot be counted safely."
|
|
12009
|
+
};
|
|
12010
|
+
function inventoryHref(item) {
|
|
12011
|
+
try {
|
|
12012
|
+
return new URL(item.path, `https://${item.host}`).href;
|
|
12013
|
+
} catch {
|
|
12014
|
+
return null;
|
|
12015
|
+
}
|
|
12016
|
+
}
|
|
12017
|
+
async function findGoalById(command, creds2, goalId) {
|
|
12018
|
+
let cursor;
|
|
12019
|
+
for (let page = 0; page < MAX_GOAL_PAGES; page += 1) {
|
|
12020
|
+
const res = await listGoals(creds2, { limit: GOALS_PAGE_SIZE, cursor });
|
|
12021
|
+
if (!res.ok || !res.data) fail(command, res.error ?? "Failed to read goals", classifyStatus(res.status), res.errorDetails);
|
|
12022
|
+
const match = res.data.data.find((item) => item.id === goalId);
|
|
12023
|
+
if (match) return match;
|
|
12024
|
+
const next = res.data.pagination?.next_cursor ?? null;
|
|
12025
|
+
if (!res.data.pagination?.has_more || !next || next === cursor) return null;
|
|
12026
|
+
cursor = next;
|
|
12027
|
+
}
|
|
12028
|
+
return null;
|
|
12029
|
+
}
|
|
12030
|
+
async function readAllInventory(command, creds2, days) {
|
|
12031
|
+
const items = [];
|
|
12032
|
+
let offset = 0;
|
|
12033
|
+
let total = 0;
|
|
12034
|
+
let truncated = false;
|
|
12035
|
+
do {
|
|
12036
|
+
const res = await getUrlInventory(creds2, { days, limit: INVENTORY_PAGE_SIZE, offset });
|
|
12037
|
+
if (!res.ok || !res.data) fail(command, res.error ?? "Failed to read URL inventory", classifyStatus(res.status), res.errorDetails);
|
|
12038
|
+
items.push(...res.data.items);
|
|
12039
|
+
total = res.data.total;
|
|
12040
|
+
truncated = truncated || res.data.truncated;
|
|
12041
|
+
offset += res.data.items.length;
|
|
12042
|
+
if (res.data.items.length === 0) break;
|
|
12043
|
+
} while (offset < total);
|
|
12044
|
+
return { items, total, truncated };
|
|
12045
|
+
}
|
|
12046
|
+
function printSuggestions(data) {
|
|
12047
|
+
if (data.goals.length) {
|
|
12048
|
+
console.log(source_default.bold("Suggested goals"));
|
|
12049
|
+
printTable(["NAME", "TYPE", "PATTERN/SELECTOR/EVENT"], data.goals.map((goal) => [goal.name.slice(0, 36), goal.type, String(goal.urlPattern ?? goal.selector ?? goal.eventName ?? "-").slice(0, 56)]), [36, 10, 56]);
|
|
12050
|
+
} else console.log(source_default.dim("No suggested goals."));
|
|
12051
|
+
if (data.funnels.length) {
|
|
12052
|
+
console.log();
|
|
12053
|
+
console.log(source_default.bold("Suggested funnels"));
|
|
12054
|
+
for (const funnel of data.funnels) console.log(`- ${funnel.name}: ${funnel.steps.join(" -> ")}`);
|
|
12055
|
+
}
|
|
12056
|
+
if (data.triggers.length) {
|
|
12057
|
+
console.log();
|
|
12058
|
+
console.log(source_default.bold("Suggested test triggers"));
|
|
12059
|
+
for (const trigger of data.triggers) console.log(`- ${trigger.page}: ${trigger.description}`);
|
|
12060
|
+
}
|
|
12061
|
+
}
|
|
11147
12062
|
function parseCapture(command, value) {
|
|
11148
12063
|
if (!value) return void 0;
|
|
11149
12064
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
@@ -11172,7 +12087,7 @@ function printGoal(goal) {
|
|
|
11172
12087
|
console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
|
|
11173
12088
|
console.log();
|
|
11174
12089
|
}
|
|
11175
|
-
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function
|
|
12090
|
+
var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action32(opts) {
|
|
11176
12091
|
const creds2 = requireCredentials();
|
|
11177
12092
|
const res = await listGoals(creds2, { limit: Number.parseInt(opts.limit, 10) });
|
|
11178
12093
|
if (!res.ok || !res.data) {
|
|
@@ -11194,7 +12109,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
|
|
|
11194
12109
|
);
|
|
11195
12110
|
});
|
|
11196
12111
|
});
|
|
11197
|
-
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").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
|
|
12112
|
+
var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").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 action33(opts) {
|
|
11198
12113
|
const creds2 = requireCredentials();
|
|
11199
12114
|
const body5 = cleanBody({
|
|
11200
12115
|
name: opts.name,
|
|
@@ -11212,7 +12127,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
|
|
|
11212
12127
|
}
|
|
11213
12128
|
output(this, { goal: res.data }, () => printGoal(res.data));
|
|
11214
12129
|
});
|
|
11215
|
-
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("--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
|
|
12130
|
+
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("--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 action34(id, opts) {
|
|
11216
12131
|
const creds2 = requireCredentials();
|
|
11217
12132
|
const body5 = cleanBody({
|
|
11218
12133
|
name: opts.name,
|
|
@@ -11233,7 +12148,137 @@ var update = new Command("update").description("Update a goal").argument("<id>",
|
|
|
11233
12148
|
}
|
|
11234
12149
|
output(this, { goal: res.data }, () => printGoal(res.data));
|
|
11235
12150
|
});
|
|
11236
|
-
var
|
|
12151
|
+
var test = new Command("test").description("Dry run a goal URL pattern against traffic").option("--pattern <regex>", "URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)").option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action35(opts) {
|
|
12152
|
+
if (!opts.pattern && !opts.goal) fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
|
|
12153
|
+
if (opts.pattern && opts.goal) fail(this, "Use only one of --pattern or --goal.", EXIT_CODES.validation);
|
|
12154
|
+
const creds2 = requireCredentials();
|
|
12155
|
+
let pattern = opts.pattern;
|
|
12156
|
+
if (opts.goal) {
|
|
12157
|
+
const goal = await findGoalById(this, creds2, opts.goal);
|
|
12158
|
+
if (!goal) fail(this, `Goal not found: ${opts.goal}`, EXIT_CODES.validation);
|
|
12159
|
+
const pageId = goalPageId(goal);
|
|
12160
|
+
if (pageId) {
|
|
12161
|
+
output(this, { verdict: "indeterminate", scope: "page", goal: goal.id, page_id: pageId, pattern: goal.url_pattern ?? null, reason: PAGE_SCOPED_REASON }, () => {
|
|
12162
|
+
console.log(source_default.yellow("INDETERMINATE"));
|
|
12163
|
+
console.log(PAGE_SCOPED_REASON);
|
|
12164
|
+
});
|
|
12165
|
+
return;
|
|
12166
|
+
}
|
|
12167
|
+
pattern = goal.url_pattern ?? void 0;
|
|
12168
|
+
if (!pattern) fail(this, `Goal ${opts.goal} has no URL pattern.`, EXIT_CODES.validation);
|
|
12169
|
+
}
|
|
12170
|
+
const days = Math.max(1, Number.parseInt(opts.days, 10) || 14);
|
|
12171
|
+
const dependence = urlPatternDependence(pattern);
|
|
12172
|
+
if (dependence !== "none") {
|
|
12173
|
+
const reason = DEPENDENCE_REASON[dependence];
|
|
12174
|
+
output(this, { verdict: "indeterminate", pattern, dependence, reason }, () => {
|
|
12175
|
+
console.log(source_default.yellow("INDETERMINATE"));
|
|
12176
|
+
console.log(reason);
|
|
12177
|
+
});
|
|
12178
|
+
return;
|
|
12179
|
+
}
|
|
12180
|
+
const inventory = await readAllInventory(this, creds2, days);
|
|
12181
|
+
const matches = inventory.items.filter((item) => {
|
|
12182
|
+
const href = inventoryHref(item);
|
|
12183
|
+
return href !== null && sdkPageviewGoalMatch(pattern, href);
|
|
12184
|
+
});
|
|
12185
|
+
const totalPageviews = matches.reduce((sum, item) => sum + item.pageviews, 0);
|
|
12186
|
+
output(this, { verdict: "determinate", pattern, days, matches, total_pageviews: totalPageviews, inventory_total: inventory.total, truncated: inventory.truncated, visitor_note: "Per-URL visitor counts are not unioned client-side.", match_note: "Patterns are tested against the full URL (https://host/path), matching the SDK pageview-goal path." }, () => {
|
|
12187
|
+
console.log(source_default.bold(`Matched pageviews: ${totalPageviews}`));
|
|
12188
|
+
console.log(source_default.dim("Per-URL visitor counts are shown; total visitors are not unioned client-side."));
|
|
12189
|
+
if (!matches.length) {
|
|
12190
|
+
console.log(source_default.dim("No matching URLs."));
|
|
12191
|
+
return;
|
|
12192
|
+
}
|
|
12193
|
+
printTable(["URL", "PAGEVIEWS", "VISITORS"], matches.map((item) => [`${item.host}${item.path}`.slice(0, 64), String(item.pageviews), String(item.visitors)]), [64, 10, 10]);
|
|
12194
|
+
});
|
|
12195
|
+
});
|
|
12196
|
+
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 action36(opts) {
|
|
12197
|
+
const creds2 = requireCredentials();
|
|
12198
|
+
const res = await suggestGoals(creds2);
|
|
12199
|
+
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to suggest goals", classifyStatus(res.status), res.errorDetails);
|
|
12200
|
+
output(this, res.data, () => printSuggestions(res.data));
|
|
12201
|
+
if (!opts.create) return;
|
|
12202
|
+
for (const goal of res.data.goals) {
|
|
12203
|
+
console.error(`Would create ${goal.type} goal: ${goal.name}`);
|
|
12204
|
+
if (!opts.yes) {
|
|
12205
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) fail(this, "Confirmation required; rerun with --yes to create suggested goals.", EXIT_CODES.confirmationRequired);
|
|
12206
|
+
const readline2 = await import("node:readline");
|
|
12207
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
12208
|
+
const answer = await new Promise((resolve) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve));
|
|
12209
|
+
rl.close();
|
|
12210
|
+
if (answer.toLowerCase() !== "y") continue;
|
|
12211
|
+
}
|
|
12212
|
+
const created = await createGoal(creds2, cleanBody({ name: goal.name, type: goal.type, selector: goal.selector, url_pattern: goal.urlPattern, event_name: goal.eventName }));
|
|
12213
|
+
if (!created.ok || !created.data) fail(this, created.error ?? "Failed to create suggested goal", classifyStatus(created.status), created.errorDetails);
|
|
12214
|
+
}
|
|
12215
|
+
});
|
|
12216
|
+
var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update).addCommand(test).addCommand(suggest).addHelpText("after", `
|
|
12217
|
+
Examples:
|
|
12218
|
+
apex goals list --limit 20
|
|
12219
|
+
apex goals create --name "Checkout click" --type click --selector "button[name=checkout]"
|
|
12220
|
+
apex goals create --name "Purchase" --type revenue --count-once
|
|
12221
|
+
`);
|
|
12222
|
+
|
|
12223
|
+
// src/commands/funnels.ts
|
|
12224
|
+
init_source();
|
|
12225
|
+
var import_node_fs13 = __toESM(require("node:fs"));
|
|
12226
|
+
var import_node_path11 = __toESM(require("node:path"));
|
|
12227
|
+
function readSteps(command, file) {
|
|
12228
|
+
let parsed;
|
|
12229
|
+
try {
|
|
12230
|
+
parsed = JSON.parse(import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), file), "utf8"));
|
|
12231
|
+
} catch (error) {
|
|
12232
|
+
fail(command, `Invalid --steps-file JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.validation);
|
|
12233
|
+
}
|
|
12234
|
+
if (!Array.isArray(parsed) || parsed.length === 0) fail(command, "steps must be a non-empty array", EXIT_CODES.validation);
|
|
12235
|
+
return parsed.map((step, index) => {
|
|
12236
|
+
if (!step || typeof step !== "object" || typeof step.label !== "string" || typeof step.pattern !== "string") {
|
|
12237
|
+
fail(command, `step ${index + 1} must have label and pattern strings`, EXIT_CODES.validation);
|
|
12238
|
+
}
|
|
12239
|
+
return { order: index + 1, label: step.label, url_pattern: step.pattern };
|
|
12240
|
+
});
|
|
12241
|
+
}
|
|
12242
|
+
function printFunnel(funnel) {
|
|
12243
|
+
console.log();
|
|
12244
|
+
console.log(source_default.bold(funnel.name));
|
|
12245
|
+
console.log(` ID: ${source_default.dim(funnel.id)}`);
|
|
12246
|
+
for (const step of [...funnel.steps].sort((a, b) => a.order - b.order)) console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
|
|
12247
|
+
console.log();
|
|
12248
|
+
}
|
|
12249
|
+
var list3 = new Command("list").alias("ls").description("List funnels").action(async function action37() {
|
|
12250
|
+
const res = await listFunnels(requireCredentials());
|
|
12251
|
+
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list funnels", classifyStatus(res.status), res.errorDetails);
|
|
12252
|
+
output(this, { funnels: res.data.data }, () => printTable(["NAME", "ID", "STEPS"], res.data.data.map((f) => [f.name.slice(0, 36), f.id, String(f.steps.length)]), [36, 36, 5]));
|
|
12253
|
+
});
|
|
12254
|
+
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 action38(opts) {
|
|
12255
|
+
const res = await createFunnel(requireCredentials(), { name: opts.name, steps: readSteps(this, opts.stepsFile) });
|
|
12256
|
+
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to create funnel", classifyStatus(res.status), res.errorDetails);
|
|
12257
|
+
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
12258
|
+
});
|
|
12259
|
+
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 action39(id, opts) {
|
|
12260
|
+
const res = await updateFunnel(requireCredentials(), id, { steps: readSteps(this, opts.stepsFile) });
|
|
12261
|
+
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update funnel", classifyStatus(res.status), res.errorDetails);
|
|
12262
|
+
output(this, { funnel: res.data }, () => printFunnel(res.data));
|
|
12263
|
+
});
|
|
12264
|
+
var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action40(id) {
|
|
12265
|
+
const res = await deleteFunnel(requireCredentials(), id);
|
|
12266
|
+
if (!res.ok) fail(this, res.error ?? "Failed to delete funnel", classifyStatus(res.status), res.errorDetails);
|
|
12267
|
+
output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
|
|
12268
|
+
});
|
|
12269
|
+
var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action41(id, opts) {
|
|
12270
|
+
if (!/^[1-9]\d*$/.test(opts.days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
|
|
12271
|
+
const days = Number.parseInt(opts.days, 10);
|
|
12272
|
+
if (!Number.isSafeInteger(days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
|
|
12273
|
+
const res = await runFunnel(requireCredentials(), id, days);
|
|
12274
|
+
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to run funnel", classifyStatus(res.status), res.errorDetails);
|
|
12275
|
+
output(this, { run: res.data }, () => printTable(["STEP", "LABEL", "VISITORS", "DROP-OFF"], res.data.steps.map((s) => [String(s.step), s.label, String(s.visitors), `${s.drop_off_rate}%`]), [5, 36, 8, 8]));
|
|
12276
|
+
});
|
|
12277
|
+
var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action42(opts) {
|
|
12278
|
+
const steps = readSteps(this, opts.stepsFile);
|
|
12279
|
+
output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
|
|
12280
|
+
});
|
|
12281
|
+
var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update2).addCommand(del2).addCommand(run).addCommand(validate2);
|
|
11237
12282
|
|
|
11238
12283
|
// src/commands/environments.ts
|
|
11239
12284
|
init_source();
|
|
@@ -11286,11 +12331,11 @@ async function load2(command) {
|
|
|
11286
12331
|
}
|
|
11287
12332
|
return result.data;
|
|
11288
12333
|
}
|
|
11289
|
-
var
|
|
12334
|
+
var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action43() {
|
|
11290
12335
|
const { environments } = await load2(this);
|
|
11291
12336
|
output(this, { environments }, () => printEnvironments(environments.items));
|
|
11292
12337
|
});
|
|
11293
|
-
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
|
|
12338
|
+
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 action44(domain, opts) {
|
|
11294
12339
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
11295
12340
|
if (!normalizedDomain) {
|
|
11296
12341
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -11333,7 +12378,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
|
|
|
11333
12378
|
() => printEnvironments(result.data.environments.items)
|
|
11334
12379
|
);
|
|
11335
12380
|
});
|
|
11336
|
-
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function
|
|
12381
|
+
var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action45(domain) {
|
|
11337
12382
|
const normalizedDomain = normalizeEnvironmentDomain(domain);
|
|
11338
12383
|
if (!normalizedDomain) {
|
|
11339
12384
|
fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
@@ -11366,7 +12411,200 @@ var removeDomain = new Command("remove-domain").description("Remove a domain map
|
|
|
11366
12411
|
() => printEnvironments(result.data.environments.items)
|
|
11367
12412
|
);
|
|
11368
12413
|
});
|
|
11369
|
-
var environmentsCommand = new Command("environments").description("Manage runtime environments and domain mappings").addCommand(
|
|
12414
|
+
var environmentsCommand = new Command("environments").description("Manage runtime environments and domain mappings").addCommand(list4).addCommand(setDomain).addCommand(removeDomain);
|
|
12415
|
+
|
|
12416
|
+
// src/commands/consent.ts
|
|
12417
|
+
init_source();
|
|
12418
|
+
function warn(command, precompile) {
|
|
12419
|
+
if (!shouldUseJson(command) && precompile && precompile.status !== "ok") {
|
|
12420
|
+
console.error(source_default.yellow(`Warning (${precompile.status}): ${precompile.summary}`));
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12423
|
+
async function load3(command) {
|
|
12424
|
+
const result = await getConsent(requireCredentials());
|
|
12425
|
+
if (!result.ok || !result.data) fail(command, result.error ?? "Failed to load consent settings", classifyStatus(result.status), result.errorDetails);
|
|
12426
|
+
return result.data;
|
|
12427
|
+
}
|
|
12428
|
+
function printConsent(data) {
|
|
12429
|
+
console.log(`Consent: ${data.consentMode.enabled ? "on" : "off"}`);
|
|
12430
|
+
console.log(`Provider: ${data.consentMode.provider || "-"}`);
|
|
12431
|
+
console.log(`Group: ${data.consentMode.requiredGroup || "-"}`);
|
|
12432
|
+
console.log(`Getter: ${data.consentMode.getter || "-"}`);
|
|
12433
|
+
console.log(`Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`);
|
|
12434
|
+
}
|
|
12435
|
+
var get2 = new Command("get").description("Show consent settings").action(async function action46() {
|
|
12436
|
+
const data = await load3(this);
|
|
12437
|
+
output(this, data, () => printConsent(data));
|
|
12438
|
+
});
|
|
12439
|
+
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 action47(opts) {
|
|
12440
|
+
const consentMode = { enabled: true, provider: opts.provider, requiredGroup: opts.group };
|
|
12441
|
+
if (opts.getter !== void 0) consentMode.getter = opts.getter;
|
|
12442
|
+
const result = await updateConsent(requireCredentials(), { consentMode, applyCanonicalSplit: true });
|
|
12443
|
+
if (!result.ok || !result.data) fail(this, result.error ?? "Failed to update consent settings", classifyStatus(result.status), result.errorDetails);
|
|
12444
|
+
warn(this, result.data.precompile);
|
|
12445
|
+
output(this, result.data, () => printConsent(result.data));
|
|
12446
|
+
});
|
|
12447
|
+
var off = new Command("off").description("Disable consent gating").action(async function action48() {
|
|
12448
|
+
const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
|
|
12449
|
+
if (!result.ok || !result.data) fail(this, result.error ?? "Failed to disable consent settings", classifyStatus(result.status), result.errorDetails);
|
|
12450
|
+
warn(this, result.data.precompile);
|
|
12451
|
+
output(this, result.data, () => printConsent(result.data));
|
|
12452
|
+
});
|
|
12453
|
+
var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
|
|
12454
|
+
|
|
12455
|
+
// src/commands/domains.ts
|
|
12456
|
+
init_source();
|
|
12457
|
+
function normalizeDomain(value) {
|
|
12458
|
+
return value.trim().replace(/^https?:\/\//i, "").replace(/[/?#].*$/, "").replace(/\.$/, "").toLowerCase();
|
|
12459
|
+
}
|
|
12460
|
+
function warn2(command, precompile) {
|
|
12461
|
+
if (!shouldUseJson(command) && precompile && precompile.status !== "ok") console.error(source_default.yellow(`Warning (${precompile.status}): ${precompile.summary}`));
|
|
12462
|
+
}
|
|
12463
|
+
function printDomains(data) {
|
|
12464
|
+
printTable(["TYPE", "DOMAIN"], [["primary", data.domain], ...data.additionalDomains.map((d) => ["additional", d])], [12, 64]);
|
|
12465
|
+
}
|
|
12466
|
+
async function load4(command) {
|
|
12467
|
+
const result = await getInstallDomains(requireCredentials());
|
|
12468
|
+
if (!result.ok || !result.data) fail(command, result.error ?? "Failed to load install domains", classifyStatus(result.status), result.errorDetails);
|
|
12469
|
+
return result.data;
|
|
12470
|
+
}
|
|
12471
|
+
async function patchWithRetry(command, build) {
|
|
12472
|
+
let result;
|
|
12473
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
12474
|
+
const current = await load4(command);
|
|
12475
|
+
result = await updateInstallDomains(requireCredentials(), { domains: build(current), ifRevision: current.revision });
|
|
12476
|
+
if (result.status !== 409 || attempt === 1) break;
|
|
12477
|
+
}
|
|
12478
|
+
if (!result?.ok || !result.data) fail(command, result?.error ?? "Failed to update install domains", classifyStatus(result?.status ?? 0), result?.errorDetails);
|
|
12479
|
+
warn2(command, result.data.precompile);
|
|
12480
|
+
return result.data;
|
|
12481
|
+
}
|
|
12482
|
+
var list5 = new Command("list").alias("ls").description("List install domains").action(async function action49() {
|
|
12483
|
+
const data = await load4(this);
|
|
12484
|
+
output(this, data, () => printDomains(data));
|
|
12485
|
+
});
|
|
12486
|
+
var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action50(domain) {
|
|
12487
|
+
const added = domain.map(normalizeDomain);
|
|
12488
|
+
const data = await patchWithRetry(this, (current) => Array.from(/* @__PURE__ */ new Set([...current.additionalDomains, ...added])));
|
|
12489
|
+
output(this, data, () => printDomains(data));
|
|
12490
|
+
});
|
|
12491
|
+
var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action51(domain) {
|
|
12492
|
+
const removeSet = new Set(domain.map(normalizeDomain));
|
|
12493
|
+
const data = await patchWithRetry(this, (current) => current.additionalDomains.filter((d) => !removeSet.has(normalizeDomain(d))));
|
|
12494
|
+
output(this, data, () => printDomains(data));
|
|
12495
|
+
});
|
|
12496
|
+
var set2 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action52(domain) {
|
|
12497
|
+
const replacement = domain.map(normalizeDomain);
|
|
12498
|
+
const data = await patchWithRetry(this, () => replacement);
|
|
12499
|
+
output(this, data, () => printDomains(data));
|
|
12500
|
+
});
|
|
12501
|
+
var domainsCommand = new Command("domains").description("Manage install domains").addCommand(list5).addCommand(add).addCommand(remove).addCommand(set2);
|
|
12502
|
+
|
|
12503
|
+
// src/commands/handover.ts
|
|
12504
|
+
init_source();
|
|
12505
|
+
function normalizeDomain2(value) {
|
|
12506
|
+
try {
|
|
12507
|
+
const candidate = value.trim().toLowerCase();
|
|
12508
|
+
const parsed = new URL(/^[a-z][a-z0-9+.-]*:\/\//.test(candidate) ? candidate : `http://${candidate}`);
|
|
12509
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, "");
|
|
12510
|
+
return hostname.includes(".") && /^[a-z0-9.-]+$/.test(hostname) ? hostname : "";
|
|
12511
|
+
} catch {
|
|
12512
|
+
return "";
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
var QA_FETCH_TIMEOUT_MS = 12e3;
|
|
12516
|
+
var NAMED_HTML_ENTITIES = {
|
|
12517
|
+
amp: "&",
|
|
12518
|
+
lt: "<",
|
|
12519
|
+
gt: ">",
|
|
12520
|
+
quot: '"',
|
|
12521
|
+
apos: "'",
|
|
12522
|
+
nbsp: "\xA0"
|
|
12523
|
+
};
|
|
12524
|
+
function decodeHtmlEntities(value) {
|
|
12525
|
+
return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
|
|
12526
|
+
if (entity.startsWith("#")) {
|
|
12527
|
+
const hex = entity[1] === "x" || entity[1] === "X";
|
|
12528
|
+
const code = Number.parseInt(hex ? entity.slice(2) : entity.slice(1), hex ? 16 : 10);
|
|
12529
|
+
return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
12530
|
+
}
|
|
12531
|
+
return NAMED_HTML_ENTITIES[entity.toLowerCase()] ?? match;
|
|
12532
|
+
});
|
|
12533
|
+
}
|
|
12534
|
+
function printSettings(settings2) {
|
|
12535
|
+
printTable(["DOMAINS", "PARAM", "TTL", "MINT TIMEOUT", "SIGNED"], [[settings2.domains.join(", ") || "off", settings2.handoverQueryParam, `${settings2.handoverTtlSeconds}s`, `${settings2.handoverMintTimeoutMs}ms`, settings2.signed ? "yes" : "no"]], [48, 20, 8, 14, 8]);
|
|
12536
|
+
}
|
|
12537
|
+
async function load5(command) {
|
|
12538
|
+
const result = await getExternalTracking(requireCredentials());
|
|
12539
|
+
if (!result.ok || !result.data) fail(command, result.error ?? "Failed to load handover settings", classifyStatus(result.status), result.errorDetails);
|
|
12540
|
+
return result.data;
|
|
12541
|
+
}
|
|
12542
|
+
async function patchWithRetry2(command, build) {
|
|
12543
|
+
let result;
|
|
12544
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
12545
|
+
const current = await load5(command);
|
|
12546
|
+
result = await updateExternalTracking(requireCredentials(), build(current));
|
|
12547
|
+
if (result.status !== 409 || attempt === 1) break;
|
|
12548
|
+
}
|
|
12549
|
+
if (!result || !result.ok || !result.data) fail(command, result?.error ?? "Failed to update handover settings", classifyStatus(result?.status ?? 0), result?.errorDetails);
|
|
12550
|
+
if (!shouldUseJson(command) && result.data.precompile?.status && result.data.precompile.status !== "ok") console.error(source_default.yellow(`Warning (${result.data.precompile.status}): ${result.data.precompile.summary}. Edge config may be stale until republish succeeds.`));
|
|
12551
|
+
return result.data;
|
|
12552
|
+
}
|
|
12553
|
+
var get3 = new Command("get").description("Show cross-domain handover settings").action(async function action53() {
|
|
12554
|
+
const data = await load5(this);
|
|
12555
|
+
output(this, data, () => printSettings(data.externalTracking));
|
|
12556
|
+
});
|
|
12557
|
+
var set3 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option("--signed", "Require signed handover tokens").option("--allow-unsigned", "Disable signing using the audited escape hatch").action(async function action54(opts) {
|
|
12558
|
+
if (opts.signed && opts.allowUnsigned) fail(this, "--signed and --allow-unsigned cannot be used together", EXIT_CODES.validation);
|
|
12559
|
+
const domains = opts.domain.map(normalizeDomain2);
|
|
12560
|
+
if (domains.some((domain) => !domain)) fail(this, "Every --domain value must be a valid hostname containing a dot", EXIT_CODES.validation);
|
|
12561
|
+
if (opts.param && !/^[a-z_][a-z0-9_]{0,31}$/.test(opts.param)) fail(this, "--param must match ^[a-z_][a-z0-9_]{0,31}$", EXIT_CODES.validation);
|
|
12562
|
+
const data = await patchWithRetry2(this, (current) => ({ externalTracking: { domains: Array.from(new Set(domains)), handoverQueryParam: opts.param ?? current.externalTracking.handoverQueryParam, signed: opts.allowUnsigned ? false : true, ...opts.allowUnsigned ? { allowUnsigned: true } : {} }, ifRevision: current.revision }));
|
|
12563
|
+
output(this, data, () => printSettings(data.externalTracking));
|
|
12564
|
+
});
|
|
12565
|
+
var off2 = new Command("off").description("Disable cross-domain handover").action(async function action55() {
|
|
12566
|
+
const data = await patchWithRetry2(this, (current) => ({ externalTracking: { domains: [], signed: true }, ifRevision: current.revision }));
|
|
12567
|
+
output(this, data, () => printSettings(data.externalTracking));
|
|
12568
|
+
});
|
|
12569
|
+
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 action56(opts) {
|
|
12570
|
+
const config = await load5(this);
|
|
12571
|
+
const target = normalizeDomain2(opts.toDomain);
|
|
12572
|
+
if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
|
|
12573
|
+
let response;
|
|
12574
|
+
let html;
|
|
12575
|
+
try {
|
|
12576
|
+
response = await fetch(opts.from, { signal: AbortSignal.timeout(QA_FETCH_TIMEOUT_MS), redirect: "follow" });
|
|
12577
|
+
if (!response.ok) fail(this, `Fetching ${opts.from} returned HTTP ${response.status}`, classifyStatus(response.status));
|
|
12578
|
+
html = await response.text();
|
|
12579
|
+
} catch (error) {
|
|
12580
|
+
if (error instanceof CliError) throw error;
|
|
12581
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
12582
|
+
fail(this, `Could not fetch ${opts.from}: ${reason}`, EXIT_CODES.general);
|
|
12583
|
+
}
|
|
12584
|
+
const param = config.externalTracking.handoverQueryParam;
|
|
12585
|
+
const baseUrl = response.url || opts.from;
|
|
12586
|
+
const hrefs = [...html.matchAll(/<(a|form)\b[^>]*(?:href|action)\s*=\s*["']([^"']+)["'][^>]*>/gi)].map((m) => decodeHtmlEntities(m[2]));
|
|
12587
|
+
const targetUrls = hrefs.map((raw) => {
|
|
12588
|
+
try {
|
|
12589
|
+
return new URL(raw, baseUrl);
|
|
12590
|
+
} catch {
|
|
12591
|
+
return null;
|
|
12592
|
+
}
|
|
12593
|
+
}).filter((url) => url !== null && url.hostname.toLowerCase().replace(/^www\./, "") === target.replace(/^www\./, ""));
|
|
12594
|
+
const carrying = targetUrls.filter((url) => url.searchParams.has(param));
|
|
12595
|
+
const warn3 = targetUrls.length === 0 ? "WARN: no static links/forms found for target domain; run the ab-harness browser check for JS-driven navigation." : void 0;
|
|
12596
|
+
const data = { ok: targetUrls.length > 0 && carrying.length === targetUrls.length, from: opts.from, toDomain: target, param, checked: targetUrls.length, carrying: carrying.length, warning: warn3 };
|
|
12597
|
+
const printVerdict = () => {
|
|
12598
|
+
console.log(data.ok ? source_default.green("PASS handover parameter is carried") : source_default.red("FAIL handover parameter is not carried by every target link/form"));
|
|
12599
|
+
if (warn3) console.error(source_default.yellow(warn3));
|
|
12600
|
+
};
|
|
12601
|
+
if (targetUrls.length > 0 && carrying.length !== targetUrls.length) {
|
|
12602
|
+
if (!shouldUseJson(this)) printVerdict();
|
|
12603
|
+
fail(this, `Handover parameter "${param}" is missing on ${targetUrls.length - carrying.length} of ${targetUrls.length} ${target} links/forms`, EXIT_CODES.validation, data);
|
|
12604
|
+
}
|
|
12605
|
+
output(this, data, printVerdict);
|
|
12606
|
+
});
|
|
12607
|
+
var handoverCommand = new Command("handover").description("Manage cross-domain identity handover").addCommand(get3).addCommand(set3).addCommand(off2).addCommand(qa2);
|
|
11370
12608
|
|
|
11371
12609
|
// src/commands/analytics.ts
|
|
11372
12610
|
function display(value) {
|
|
@@ -11401,7 +12639,7 @@ function analyticsSubcommand(name, description, defaultDays) {
|
|
|
11401
12639
|
command.option("--planning-days <n>", "Planning window in days", "180");
|
|
11402
12640
|
command.option("--limit <n>", "Maximum page rows", "50");
|
|
11403
12641
|
}
|
|
11404
|
-
return command.action(async function
|
|
12642
|
+
return command.action(async function action68(opts) {
|
|
11405
12643
|
const result = await getAnalytics(requireCredentials(), name, {
|
|
11406
12644
|
days: positiveInteger(this, opts.days, "--days"),
|
|
11407
12645
|
planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
|
|
@@ -11413,12 +12651,17 @@ function analyticsSubcommand(name, description, defaultDays) {
|
|
|
11413
12651
|
output(this, { data: result.data.data }, () => printAnalytics(result.data.data));
|
|
11414
12652
|
});
|
|
11415
12653
|
}
|
|
11416
|
-
var analyticsCommand = new Command("analytics").description("Read shop analytics").addCommand(analyticsSubcommand("stats", "Show dashboard analytics stats", 7)).addCommand(analyticsSubcommand("timeseries", "Show analytics timeseries", 30)).addCommand(analyticsSubcommand("traffic", "Show traffic sources", 30)).addCommand(analyticsSubcommand("pages", "Show page analytics", 30)).addCommand(analyticsSubcommand("geo", "Show geography and device analytics", 30)).addCommand(analyticsSubcommand("sessions", "Show session analytics", 30)).addCommand(analyticsSubcommand("web-vitals", "Show web vitals analytics", 7)).addCommand(analyticsSubcommand("realtime", "Show realtime analytics")).addCommand(analyticsSubcommand("entry-exit", "Show entry and exit page analytics", 30))
|
|
12654
|
+
var analyticsCommand = new Command("analytics").description("Read shop analytics").addCommand(analyticsSubcommand("stats", "Show dashboard analytics stats", 7)).addCommand(analyticsSubcommand("timeseries", "Show analytics timeseries", 30)).addCommand(analyticsSubcommand("traffic", "Show traffic sources", 30)).addCommand(analyticsSubcommand("pages", "Show page analytics", 30)).addCommand(analyticsSubcommand("geo", "Show geography and device analytics", 30)).addCommand(analyticsSubcommand("sessions", "Show session analytics", 30)).addCommand(analyticsSubcommand("web-vitals", "Show web vitals analytics", 7)).addCommand(analyticsSubcommand("realtime", "Show realtime analytics")).addCommand(analyticsSubcommand("entry-exit", "Show entry and exit page analytics", 30)).addHelpText("after", `
|
|
12655
|
+
Examples:
|
|
12656
|
+
apex analytics stats --days 7
|
|
12657
|
+
apex analytics pages --planning-days 180 --limit 25
|
|
12658
|
+
apex analytics realtime --json
|
|
12659
|
+
`);
|
|
11417
12660
|
|
|
11418
12661
|
// src/commands/flags.ts
|
|
11419
12662
|
init_source();
|
|
11420
|
-
var
|
|
11421
|
-
var
|
|
12663
|
+
var import_node_fs14 = __toESM(require("node:fs"));
|
|
12664
|
+
var import_node_path12 = __toESM(require("node:path"));
|
|
11422
12665
|
|
|
11423
12666
|
// src/edge-status.ts
|
|
11424
12667
|
function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 420) {
|
|
@@ -11460,7 +12703,7 @@ function body(command, value, opts) {
|
|
|
11460
12703
|
let parsed = {};
|
|
11461
12704
|
if (value) {
|
|
11462
12705
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11463
|
-
const text = value.startsWith("@") ||
|
|
12706
|
+
const text = value.startsWith("@") || import_node_fs14.default.existsSync(source) ? import_node_fs14.default.readFileSync(import_node_path12.default.resolve(process.cwd(), source), "utf8") : value;
|
|
11464
12707
|
try {
|
|
11465
12708
|
parsed = JSON.parse(text);
|
|
11466
12709
|
} catch (e) {
|
|
@@ -11490,7 +12733,7 @@ function detail(x) {
|
|
|
11490
12733
|
console.log(` State: ${x.status}`);
|
|
11491
12734
|
console.log();
|
|
11492
12735
|
}
|
|
11493
|
-
var
|
|
12736
|
+
var list6 = new Command("list").alias("ls").description("List flags").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
|
|
11494
12737
|
const r = await listManagedDeliveries(
|
|
11495
12738
|
requireCredentials(),
|
|
11496
12739
|
kind,
|
|
@@ -11517,7 +12760,7 @@ var list4 = new Command("list").alias("ls").description("List flags").option("-n
|
|
|
11517
12760
|
)
|
|
11518
12761
|
);
|
|
11519
12762
|
});
|
|
11520
|
-
var
|
|
12763
|
+
var get4 = new Command("get").description("Get a flag").argument("<id>").action(async function(id) {
|
|
11521
12764
|
const r = await getManagedDelivery(requireCredentials(), kind, id);
|
|
11522
12765
|
if (!r.ok || !r.data)
|
|
11523
12766
|
fail(
|
|
@@ -11588,29 +12831,35 @@ function write(name) {
|
|
|
11588
12831
|
});
|
|
11589
12832
|
return c;
|
|
11590
12833
|
}
|
|
11591
|
-
function lifecycle(
|
|
11592
|
-
return new Command(
|
|
12834
|
+
function lifecycle(action68) {
|
|
12835
|
+
return new Command(action68).description(`${action68[0].toUpperCase()}${action68.slice(1)} a flag`).argument("<id>").action(async function(id) {
|
|
11593
12836
|
const r = await transitionManagedDelivery(
|
|
11594
12837
|
requireCredentials(),
|
|
11595
12838
|
kind,
|
|
11596
12839
|
id,
|
|
11597
|
-
|
|
12840
|
+
action68
|
|
11598
12841
|
);
|
|
11599
12842
|
if (!r.ok || !r.data)
|
|
11600
12843
|
fail(
|
|
11601
12844
|
this,
|
|
11602
|
-
r.error ?? `Failed to ${
|
|
12845
|
+
r.error ?? `Failed to ${action68}`,
|
|
11603
12846
|
classifyStatus(r.status),
|
|
11604
12847
|
r.errorDetails
|
|
11605
12848
|
);
|
|
11606
12849
|
output(this, { flag: r.data }, () => detail(r.data));
|
|
11607
12850
|
});
|
|
11608
12851
|
}
|
|
11609
|
-
var flagsCommand = new Command("flags").description("Manage flags").addCommand(
|
|
12852
|
+
var flagsCommand = new Command("flags").description("Manage flags").addCommand(list6).addCommand(get4).addCommand(status).addCommand(write("create")).addCommand(write("update")).addCommand(lifecycle("activate")).addCommand(lifecycle("pause")).addCommand(lifecycle("archive")).addHelpText("after", `
|
|
12853
|
+
Examples:
|
|
12854
|
+
apex flags list --limit 20
|
|
12855
|
+
apex flags get checkout-redesign
|
|
12856
|
+
apex flags create --key checkout-redesign --name "Checkout redesign"
|
|
12857
|
+
apex flags activate <id>
|
|
12858
|
+
`);
|
|
11610
12859
|
|
|
11611
12860
|
// src/commands/backlog.ts
|
|
11612
|
-
var
|
|
11613
|
-
var
|
|
12861
|
+
var import_node_fs15 = __toESM(require("node:fs"));
|
|
12862
|
+
var import_node_path13 = __toESM(require("node:path"));
|
|
11614
12863
|
init_source();
|
|
11615
12864
|
var kind2 = "backlog";
|
|
11616
12865
|
var singular = "backlog item";
|
|
@@ -11618,7 +12867,7 @@ function parse(command, value) {
|
|
|
11618
12867
|
if (!value) return {};
|
|
11619
12868
|
try {
|
|
11620
12869
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11621
|
-
const text = value.startsWith("@") ||
|
|
12870
|
+
const text = value.startsWith("@") || import_node_fs15.default.existsSync(source) ? import_node_fs15.default.readFileSync(import_node_path13.default.resolve(process.cwd(), source), "utf8") : value;
|
|
11622
12871
|
const body5 = JSON.parse(text);
|
|
11623
12872
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
|
|
11624
12873
|
return body5;
|
|
@@ -11633,28 +12882,28 @@ function detail2(x) {
|
|
|
11633
12882
|
if (x.status) console.log(` State: ${x.status}`);
|
|
11634
12883
|
console.log();
|
|
11635
12884
|
}
|
|
11636
|
-
var
|
|
12885
|
+
var list7 = new Command("list").alias("ls").description("List backlog").option("-n, --limit <n>", "Max results", "50").option("--cursor <cursor>", "Continue from cursor").option("--include-archived", "Include archived backlog items").action(async function(o) {
|
|
11637
12886
|
const r = await listInsights(requireCredentials(), kind2, Number(o.limit), o.cursor, o.includeArchived);
|
|
11638
12887
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
|
|
11639
12888
|
output(this, { backlog: r.data.data, pagination: r.data.pagination }, () => printTable(["ID", "NAME", "STATE"], r.data.data.map((x) => [x.id, String(x.title ?? x.label ?? x.key ?? "-"), x.status ?? "-"]), [36, 40, 12]));
|
|
11640
12889
|
});
|
|
11641
|
-
var
|
|
12890
|
+
var get5 = new Command("get").description("Get a backlog item").argument("<id>", "backlog item ID").action(async function(id) {
|
|
11642
12891
|
const r = await getInsight(requireCredentials(), kind2, id);
|
|
11643
12892
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
11644
12893
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
11645
12894
|
});
|
|
11646
|
-
function write2(
|
|
11647
|
-
const c = new Command(
|
|
11648
|
-
if (
|
|
12895
|
+
function write2(action68) {
|
|
12896
|
+
const c = new Command(action68).description(`${action68 === "create" ? "Create" : "Update"} a ${singular}`);
|
|
12897
|
+
if (action68 === "update") c.argument("<id>", `${singular} ID`);
|
|
11649
12898
|
c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
|
|
11650
12899
|
c.action(async function(...args) {
|
|
11651
12900
|
const o = this.opts();
|
|
11652
|
-
const id =
|
|
11653
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
12901
|
+
const id = action68 === "update" ? String(args[0]) : void 0;
|
|
12902
|
+
const raw = o.file ? `@${o.file}` : String(args[action68 === "update" ? 1 : 0] ?? "") || void 0;
|
|
11654
12903
|
const body5 = parse(this, raw);
|
|
11655
12904
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
11656
|
-
const r =
|
|
11657
|
-
if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${
|
|
12905
|
+
const r = action68 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
|
|
12906
|
+
if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action68}`, classifyStatus(r.status), r.errorDetails);
|
|
11658
12907
|
output(this, { backlog_item: r.data }, () => detail2(r.data));
|
|
11659
12908
|
});
|
|
11660
12909
|
return c;
|
|
@@ -11670,7 +12919,7 @@ var restore = new Command("restore").description("Restore an archived backlog it
|
|
|
11670
12919
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to restore", classifyStatus(r.status), r.errorDetails);
|
|
11671
12920
|
output(this, { restored: true, id }, () => console.log(`Restored ${id}`));
|
|
11672
12921
|
});
|
|
11673
|
-
var
|
|
12922
|
+
var remove2 = new Command("delete").description("Delete a backlog item").argument("<id>", "backlog item ID").option("-f, --force", "Skip confirmation").action(async function(id, opts) {
|
|
11674
12923
|
if (!opts.force) {
|
|
11675
12924
|
if (!process.stdin.isTTY || !process.stdout.isTTY) confirmationRequired(this, "Deleting a backlog item requires confirmation in non-TTY mode.", `apex backlog delete ${id} --force`);
|
|
11676
12925
|
const readline2 = await import("node:readline");
|
|
@@ -11686,11 +12935,11 @@ var remove = new Command("delete").description("Delete a backlog item").argument
|
|
|
11686
12935
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
|
|
11687
12936
|
output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
|
|
11688
12937
|
});
|
|
11689
|
-
var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(
|
|
12938
|
+
var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get5).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
|
|
11690
12939
|
|
|
11691
12940
|
// src/commands/signals.ts
|
|
11692
|
-
var
|
|
11693
|
-
var
|
|
12941
|
+
var import_node_fs16 = __toESM(require("node:fs"));
|
|
12942
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
11694
12943
|
init_source();
|
|
11695
12944
|
var kind3 = "signals";
|
|
11696
12945
|
var singular2 = "signal";
|
|
@@ -11698,7 +12947,7 @@ function parse2(command, value) {
|
|
|
11698
12947
|
if (!value) return {};
|
|
11699
12948
|
try {
|
|
11700
12949
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11701
|
-
const text = value.startsWith("@") ||
|
|
12950
|
+
const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
|
|
11702
12951
|
const body5 = JSON.parse(text);
|
|
11703
12952
|
if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
|
|
11704
12953
|
return body5;
|
|
@@ -11713,49 +12962,49 @@ function detail3(x) {
|
|
|
11713
12962
|
if (x.status) console.log(` State: ${x.status}`);
|
|
11714
12963
|
console.log();
|
|
11715
12964
|
}
|
|
11716
|
-
var
|
|
12965
|
+
var list8 = new Command("list").alias("ls").description("List signals").option("-n, --limit <n>", "Max results", "50").option("--cursor <cursor>", "Continue from cursor").option("--include-archived", "Include archived signals").action(async function(o) {
|
|
11717
12966
|
const r = await listInsights(requireCredentials(), kind3, Number(o.limit), o.cursor, o.includeArchived);
|
|
11718
12967
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
|
|
11719
12968
|
output(this, { signals: r.data.data, pagination: r.data.pagination }, () => printTable(["ID", "NAME", "STATE"], r.data.data.map((x) => [x.id, String(x.title ?? x.label ?? x.key ?? "-"), x.status ?? "-"]), [36, 40, 12]));
|
|
11720
12969
|
});
|
|
11721
|
-
var
|
|
12970
|
+
var get6 = new Command("get").description("Get a signal").argument("<id>", "signal ID").action(async function(id) {
|
|
11722
12971
|
const r = await getInsight(requireCredentials(), kind3, id);
|
|
11723
12972
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
|
|
11724
12973
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
11725
12974
|
});
|
|
11726
|
-
function write3(
|
|
11727
|
-
const c = new Command(
|
|
11728
|
-
if (
|
|
12975
|
+
function write3(action68) {
|
|
12976
|
+
const c = new Command(action68).description(`${action68 === "create" ? "Create" : "Update"} a ${singular2}`);
|
|
12977
|
+
if (action68 === "update") c.argument("<id>", `${singular2} ID`);
|
|
11729
12978
|
c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
|
|
11730
12979
|
c.action(async function(...args) {
|
|
11731
12980
|
const o = this.opts();
|
|
11732
|
-
const id =
|
|
11733
|
-
const raw = o.file ? `@${o.file}` : String(args[
|
|
12981
|
+
const id = action68 === "update" ? String(args[0]) : void 0;
|
|
12982
|
+
const raw = o.file ? `@${o.file}` : String(args[action68 === "update" ? 1 : 0] ?? "") || void 0;
|
|
11734
12983
|
const body5 = parse2(this, raw);
|
|
11735
12984
|
if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
|
|
11736
|
-
const r =
|
|
11737
|
-
if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${
|
|
12985
|
+
const r = action68 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
|
|
12986
|
+
if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action68}`, classifyStatus(r.status), r.errorDetails);
|
|
11738
12987
|
output(this, { signal: r.data }, () => detail3(r.data));
|
|
11739
12988
|
});
|
|
11740
12989
|
return c;
|
|
11741
12990
|
}
|
|
11742
|
-
var
|
|
12991
|
+
var remove3 = new Command("delete").alias("archive").description("Delete or archive a signal").argument("<id>", "signal ID").action(async function(id) {
|
|
11743
12992
|
const r = await deleteInsight(requireCredentials(), kind3, id);
|
|
11744
12993
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
|
|
11745
12994
|
output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
|
|
11746
12995
|
});
|
|
11747
|
-
var signalsCommand = new Command("signals").description("Manage signals").addCommand(
|
|
12996
|
+
var signalsCommand = new Command("signals").description("Manage signals").addCommand(list8).addCommand(get6).addCommand(write3("create")).addCommand(write3("update")).addCommand(remove3);
|
|
11748
12997
|
|
|
11749
12998
|
// src/commands/personalizations.ts
|
|
11750
12999
|
init_source();
|
|
11751
|
-
var
|
|
11752
|
-
var
|
|
13000
|
+
var import_node_fs17 = __toESM(require("node:fs"));
|
|
13001
|
+
var import_node_path15 = __toESM(require("node:path"));
|
|
11753
13002
|
var kind4 = "personalizations";
|
|
11754
13003
|
function body2(command, value, opts) {
|
|
11755
13004
|
let parsed = {};
|
|
11756
13005
|
if (value) {
|
|
11757
13006
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11758
|
-
const text = value.startsWith("@") ||
|
|
13007
|
+
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;
|
|
11759
13008
|
try {
|
|
11760
13009
|
parsed = JSON.parse(text);
|
|
11761
13010
|
} catch (e) {
|
|
@@ -11785,7 +13034,7 @@ function detail4(x) {
|
|
|
11785
13034
|
console.log(` State: ${x.status}`);
|
|
11786
13035
|
console.log();
|
|
11787
13036
|
}
|
|
11788
|
-
var
|
|
13037
|
+
var list9 = new Command("list").alias("ls").description("List personalizations").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
|
|
11789
13038
|
const r = await listManagedDeliveries(
|
|
11790
13039
|
requireCredentials(),
|
|
11791
13040
|
kind4,
|
|
@@ -11812,7 +13061,7 @@ var list7 = new Command("list").alias("ls").description("List personalizations")
|
|
|
11812
13061
|
)
|
|
11813
13062
|
);
|
|
11814
13063
|
});
|
|
11815
|
-
var
|
|
13064
|
+
var get7 = new Command("get").description("Get a personalization").argument("<id>").action(async function(id) {
|
|
11816
13065
|
const r = await getManagedDelivery(requireCredentials(), kind4, id);
|
|
11817
13066
|
if (!r.ok || !r.data)
|
|
11818
13067
|
fail(
|
|
@@ -11852,37 +13101,37 @@ function write4(name) {
|
|
|
11852
13101
|
});
|
|
11853
13102
|
return c;
|
|
11854
13103
|
}
|
|
11855
|
-
function lifecycle2(
|
|
11856
|
-
return new Command(
|
|
11857
|
-
`${
|
|
13104
|
+
function lifecycle2(action68) {
|
|
13105
|
+
return new Command(action68).description(
|
|
13106
|
+
`${action68[0].toUpperCase()}${action68.slice(1)} a personalization`
|
|
11858
13107
|
).argument("<id>").action(async function(id) {
|
|
11859
13108
|
const r = await transitionManagedDelivery(
|
|
11860
13109
|
requireCredentials(),
|
|
11861
13110
|
kind4,
|
|
11862
13111
|
id,
|
|
11863
|
-
|
|
13112
|
+
action68
|
|
11864
13113
|
);
|
|
11865
13114
|
if (!r.ok || !r.data)
|
|
11866
13115
|
fail(
|
|
11867
13116
|
this,
|
|
11868
|
-
r.error ?? `Failed to ${
|
|
13117
|
+
r.error ?? `Failed to ${action68}`,
|
|
11869
13118
|
classifyStatus(r.status),
|
|
11870
13119
|
r.errorDetails
|
|
11871
13120
|
);
|
|
11872
13121
|
output(this, { personalization: r.data }, () => detail4(r.data));
|
|
11873
13122
|
});
|
|
11874
13123
|
}
|
|
11875
|
-
var personalizationsCommand = new Command("personalizations").description("Manage personalizations").addCommand(
|
|
13124
|
+
var personalizationsCommand = new Command("personalizations").description("Manage personalizations").addCommand(list9).addCommand(get7).addCommand(write4("create")).addCommand(write4("update")).addCommand(lifecycle2("activate")).addCommand(lifecycle2("pause")).addCommand(lifecycle2("archive"));
|
|
11876
13125
|
|
|
11877
13126
|
// src/commands/targeting-resources.ts
|
|
11878
13127
|
init_source();
|
|
11879
|
-
var
|
|
11880
|
-
var
|
|
13128
|
+
var import_node_fs18 = __toESM(require("node:fs"));
|
|
13129
|
+
var import_node_path16 = __toESM(require("node:path"));
|
|
11881
13130
|
function body3(command, value) {
|
|
11882
13131
|
if (!value) return {};
|
|
11883
13132
|
const source = value.startsWith("@") ? value.slice(1) : value;
|
|
11884
13133
|
try {
|
|
11885
|
-
const text = value.startsWith("@") ||
|
|
13134
|
+
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;
|
|
11886
13135
|
return JSON.parse(text);
|
|
11887
13136
|
} catch (error) {
|
|
11888
13137
|
fail(
|
|
@@ -11961,17 +13210,17 @@ function targetingResourceCommand(kind5) {
|
|
|
11961
13210
|
);
|
|
11962
13211
|
})
|
|
11963
13212
|
);
|
|
11964
|
-
for (const
|
|
11965
|
-
const command = new Command(
|
|
11966
|
-
`${
|
|
13213
|
+
for (const action68 of ["create", "update"]) {
|
|
13214
|
+
const command = new Command(action68).description(
|
|
13215
|
+
`${action68 === "create" ? "Create" : "Update"} a ${singular3}`
|
|
11967
13216
|
);
|
|
11968
|
-
if (
|
|
13217
|
+
if (action68 === "update") command.argument("<id>");
|
|
11969
13218
|
command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
|
|
11970
13219
|
const opts = this.opts();
|
|
11971
|
-
const id =
|
|
11972
|
-
const value = opts.file ? `@${opts.file}` : String(args[
|
|
13220
|
+
const id = action68 === "update" ? String(args[0]) : void 0;
|
|
13221
|
+
const value = opts.file ? `@${opts.file}` : String(args[action68 === "update" ? 1 : 0] ?? "") || void 0;
|
|
11973
13222
|
const requestBody = body3(this, value);
|
|
11974
|
-
if (
|
|
13223
|
+
if (action68 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
|
|
11975
13224
|
fail(
|
|
11976
13225
|
this,
|
|
11977
13226
|
`Updating a ${singular3} requires at least one field.`,
|
|
@@ -11979,7 +13228,7 @@ function targetingResourceCommand(kind5) {
|
|
|
11979
13228
|
);
|
|
11980
13229
|
const response = await requestTargetingResource(
|
|
11981
13230
|
requireCredentials(),
|
|
11982
|
-
|
|
13231
|
+
action68 === "create" ? "POST" : "PATCH",
|
|
11983
13232
|
kind5,
|
|
11984
13233
|
id,
|
|
11985
13234
|
requestBody
|
|
@@ -11987,7 +13236,7 @@ function targetingResourceCommand(kind5) {
|
|
|
11987
13236
|
if (!response.ok || !response.data)
|
|
11988
13237
|
fail(
|
|
11989
13238
|
this,
|
|
11990
|
-
response.error ?? `Failed to ${
|
|
13239
|
+
response.error ?? `Failed to ${action68}`,
|
|
11991
13240
|
classifyStatus(response.status),
|
|
11992
13241
|
response.errorDetails
|
|
11993
13242
|
);
|
|
@@ -11996,7 +13245,7 @@ function targetingResourceCommand(kind5) {
|
|
|
11996
13245
|
{
|
|
11997
13246
|
[kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
|
|
11998
13247
|
},
|
|
11999
|
-
() => console.log(source_default.green(`${
|
|
13248
|
+
() => console.log(source_default.green(`${action68}d ${singular3}`))
|
|
12000
13249
|
);
|
|
12001
13250
|
});
|
|
12002
13251
|
root.addCommand(command);
|
|
@@ -12083,7 +13332,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
|
|
|
12083
13332
|
// src/commands/brand.ts
|
|
12084
13333
|
init_source();
|
|
12085
13334
|
var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
|
|
12086
|
-
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function
|
|
13335
|
+
var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action57() {
|
|
12087
13336
|
const creds2 = requireCredentials();
|
|
12088
13337
|
const res = await getBrandContext(creds2);
|
|
12089
13338
|
if (res.status === 404) {
|
|
@@ -12116,7 +13365,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
|
|
|
12116
13365
|
|
|
12117
13366
|
// src/commands/targeting.ts
|
|
12118
13367
|
init_source();
|
|
12119
|
-
var
|
|
13368
|
+
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 action58(url, opts) {
|
|
12120
13369
|
const creds2 = requireCredentials();
|
|
12121
13370
|
const res = await testTargeting(creds2, url, { status: opts.status });
|
|
12122
13371
|
if (!res.ok || !res.data) {
|
|
@@ -12152,17 +13401,17 @@ var test = new Command("test").description("Evaluate experiment URL targeting fo
|
|
|
12152
13401
|
}
|
|
12153
13402
|
});
|
|
12154
13403
|
});
|
|
12155
|
-
var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(
|
|
13404
|
+
var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
|
|
12156
13405
|
|
|
12157
13406
|
// src/commands/sos.ts
|
|
12158
13407
|
init_source();
|
|
12159
|
-
var
|
|
12160
|
-
var
|
|
13408
|
+
var import_node_fs19 = __toESM(require("node:fs"));
|
|
13409
|
+
var import_node_path17 = __toESM(require("node:path"));
|
|
12161
13410
|
var SOS_TYPES = /* @__PURE__ */ new Set(["full_handover", "design", "development", "qa", "config", "bug"]);
|
|
12162
13411
|
function readText(value) {
|
|
12163
13412
|
if (!value) return void 0;
|
|
12164
13413
|
if (!value.startsWith("@")) return value;
|
|
12165
|
-
return
|
|
13414
|
+
return import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), value.slice(1)), "utf8");
|
|
12166
13415
|
}
|
|
12167
13416
|
function printTicket(ticket, reused) {
|
|
12168
13417
|
console.log();
|
|
@@ -12173,7 +13422,7 @@ function printTicket(ticket, reused) {
|
|
|
12173
13422
|
console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
|
|
12174
13423
|
console.log();
|
|
12175
13424
|
}
|
|
12176
|
-
var
|
|
13425
|
+
var create4 = new Command("create").description("Create an Apex SOS ticket").option("--type <type>", "full_handover|design|development|qa|config|bug", "qa").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 action59(descriptionArg, opts) {
|
|
12177
13426
|
if (!SOS_TYPES.has(opts.type)) {
|
|
12178
13427
|
fail(this, "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.", 3);
|
|
12179
13428
|
}
|
|
@@ -12191,7 +13440,7 @@ var create3 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
12191
13440
|
}
|
|
12192
13441
|
output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
|
|
12193
13442
|
});
|
|
12194
|
-
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function
|
|
13443
|
+
var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action60() {
|
|
12195
13444
|
const creds2 = requireCredentials();
|
|
12196
13445
|
const res = await listMySos(creds2);
|
|
12197
13446
|
if (!res.ok || !res.data) {
|
|
@@ -12215,18 +13464,18 @@ var mine = new Command("mine").description("List SOS tickets created by this API
|
|
|
12215
13464
|
);
|
|
12216
13465
|
});
|
|
12217
13466
|
});
|
|
12218
|
-
var sosCommand = new Command("sos").description("Create and inspect Apex SOS tickets").addCommand(
|
|
13467
|
+
var sosCommand = new Command("sos").description("Create and inspect Apex SOS tickets").addCommand(create4).addCommand(mine);
|
|
12219
13468
|
|
|
12220
13469
|
// src/commands/audit-log.ts
|
|
12221
|
-
var
|
|
12222
|
-
const res = await listAuditLogs(requireCredentials(), { entityType: opts.entityType, entityId: opts.entityId, limit: Number.parseInt(opts.limit, 10), offset: Number.parseInt(opts.offset, 10) });
|
|
13470
|
+
var list10 = new Command("list").alias("ls").option("--entity-type <type>").option("--entity-id <id>").option("--shop <id>", "Shop ID for user-key fallback").option("-n, --limit <n>", "Max results", "50").option("--offset <n>", "Pagination offset", "0").action(async function(opts) {
|
|
13471
|
+
const res = await listAuditLogs(requireCredentials(), { entityType: opts.entityType, entityId: opts.entityId, shopId: opts.shop, limit: Number.parseInt(opts.limit, 10), offset: Number.parseInt(opts.offset, 10) });
|
|
12223
13472
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list audit logs", classifyStatus(res.status));
|
|
12224
13473
|
output(this, res.data, () => printTable(["TIME", "ACTION", "ENTITY", "ENTITY_ID", "ACTOR"], res.data.data.map((x) => [x.created_at, x.action, x.entity_type, x.entity_id, x.user_id])));
|
|
12225
13474
|
});
|
|
12226
|
-
var auditLogCommand = new Command("audit-log").description("Inspect audit logs").addCommand(
|
|
13475
|
+
var auditLogCommand = new Command("audit-log").description("Inspect audit logs").addCommand(list10);
|
|
12227
13476
|
|
|
12228
13477
|
// src/commands/inbox.ts
|
|
12229
|
-
var
|
|
13478
|
+
var list11 = new Command("list").option("--experiment <id>").action(async function(opts) {
|
|
12230
13479
|
const r = await listInbox(requireCredentials(), opts.experiment);
|
|
12231
13480
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list inbox", classifyStatus(r.status));
|
|
12232
13481
|
output(this, r.data, () => printTable(["TIME", "ROLE", "MESSAGE"], r.data.messages.map((m) => [m.created_at, m.role, m.body])));
|
|
@@ -12241,16 +13490,16 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
|
|
|
12241
13490
|
if (!r.ok || !r.data) fail(this, r.error ?? "Failed to mark inbox read", classifyStatus(r.status));
|
|
12242
13491
|
output(this, r.data, () => console.log(`Marked ${r.data.channelKeys.length} channel(s) read`));
|
|
12243
13492
|
});
|
|
12244
|
-
var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(
|
|
13493
|
+
var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send).addCommand(markRead);
|
|
12245
13494
|
|
|
12246
13495
|
// src/commands/shops.ts
|
|
12247
|
-
var
|
|
12248
|
-
var
|
|
13496
|
+
var import_node_fs20 = __toESM(require("node:fs"));
|
|
13497
|
+
var import_node_path18 = __toESM(require("node:path"));
|
|
12249
13498
|
function body4(command, raw) {
|
|
12250
13499
|
if (!raw) return {};
|
|
12251
13500
|
try {
|
|
12252
13501
|
const source = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
12253
|
-
const text = raw.startsWith("@") ||
|
|
13502
|
+
const text = raw.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(source), "utf8") : raw;
|
|
12254
13503
|
const value = JSON.parse(text);
|
|
12255
13504
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12256
13505
|
throw new Error("body must be an object");
|
|
@@ -12271,9 +13520,10 @@ function done(command, r, label) {
|
|
|
12271
13520
|
classifyStatus(r.status),
|
|
12272
13521
|
r.errorDetails
|
|
12273
13522
|
);
|
|
12274
|
-
output(command, r.data, () =>
|
|
13523
|
+
output(command, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13524
|
+
`));
|
|
12275
13525
|
}
|
|
12276
|
-
var
|
|
13526
|
+
var list12 = new Command("list").alias("ls").description("List organization shops").action(async function() {
|
|
12277
13527
|
const r = await listShops(requireUserCredentials());
|
|
12278
13528
|
if (!r.ok || !r.data)
|
|
12279
13529
|
fail(
|
|
@@ -12297,10 +13547,10 @@ var list10 = new Command("list").alias("ls").description("List organization shop
|
|
|
12297
13547
|
)
|
|
12298
13548
|
);
|
|
12299
13549
|
});
|
|
12300
|
-
var
|
|
13550
|
+
var get8 = new Command("get").description("Get a shop").argument("<id>").action(async function(id) {
|
|
12301
13551
|
done(this, await getShop(requireUserCredentials(), id), "get shop");
|
|
12302
13552
|
});
|
|
12303
|
-
var
|
|
13553
|
+
var create5 = new Command("create").description("Create a shop").argument("[json]", "Inline JSON, @file, or file path").option("--name <name>", "Shop name").option("--domain <domain>", "Public shop domain").option("--origin <origin>", "Backend origin URL").option("--currency <code>", "Three-letter currency code").action(async function(raw, o) {
|
|
12304
13554
|
const flags = {
|
|
12305
13555
|
...o.name ? { name: o.name } : {},
|
|
12306
13556
|
...o.domain ? { domain: o.domain } : {},
|
|
@@ -12313,7 +13563,7 @@ var create4 = new Command("create").description("Create a shop").argument("[json
|
|
|
12313
13563
|
const r = await createShop(requireUserCredentials(), payload);
|
|
12314
13564
|
done(this, r, "create shop");
|
|
12315
13565
|
});
|
|
12316
|
-
var
|
|
13566
|
+
var update3 = new Command("update").description("Update a shop").argument("<id>").argument("[json]", "Inline JSON, @file, or file path").option("--holdout-pct <percent>", "Global holdout percent (0-10)").option("--holdout-enabled", "Enable global holdout v1").option("--holdout-disabled", "Disable global holdout v1").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
|
|
12317
13567
|
const b = raw === void 0 ? {} : body4(this, raw);
|
|
12318
13568
|
if (o.holdoutEnabled && o.holdoutDisabled)
|
|
12319
13569
|
fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
|
|
@@ -12352,10 +13602,10 @@ function settings(name, description, toggle) {
|
|
|
12352
13602
|
async function(id, raw) {
|
|
12353
13603
|
const opts = this.opts();
|
|
12354
13604
|
const on = toggle ? Boolean(opts[toggle.on]) : false;
|
|
12355
|
-
const
|
|
12356
|
-
if (on &&
|
|
13605
|
+
const off3 = toggle ? Boolean(opts[toggle.off]) : false;
|
|
13606
|
+
if (on && off3)
|
|
12357
13607
|
fail(this, `Use only one of --${toggle.on} or --${toggle.off}`, EXIT_CODES.validation);
|
|
12358
|
-
const flagBody = on ||
|
|
13608
|
+
const flagBody = on || off3 ? { enabled: on } : void 0;
|
|
12359
13609
|
const patchBody = raw !== void 0 || flagBody !== void 0 ? { ...raw !== void 0 ? body4(this, raw) : {}, ...flagBody ?? {} } : void 0;
|
|
12360
13610
|
const r = patchBody !== void 0 ? await patchShopArea(requireUserCredentials(), id, name, patchBody) : await getShopArea(requireUserCredentials(), id, name);
|
|
12361
13611
|
if (!r.ok || !r.data)
|
|
@@ -12365,7 +13615,8 @@ function settings(name, description, toggle) {
|
|
|
12365
13615
|
classifyStatus(r.status),
|
|
12366
13616
|
r.errorDetails
|
|
12367
13617
|
);
|
|
12368
|
-
output(this, r.data, () =>
|
|
13618
|
+
output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13619
|
+
`));
|
|
12369
13620
|
}
|
|
12370
13621
|
);
|
|
12371
13622
|
}
|
|
@@ -12406,7 +13657,8 @@ var reviewFact = new Command("review-fact").argument("<id>").argument("<fact-id>
|
|
|
12406
13657
|
classifyStatus(r.status),
|
|
12407
13658
|
r.errorDetails
|
|
12408
13659
|
);
|
|
12409
|
-
output(this, r.data, () =>
|
|
13660
|
+
output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13661
|
+
`));
|
|
12410
13662
|
});
|
|
12411
13663
|
var reviewFinding = new Command("review-finding").argument("<id>").argument("<json>").action(async function(id, raw) {
|
|
12412
13664
|
const r = await postShopArea(
|
|
@@ -12422,7 +13674,8 @@ var reviewFinding = new Command("review-finding").argument("<id>").argument("<js
|
|
|
12422
13674
|
classifyStatus(r.status),
|
|
12423
13675
|
r.errorDetails
|
|
12424
13676
|
);
|
|
12425
|
-
output(this, r.data, () =>
|
|
13677
|
+
output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13678
|
+
`));
|
|
12426
13679
|
});
|
|
12427
13680
|
var startLearningRun = new Command("start-learning-run").argument("<id>").argument("[json]").action(async function(id, raw) {
|
|
12428
13681
|
const r = await postShopArea(
|
|
@@ -12438,7 +13691,8 @@ var startLearningRun = new Command("start-learning-run").argument("<id>").argume
|
|
|
12438
13691
|
classifyStatus(r.status),
|
|
12439
13692
|
r.errorDetails
|
|
12440
13693
|
);
|
|
12441
|
-
output(this, r.data, () =>
|
|
13694
|
+
output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13695
|
+
`));
|
|
12442
13696
|
});
|
|
12443
13697
|
profile.addCommand(reviewFact).addCommand(reviewFinding).addCommand(startLearningRun);
|
|
12444
13698
|
var shopware = new Command("shopware").description("Manage Shopware bindings");
|
|
@@ -12455,7 +13709,7 @@ shopware.addCommand(
|
|
|
12455
13709
|
output(this, result.data, () => console.log(`Pre-bound ${options.shopUrl} to ${id}`));
|
|
12456
13710
|
})
|
|
12457
13711
|
);
|
|
12458
|
-
var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(
|
|
13712
|
+
var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get8).addCommand(create5).addCommand(update3).addCommand(settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })).addCommand(settings("testing-defaults", "Read or update testing defaults")).addCommand(settings("guardian", "Read or update guardian defaults", { on: "on", off: "off" })).addCommand(profile).addCommand(shopware);
|
|
12459
13713
|
|
|
12460
13714
|
// src/commands/schema.ts
|
|
12461
13715
|
function optionSchema(option) {
|
|
@@ -12486,25 +13740,26 @@ function commandSchema(command) {
|
|
|
12486
13740
|
};
|
|
12487
13741
|
}
|
|
12488
13742
|
function createSchemaCommand(program3) {
|
|
12489
|
-
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function
|
|
13743
|
+
return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action68() {
|
|
12490
13744
|
const schema = commandSchema(program3);
|
|
12491
13745
|
output(this, schema, () => {
|
|
12492
|
-
|
|
13746
|
+
process.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
13747
|
+
`);
|
|
12493
13748
|
});
|
|
12494
13749
|
});
|
|
12495
13750
|
}
|
|
12496
13751
|
|
|
12497
13752
|
// src/commands/skills.ts
|
|
12498
|
-
var
|
|
12499
|
-
var
|
|
13753
|
+
var import_node_fs21 = __toESM(require("node:fs"));
|
|
13754
|
+
var import_node_path19 = __toESM(require("node:path"));
|
|
12500
13755
|
function packageRoot() {
|
|
12501
13756
|
const candidates = [
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
13757
|
+
import_node_path19.default.resolve(__dirname, ".."),
|
|
13758
|
+
import_node_path19.default.resolve(import_node_path19.default.dirname(process.argv[1] ?? process.cwd()), ".."),
|
|
13759
|
+
import_node_path19.default.resolve(process.cwd(), "packages/cli")
|
|
12505
13760
|
];
|
|
12506
13761
|
for (const candidate of candidates) {
|
|
12507
|
-
if (
|
|
13762
|
+
if (import_node_fs21.default.existsSync(import_node_path19.default.join(candidate, "AGENTS.md"))) return candidate;
|
|
12508
13763
|
}
|
|
12509
13764
|
throw new Error(
|
|
12510
13765
|
`Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
|
|
@@ -12513,24 +13768,24 @@ function packageRoot() {
|
|
|
12513
13768
|
function skillPaths() {
|
|
12514
13769
|
const root = packageRoot();
|
|
12515
13770
|
return {
|
|
12516
|
-
agents:
|
|
12517
|
-
skill:
|
|
13771
|
+
agents: import_node_path19.default.join(root, "AGENTS.md"),
|
|
13772
|
+
skill: import_node_path19.default.join(root, "skills", "apex-cli", "SKILL.md")
|
|
12518
13773
|
};
|
|
12519
13774
|
}
|
|
12520
13775
|
function installSkill(paths, targetRoot) {
|
|
12521
|
-
const missing = [paths.skill, paths.agents].filter((filePath) => !
|
|
13776
|
+
const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs21.default.existsSync(filePath));
|
|
12522
13777
|
if (missing.length > 0) {
|
|
12523
13778
|
throw new Error(
|
|
12524
13779
|
`Apex CLI package files not found. Missing: ${missing.join(", ")}`
|
|
12525
13780
|
);
|
|
12526
13781
|
}
|
|
12527
|
-
const target =
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
13782
|
+
const target = import_node_path19.default.resolve(targetRoot, "apex-cli");
|
|
13783
|
+
import_node_fs21.default.mkdirSync(target, { recursive: true });
|
|
13784
|
+
import_node_fs21.default.copyFileSync(paths.skill, import_node_path19.default.join(target, "SKILL.md"));
|
|
13785
|
+
import_node_fs21.default.copyFileSync(paths.agents, import_node_path19.default.join(target, "AGENTS.md"));
|
|
12531
13786
|
return target;
|
|
12532
13787
|
}
|
|
12533
|
-
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
|
|
13788
|
+
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 action61(opts) {
|
|
12534
13789
|
let paths;
|
|
12535
13790
|
try {
|
|
12536
13791
|
paths = skillPaths();
|
|
@@ -12564,7 +13819,7 @@ var settingsGet = new Command("settings").description("Show platform operator se
|
|
|
12564
13819
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
|
|
12565
13820
|
output(this, res.data, () => printTable(["SETTING", "VALUE"], Object.entries(res.data.data).map(([k, v]) => [k, v ? "on" : "off"])));
|
|
12566
13821
|
});
|
|
12567
|
-
var
|
|
13822
|
+
var set4 = new Command("set").description("Set platform kill switches").option("--guardian <on|off>").option("--srm-check <on|off>").option("--confirm", "Confirm immediate production impact").action(async function(opts) {
|
|
12568
13823
|
if (!opts.guardian && !opts.srmCheck) fail(this, "Provide --guardian or --srm-check", 3);
|
|
12569
13824
|
let body5 = {};
|
|
12570
13825
|
try {
|
|
@@ -12581,7 +13836,7 @@ var set = new Command("set").description("Set platform kill switches").option("-
|
|
|
12581
13836
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
|
|
12582
13837
|
output(this, res.data, () => console.log("Platform settings updated."));
|
|
12583
13838
|
});
|
|
12584
|
-
settingsGet.addCommand(
|
|
13839
|
+
settingsGet.addCommand(set4);
|
|
12585
13840
|
var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
|
|
12586
13841
|
const res = await getAdminQuarantine(requireCredentials());
|
|
12587
13842
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
|
|
@@ -12591,10 +13846,11 @@ var adminCommand = new Command("admin").description("Operate allowlisted platfor
|
|
|
12591
13846
|
|
|
12592
13847
|
// src/commands/membership.ts
|
|
12593
13848
|
init_source();
|
|
12594
|
-
async function
|
|
13849
|
+
async function run2(c, p, label) {
|
|
12595
13850
|
const r = await p;
|
|
12596
13851
|
if (!r.ok) fail(c, r.error ?? `Failed to ${label}`, classifyStatus(r.status), r.errorDetails);
|
|
12597
|
-
output(c, r.data ?? {}, () =>
|
|
13852
|
+
output(c, r.data ?? {}, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
|
|
13853
|
+
`));
|
|
12598
13854
|
}
|
|
12599
13855
|
async function confirm(c, force, label, retry) {
|
|
12600
13856
|
if (force) return true;
|
|
@@ -12611,43 +13867,43 @@ async function confirm(c, force, label, retry) {
|
|
|
12611
13867
|
}
|
|
12612
13868
|
var orgCommand = new Command("org").description("Manage the current organization");
|
|
12613
13869
|
orgCommand.command("get").action(async function() {
|
|
12614
|
-
await
|
|
13870
|
+
await run2(this, getOrg(requireUserCredentials()), "get organization");
|
|
12615
13871
|
});
|
|
12616
13872
|
orgCommand.command("update").option("--name <name>").option("--slug <slug>").option("--billing-email <email>").action(async function(o) {
|
|
12617
|
-
await
|
|
13873
|
+
await run2(this, updateOrg(requireUserCredentials(), { ...o.name ? { name: o.name } : {}, ...o.slug ? { slug: o.slug } : {}, ...o.billingEmail ? { billing_email: o.billingEmail } : {} }), "update organization");
|
|
12618
13874
|
});
|
|
12619
13875
|
orgCommand.command("members").action(async function() {
|
|
12620
|
-
await
|
|
13876
|
+
await run2(this, listOrgMembers(requireUserCredentials()), "list members");
|
|
12621
13877
|
});
|
|
12622
13878
|
orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").option("--default-shop-role <role>", "Default shop role", "viewer").action(async function(email, o) {
|
|
12623
|
-
await
|
|
13879
|
+
await run2(this, inviteOrg(requireUserCredentials(), email, o.role ?? o.orgRole, o.defaultShopRole), "invite member");
|
|
12624
13880
|
});
|
|
12625
13881
|
orgCommand.command("revoke-invite").argument("<id>").action(async function(id) {
|
|
12626
|
-
await
|
|
13882
|
+
await run2(this, revokeOrgInvite(requireUserCredentials(), id), "revoke invitation");
|
|
12627
13883
|
});
|
|
12628
13884
|
orgCommand.command("set-role").argument("<memberId>").argument("<role>").option("--default-shop-role <role>").action(async function(id, role, o) {
|
|
12629
|
-
await
|
|
13885
|
+
await run2(this, setOrgMember(requireUserCredentials(), id, { role, ...o.defaultShopRole ? { default_shop_role: o.defaultShopRole } : {} }), "set role");
|
|
12630
13886
|
});
|
|
12631
13887
|
orgCommand.command("remove-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
|
|
12632
13888
|
if (!await confirm(this, o.force, `Remove organization member ${id}`, `apex org remove-member ${id} --force`)) return;
|
|
12633
|
-
await
|
|
13889
|
+
await run2(this, removeOrgMember(requireUserCredentials(), id), "remove member");
|
|
12634
13890
|
});
|
|
12635
13891
|
var teamCommand = new Command("team").description("Manage a shop team");
|
|
12636
13892
|
teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
|
|
12637
|
-
await
|
|
13893
|
+
await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
|
|
12638
13894
|
});
|
|
12639
13895
|
teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").requiredOption("--role <role>").action(async function(email, o) {
|
|
12640
|
-
await
|
|
13896
|
+
await run2(this, inviteTeam(requireUserCredentials(), email, o.shop, o.role), "invite team member");
|
|
12641
13897
|
});
|
|
12642
13898
|
teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id>").action(async function(id, o) {
|
|
12643
|
-
await
|
|
13899
|
+
await run2(this, revokeTeamInvite(requireUserCredentials(), id, o.shop), "revoke invitation");
|
|
12644
13900
|
});
|
|
12645
13901
|
teamCommand.command("set-role").argument("<memberId>").argument("<role>").requiredOption("--shop <id>").action(async function(id, role, o) {
|
|
12646
|
-
await
|
|
13902
|
+
await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
|
|
12647
13903
|
});
|
|
12648
13904
|
teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
|
|
12649
13905
|
if (!await confirm(this, o.force, `Remove team member ${id}`, `apex team remove ${id} --shop ${o.shop} --force`)) return;
|
|
12650
|
-
await
|
|
13906
|
+
await run2(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
|
|
12651
13907
|
});
|
|
12652
13908
|
|
|
12653
13909
|
// src/commands/account.ts
|
|
@@ -12656,7 +13912,7 @@ function userCredentials2() {
|
|
|
12656
13912
|
if (!creds2.userApiKey) throw new CliError('User authentication required. Run "apex login --user".', EXIT_CODES.auth);
|
|
12657
13913
|
return creds2;
|
|
12658
13914
|
}
|
|
12659
|
-
var
|
|
13915
|
+
var set5 = new Command("set").option("--name <text>", "Display name").option("--avatar <url>", "Avatar data URL").action(async function(opts) {
|
|
12660
13916
|
if (opts.name === void 0 && opts.avatar === void 0) throw new CliError("At least one of --name or --avatar is required", EXIT_CODES.validation);
|
|
12661
13917
|
const res = await setAccountProfile(userCredentials2(), { ...opts.name !== void 0 ? { displayName: opts.name } : {}, ...opts.avatar !== void 0 ? { avatarDataUrl: opts.avatar } : {} });
|
|
12662
13918
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update account profile", classifyStatus(res.status));
|
|
@@ -12667,7 +13923,7 @@ var profile2 = new Command("profile").description("Show the current account prof
|
|
|
12667
13923
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to get account profile", classifyStatus(res.status));
|
|
12668
13924
|
output(this, res.data, () => console.log(`${res.data.data.displayName ?? "(no name)"}
|
|
12669
13925
|
${res.data.data.avatarDataUrl ?? "(no avatar)"}`));
|
|
12670
|
-
}).addCommand(
|
|
13926
|
+
}).addCommand(set5);
|
|
12671
13927
|
var accountCommand = new Command("account").description("Manage your Apex account").addCommand(profile2);
|
|
12672
13928
|
|
|
12673
13929
|
// src/commands/keys.ts
|
|
@@ -12676,12 +13932,12 @@ function creds() {
|
|
|
12676
13932
|
if (!value.userApiKey) throw new CliError('User authentication required. Run "apex login --user".', EXIT_CODES.auth);
|
|
12677
13933
|
return value;
|
|
12678
13934
|
}
|
|
12679
|
-
var
|
|
13935
|
+
var list13 = new Command("list").requiredOption("--shop <id>").action(async function(opts) {
|
|
12680
13936
|
const res = await listUserManagedKeys(creds(), opts.shop);
|
|
12681
13937
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list API keys", classifyStatus(res.status));
|
|
12682
13938
|
output(this, res.data, () => printTable(["ID", "NAME", "PREFIX", "TYPE"], res.data.data.map((key) => [key.id, key.name, key.key_prefix, key.type])));
|
|
12683
13939
|
});
|
|
12684
|
-
var
|
|
13940
|
+
var create6 = new Command("create").requiredOption("--shop <id>").requiredOption("--name <name>").requiredOption("--type <type>").option("--permissions <csv>").action(async function(opts) {
|
|
12685
13941
|
if (!["live_secret", "test_secret", "publishable"].includes(opts.type)) throw new CliError("--type must be live_secret, test_secret, or publishable", EXIT_CODES.validation);
|
|
12686
13942
|
const permissions = opts.permissions?.split(",").map((item) => item.trim()).filter(Boolean);
|
|
12687
13943
|
const res = await createUserManagedKey(creds(), { shop_id: opts.shop, name: opts.name, type: opts.type, ...permissions?.length ? { permissions } : {} });
|
|
@@ -12696,13 +13952,311 @@ var revoke = new Command("revoke").argument("<keyId>").requiredOption("--shop <i
|
|
|
12696
13952
|
if (!res.ok || !res.data) fail(this, res.error ?? "Failed to revoke API key", classifyStatus(res.status));
|
|
12697
13953
|
output(this, res.data, () => console.log(`Revoked ${keyId}`));
|
|
12698
13954
|
});
|
|
12699
|
-
var keysCommand = new Command("keys").description("Manage shop API keys").addCommand(
|
|
13955
|
+
var keysCommand = new Command("keys").description("Manage shop API keys").addCommand(list13).addCommand(create6).addCommand(revoke);
|
|
13956
|
+
|
|
13957
|
+
// src/commands/prospect.ts
|
|
13958
|
+
init_source();
|
|
13959
|
+
|
|
13960
|
+
// src/lib/detect-markers.ts
|
|
13961
|
+
var DETECTION_MARKERS = [
|
|
13962
|
+
{ id: "cloudflare_challenge", kind: "protected", verdict: "Cloudflare challenge", match: /cf-browser-verification|cf-chl-|cdn-cgi\/challenge-platform/i },
|
|
13963
|
+
{ id: "shopify_cart_js", kind: "platform", verdict: "Shopify", confidence: "high", recipe: { placement: "Shopify theme.liquid before </head> or the customer-deployed edge worker for headless themes." }, match: /__apex_secondary_shopify_cart_js__/i },
|
|
13964
|
+
{ id: "shopify", kind: "platform", verdict: "Shopify", confidence: "high", recipe: { placement: "Shopify theme.liquid before </head> or the customer-deployed edge worker for headless themes." }, match: /cdn\.shopify\.com|Shopify\.theme|window\.Shopify|\/cart\.js/i },
|
|
13965
|
+
{ id: "woocommerce", kind: "platform", verdict: "WooCommerce", confidence: "high", recipe: { placement: "WordPress header or WooCommerce plugin template before </head>." }, match: /woocommerce|wp-content\/plugins\/woocommerce|wc-cart-fragments/i },
|
|
13966
|
+
{ id: "shopware", kind: "platform", verdict: "Shopware", confidence: "high", recipe: { placement: "Shopware storefront base template before </head>." }, match: /shopware|sw-?storefront|data-shopware/i },
|
|
13967
|
+
{ id: "next_headless", kind: "platform", verdict: "Next.js headless", confidence: "high", recipe: { placement: "Headless app document/root layout before </head> so Apex loads on every route." }, match: /__NEXT_DATA__|\/_next\//i },
|
|
13968
|
+
{ id: "webflow", kind: "platform", verdict: "Webflow", confidence: "high", recipe: { placement: "Webflow Project Settings \u2192 Custom Code \u2192 Head Code." }, match: /data-wf-page|data-wf-site|webflow\.js|assets\.website-files\.com/i },
|
|
13969
|
+
{ id: "cookiescript", kind: "cmp", verdict: "CookieScript", recipe: { consentProvider: "cookiescript", consentGroup: "targeting" }, match: /cookie-script\.com|CookieScript|cookiescript/i },
|
|
13970
|
+
{ id: "usercentrics", kind: "cmp", verdict: "Usercentrics", recipe: { consentProvider: "usercentrics", consentGroup: "marketing" }, match: /app\.usercentrics\.eu|usercentrics\.eu|UC_UI|usercentrics/i },
|
|
13971
|
+
{ id: "cookiebot", kind: "cmp", verdict: "Cookiebot", recipe: { consentProvider: "cookiebot", consentGroup: "marketing" }, match: /consent\.cookiebot\.com|Cookiebot/i },
|
|
13972
|
+
{ id: "ccm19", kind: "cmp", verdict: "CCM19", recipe: { consentProvider: "ccm19", consentGroup: "targeting" }, match: /ccm19\.de|CCM\.|ccm19/i },
|
|
13973
|
+
{ id: "onetrust", kind: "cmp", verdict: "OneTrust", recipe: { consentProvider: "onetrust", consentGroup: "C0004" }, match: /cdn\.cookielaw\.org|Optanon|OneTrust/i },
|
|
13974
|
+
{ id: "pandectes", kind: "cmp", verdict: "Pandectes", recipe: { consentProvider: "pandectes", consentGroup: "marketing" }, match: /pandectes|Pandectes/i },
|
|
13975
|
+
{ id: "gtm", kind: "analytics", verdict: "Google Tag Manager", match: /googletagmanager\.com\/gtm\.js|GTM-[A-Z0-9]+/i },
|
|
13976
|
+
{ id: "ga4", kind: "analytics", verdict: "Google Analytics", match: /googletagmanager\.com\/gtag\/js\?id=|\bG-[A-Z0-9]{6,}\b|google-analytics\.com/ },
|
|
13977
|
+
{ id: "meta_pixel", kind: "analytics", verdict: "Meta Pixel", match: /fbevents\.js|fbq\(/i },
|
|
13978
|
+
{ id: "next_spa", kind: "spa", verdict: "Next.js", recipe: { runtimeHint: "applyTiming=after_hydration" }, match: /__NEXT_DATA__|\/_next\//i },
|
|
13979
|
+
{ id: "react", kind: "spa", verdict: "React", recipe: { runtimeHint: "applyTiming=after_hydration" }, match: /data-reactroot|react-dom|__REACT_DEVTOOLS_GLOBAL_HOOK__/i },
|
|
13980
|
+
{ id: "vue", kind: "spa", verdict: "Vue", recipe: { runtimeHint: "applyTiming=after_hydration" }, match: /data-v-|__VUE__|vue\.runtime/i },
|
|
13981
|
+
{ id: "apex_snippet", kind: "apex", verdict: "Apex snippet", match: /sdk\.drip-apex\.com\/drip\.js|events\.drip-apex\.com|edge\.drip-apex\.com|Drip\.init|data-apex/i }
|
|
13982
|
+
];
|
|
13983
|
+
function matchDetectionMarkers(source, kind5) {
|
|
13984
|
+
return DETECTION_MARKERS.filter((marker) => (!kind5 || marker.kind === kind5) && marker.match.test(source));
|
|
13985
|
+
}
|
|
13986
|
+
|
|
13987
|
+
// src/commands/prospect.ts
|
|
13988
|
+
var DESKTOP_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36 ApexProspectAudit/1.0";
|
|
13989
|
+
var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
13990
|
+
async function readResponseText(res) {
|
|
13991
|
+
if (!res.body) return "";
|
|
13992
|
+
const reader = res.body.getReader();
|
|
13993
|
+
const chunks = [];
|
|
13994
|
+
let totalBytes = 0;
|
|
13995
|
+
try {
|
|
13996
|
+
while (totalBytes < MAX_RESPONSE_BYTES) {
|
|
13997
|
+
const { done: done2, value } = await reader.read();
|
|
13998
|
+
if (done2) break;
|
|
13999
|
+
const remainingBytes = MAX_RESPONSE_BYTES - totalBytes;
|
|
14000
|
+
if (value.byteLength >= remainingBytes) {
|
|
14001
|
+
chunks.push(value.byteLength === remainingBytes ? value : value.slice(0, remainingBytes));
|
|
14002
|
+
totalBytes += remainingBytes;
|
|
14003
|
+
await reader.cancel().catch(() => void 0);
|
|
14004
|
+
break;
|
|
14005
|
+
}
|
|
14006
|
+
chunks.push(value);
|
|
14007
|
+
totalBytes += value.byteLength;
|
|
14008
|
+
}
|
|
14009
|
+
} finally {
|
|
14010
|
+
reader.releaseLock();
|
|
14011
|
+
}
|
|
14012
|
+
const body5 = new Uint8Array(totalBytes);
|
|
14013
|
+
let offset = 0;
|
|
14014
|
+
for (const chunk of chunks) {
|
|
14015
|
+
body5.set(chunk, offset);
|
|
14016
|
+
offset += chunk.byteLength;
|
|
14017
|
+
}
|
|
14018
|
+
return new TextDecoder().decode(body5);
|
|
14019
|
+
}
|
|
14020
|
+
async function fetchText(url) {
|
|
14021
|
+
if (process.env.APEX_PROSPECT_AUDIT_FIXTURE) {
|
|
14022
|
+
const fixture = JSON.parse(process.env.APEX_PROSPECT_AUDIT_FIXTURE);
|
|
14023
|
+
const hit = fixture[url] ?? fixture[new URL(url).pathname] ?? fixture["*"];
|
|
14024
|
+
if (hit?.throws) throw new Error(hit.throws);
|
|
14025
|
+
return { status: hit?.status ?? 200, text: hit?.body ?? "", finalUrl: url };
|
|
14026
|
+
}
|
|
14027
|
+
const res = await fetch(url, { headers: { "user-agent": DESKTOP_UA, accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8" }, redirect: "follow", signal: AbortSignal.timeout(12e3) });
|
|
14028
|
+
return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
|
|
14029
|
+
}
|
|
14030
|
+
function abs(base, path21) {
|
|
14031
|
+
return new URL(path21, base).toString();
|
|
14032
|
+
}
|
|
14033
|
+
function looksLikeShopifyCart(text) {
|
|
14034
|
+
try {
|
|
14035
|
+
const payload = JSON.parse(text);
|
|
14036
|
+
return payload !== null && typeof payload === "object" && (typeof payload.token === "string" || payload.token === null) && Array.isArray(payload.items);
|
|
14037
|
+
} catch {
|
|
14038
|
+
return false;
|
|
14039
|
+
}
|
|
14040
|
+
}
|
|
14041
|
+
async function collectSignals(url) {
|
|
14042
|
+
let primary;
|
|
14043
|
+
try {
|
|
14044
|
+
primary = await fetchText(url);
|
|
14045
|
+
} catch {
|
|
14046
|
+
return { reachable: false, protected: false, source: "" };
|
|
14047
|
+
}
|
|
14048
|
+
const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
|
|
14049
|
+
const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
|
|
14050
|
+
if (!protectedHit) {
|
|
14051
|
+
for (const path21 of ["/cart.js"]) {
|
|
14052
|
+
try {
|
|
14053
|
+
const secondary = await fetchText(abs(primary.finalUrl || url, path21));
|
|
14054
|
+
if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
|
|
14055
|
+
chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
|
|
14056
|
+
}
|
|
14057
|
+
} catch {
|
|
14058
|
+
}
|
|
14059
|
+
}
|
|
14060
|
+
}
|
|
14061
|
+
return { reachable: primary.status < 500, protected: protectedHit, source: chunks.join("\n") };
|
|
14062
|
+
}
|
|
14063
|
+
function unique2(values) {
|
|
14064
|
+
return [...new Set(values)];
|
|
14065
|
+
}
|
|
14066
|
+
async function runProspectAudit(url) {
|
|
14067
|
+
let parsed;
|
|
14068
|
+
try {
|
|
14069
|
+
parsed = new URL(url);
|
|
14070
|
+
} catch {
|
|
14071
|
+
parsed = new URL(`https://${url}`);
|
|
14072
|
+
}
|
|
14073
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("URL must use http or https");
|
|
14074
|
+
const { reachable, protected: isProtected, source } = await collectSignals(parsed.toString());
|
|
14075
|
+
if (isProtected) {
|
|
14076
|
+
return { url: parsed.toString(), reachable, protected: true, platform: { verdict: "unknown", confidence: "low", markers: [] }, cmp: { markers: [] }, analytics: [], spa: { markers: [] }, apexSnippet: false, recipe: { placement: "protected: verify in a real browser", runtimeHints: [] }, openQuestions: ["protected: verify in a real browser", "Who owns checkout and revenue-event access?"] };
|
|
14077
|
+
}
|
|
14078
|
+
const platforms = matchDetectionMarkers(source, "platform");
|
|
14079
|
+
const cmps = matchDetectionMarkers(source, "cmp");
|
|
14080
|
+
const analyticsMarkers = matchDetectionMarkers(source, "analytics");
|
|
14081
|
+
const spaMarkers = matchDetectionMarkers(source, "spa");
|
|
14082
|
+
const apexSnippet = matchDetectionMarkers(source, "apex").length > 0;
|
|
14083
|
+
const platform2 = platforms[0];
|
|
14084
|
+
const cmp = cmps[0];
|
|
14085
|
+
const runtimeHints = unique2(spaMarkers.map((marker) => marker.recipe?.runtimeHint).filter(Boolean));
|
|
14086
|
+
return {
|
|
14087
|
+
url: parsed.toString(),
|
|
14088
|
+
reachable,
|
|
14089
|
+
protected: false,
|
|
14090
|
+
platform: { verdict: platform2?.verdict ?? "unknown", confidence: platform2?.confidence ?? "low", markers: platforms.map((m) => m.id) },
|
|
14091
|
+
cmp: { ...cmp?.recipe?.consentProvider ? { vendor: cmp.verdict } : {}, markers: cmps.map((m) => m.id) },
|
|
14092
|
+
analytics: unique2(analyticsMarkers.map((m) => m.verdict)),
|
|
14093
|
+
spa: { ...spaMarkers[0] ? { framework: spaMarkers[0].verdict } : {}, markers: spaMarkers.map((m) => m.id) },
|
|
14094
|
+
apexSnippet,
|
|
14095
|
+
recipe: {
|
|
14096
|
+
placement: platform2?.recipe?.placement ?? "Install the Apex snippet in the global <head> before other experiment code, or use the edge worker for a headless/custom stack.",
|
|
14097
|
+
...cmp?.recipe?.consentProvider && cmp.recipe.consentGroup ? { consentPreset: { provider: cmp.recipe.consentProvider, suggestedGroup: cmp.recipe.consentGroup } } : {},
|
|
14098
|
+
runtimeHints
|
|
14099
|
+
},
|
|
14100
|
+
openQuestions: ["Who owns checkout and can add/verify order attribution?", "Which revenue event is authoritative and available to Apex?", ...cmp ? [] : ["Which CMP consent category should gate analytics and storage?"]]
|
|
14101
|
+
};
|
|
14102
|
+
}
|
|
14103
|
+
var prospectCommand = new Command("prospect").description("Pre-sales storefront diagnostics").addCommand(new Command("audit").argument("<url>", "Prospect storefront URL").description("Audit a public storefront URL and recommend an Apex install recipe.").action(async (url, _options, command) => {
|
|
14104
|
+
try {
|
|
14105
|
+
const data = await runProspectAudit(url);
|
|
14106
|
+
if (data.protected) process.stderr.write("WARN protected: verify in a real browser\n");
|
|
14107
|
+
output(command, data, () => {
|
|
14108
|
+
console.log(source_default.bold(`Apex prospect audit: ${data.url}`));
|
|
14109
|
+
console.log(`Readiness: ${data.protected ? "WARN protected: verify in a real browser" : data.reachable ? "reachable" : "unreachable"}`);
|
|
14110
|
+
console.log(`Platform: ${data.platform.verdict} (${data.platform.confidence})`);
|
|
14111
|
+
console.log(`CMP: ${data.cmp.vendor ?? "unknown"}`);
|
|
14112
|
+
console.log(`Analytics: ${data.analytics.join(", ") || "none detected"}`);
|
|
14113
|
+
console.log(`SPA: ${data.spa.framework ?? "none detected"}`);
|
|
14114
|
+
console.log(`Apex snippet: ${data.apexSnippet ? "present" : "not detected"}`);
|
|
14115
|
+
console.log(`Install recipe: ${data.recipe.placement}`);
|
|
14116
|
+
if (data.recipe.consentPreset) console.log(`Consent preset: ${data.recipe.consentPreset.provider} / ${data.recipe.consentPreset.suggestedGroup}`);
|
|
14117
|
+
for (const hint of data.recipe.runtimeHints) console.log(`Runtime hint: ${hint}`);
|
|
14118
|
+
console.log("Open questions:");
|
|
14119
|
+
for (const question of data.openQuestions) console.log(`- ${question}`);
|
|
14120
|
+
});
|
|
14121
|
+
} catch (error) {
|
|
14122
|
+
fail(command, error instanceof Error ? error.message : String(error), EXIT_CODES.general);
|
|
14123
|
+
}
|
|
14124
|
+
}));
|
|
14125
|
+
|
|
14126
|
+
// src/commands/webhooks.ts
|
|
14127
|
+
init_source();
|
|
14128
|
+
var SIGNATURE_DOC = "https://app.drip-apex.com/docs/api/webhooks#signatures";
|
|
14129
|
+
function requireShopSecretKey(command) {
|
|
14130
|
+
const creds2 = requireCredentials();
|
|
14131
|
+
if (creds2.apiKey.startsWith("apx_uk_")) {
|
|
14132
|
+
fail(
|
|
14133
|
+
command,
|
|
14134
|
+
"apex webhooks requires a secret shop API key (apx_...). User API keys (apx_uk_...) are not accepted by shop-scoped v1 webhook routes.",
|
|
14135
|
+
EXIT_CODES.auth,
|
|
14136
|
+
{ required: "secret_shop_api_key", rejectedPrefix: "apx_uk_" }
|
|
14137
|
+
);
|
|
14138
|
+
}
|
|
14139
|
+
}
|
|
14140
|
+
function handleResult(command, result, fallback2) {
|
|
14141
|
+
if (!result.ok || !result.data) {
|
|
14142
|
+
fail(command, result.error ?? fallback2, classifyStatus(result.status), result.errorDetails);
|
|
14143
|
+
}
|
|
14144
|
+
return result.data;
|
|
14145
|
+
}
|
|
14146
|
+
async function loadWebhookIndex(command) {
|
|
14147
|
+
requireShopSecretKey(command);
|
|
14148
|
+
return handleResult(command, await listWebhooks(requireCredentials()), "Failed to load webhooks");
|
|
14149
|
+
}
|
|
14150
|
+
function printWebhooks(items) {
|
|
14151
|
+
if (items.length === 0) {
|
|
14152
|
+
console.log(source_default.dim("No webhooks configured."));
|
|
14153
|
+
return;
|
|
14154
|
+
}
|
|
14155
|
+
printTable(
|
|
14156
|
+
["ID", "ACTIVE", "FAILURES", "DISABLED", "SECRET", "URL", "EVENTS"],
|
|
14157
|
+
items.map((item) => [
|
|
14158
|
+
item.id,
|
|
14159
|
+
item.active ? "yes" : "no",
|
|
14160
|
+
String(item.consecutiveFailures ?? 0),
|
|
14161
|
+
item.disabledAt ?? "-",
|
|
14162
|
+
item.hasSecret ? "true" : "false",
|
|
14163
|
+
item.url,
|
|
14164
|
+
item.events.join(", ")
|
|
14165
|
+
]),
|
|
14166
|
+
[28, 8, 8, 24, 8, 42, 48]
|
|
14167
|
+
);
|
|
14168
|
+
}
|
|
14169
|
+
function printDeliveries(deliveries2, webhook) {
|
|
14170
|
+
if (webhook) {
|
|
14171
|
+
console.log(`Webhook ${webhook.id}: ${webhook.consecutiveFailures ?? 0} consecutive failure(s)${webhook.disabledAt ? `, disabled at ${webhook.disabledAt}. Re-create the webhook to rotate its secret.` : ""}`);
|
|
14172
|
+
}
|
|
14173
|
+
if (deliveries2.length === 0) {
|
|
14174
|
+
console.log(source_default.dim("No deliveries found."));
|
|
14175
|
+
return;
|
|
14176
|
+
}
|
|
14177
|
+
printTable(
|
|
14178
|
+
["ID", "EVENT", "STATUS", "ATTEMPTS", "HTTP", "ERROR", "CREATED"],
|
|
14179
|
+
deliveries2.map((delivery) => [
|
|
14180
|
+
delivery.id,
|
|
14181
|
+
delivery.event,
|
|
14182
|
+
delivery.status,
|
|
14183
|
+
String(delivery.attempts),
|
|
14184
|
+
delivery.response_status == null ? "-" : String(delivery.response_status),
|
|
14185
|
+
delivery.last_error ?? "-",
|
|
14186
|
+
delivery.created_at
|
|
14187
|
+
]),
|
|
14188
|
+
[28, 28, 12, 8, 6, 36, 24]
|
|
14189
|
+
);
|
|
14190
|
+
}
|
|
14191
|
+
function selectedEvents(opts) {
|
|
14192
|
+
return [...new Set((opts.event ?? []).map((event) => event.trim()).filter(Boolean))];
|
|
14193
|
+
}
|
|
14194
|
+
var list14 = new Command("list").alias("ls").description("List webhooks. JSON includes availableEvents; secrets are never returned after create.").action(async function action62() {
|
|
14195
|
+
const data = await loadWebhookIndex(this);
|
|
14196
|
+
output(this, data, () => printWebhooks(data.data));
|
|
14197
|
+
});
|
|
14198
|
+
var create7 = new Command("create").description(`Create a webhook. Prints the signing secret once. Signature docs: ${SIGNATURE_DOC}`).requiredOption("--url <url>", "HTTPS callback URL").option("--event <event>", "Webhook event to subscribe to; repeat for multiple events", (value, previous = []) => [...previous, value], []).action(async function action63(opts) {
|
|
14199
|
+
const events = selectedEvents(opts);
|
|
14200
|
+
if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
|
|
14201
|
+
const index = await loadWebhookIndex(this);
|
|
14202
|
+
const allowed = new Set(index.availableEvents);
|
|
14203
|
+
const unknown = events.filter((event) => !allowed.has(event));
|
|
14204
|
+
if (unknown.length > 0) {
|
|
14205
|
+
fail(this, `Unknown webhook event(s): ${unknown.join(", ")}`, EXIT_CODES.validation, { availableEvents: index.availableEvents });
|
|
14206
|
+
}
|
|
14207
|
+
const result = handleResult(this, await createWebhook(requireCredentials(), { url: opts.url, events }), "Failed to create webhook");
|
|
14208
|
+
output(this, { webhook: result, secret: result.secret, signatureDoc: SIGNATURE_DOC, warning: "Store this signing secret now. Apex will not show it again." }, () => {
|
|
14209
|
+
console.log(source_default.green(`Created webhook ${result.id}`));
|
|
14210
|
+
console.log(source_default.yellow("Store this signing secret now. Apex will not show it again."));
|
|
14211
|
+
console.log(result.secret);
|
|
14212
|
+
console.log(`Signature verification: ${SIGNATURE_DOC}`);
|
|
14213
|
+
});
|
|
14214
|
+
});
|
|
14215
|
+
var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action64(id) {
|
|
14216
|
+
requireShopSecretKey(this);
|
|
14217
|
+
const result = handleResult(this, await deleteWebhook(requireCredentials(), id), "Failed to delete webhook");
|
|
14218
|
+
output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
|
|
14219
|
+
});
|
|
14220
|
+
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 action65(id, opts) {
|
|
14221
|
+
const limit = Number.parseInt(opts.limit, 10);
|
|
14222
|
+
if (!Number.isInteger(limit) || limit < 1) fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
|
|
14223
|
+
requireShopSecretKey(this);
|
|
14224
|
+
const result = handleResult(this, await getWebhookDeliveries(requireCredentials(), id, limit), "Failed to load deliveries");
|
|
14225
|
+
output(this, result, () => printDeliveries(result.data, result.webhook));
|
|
14226
|
+
});
|
|
14227
|
+
var update4 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option("--event <event>", "Replacement event; repeat for multiple events", (value, previous = []) => [...previous, value], []).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action66(id, opts) {
|
|
14228
|
+
if (opts.active && opts.inactive) fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
|
|
14229
|
+
const events = selectedEvents(opts);
|
|
14230
|
+
const body5 = {};
|
|
14231
|
+
if (opts.url) body5.url = opts.url;
|
|
14232
|
+
if (events.length > 0) {
|
|
14233
|
+
const index = await loadWebhookIndex(this);
|
|
14234
|
+
const allowed = new Set(index.availableEvents);
|
|
14235
|
+
const unknown = events.filter((event) => !allowed.has(event));
|
|
14236
|
+
if (unknown.length > 0) fail(this, `Unknown webhook event(s): ${unknown.join(", ")}`, EXIT_CODES.validation, { availableEvents: index.availableEvents });
|
|
14237
|
+
body5.events = events;
|
|
14238
|
+
} else {
|
|
14239
|
+
requireShopSecretKey(this);
|
|
14240
|
+
}
|
|
14241
|
+
if (opts.active) body5.active = true;
|
|
14242
|
+
if (opts.inactive) body5.active = false;
|
|
14243
|
+
if (Object.keys(body5).length === 0) fail(this, "Pass at least one of --url, --event, --active, or --inactive", EXIT_CODES.validation);
|
|
14244
|
+
const result = handleResult(this, await updateWebhook(requireCredentials(), id, body5), "Failed to update webhook");
|
|
14245
|
+
output(this, { webhook: result }, () => printWebhooks([result]));
|
|
14246
|
+
});
|
|
14247
|
+
var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
|
|
14248
|
+
requireShopSecretKey(this);
|
|
14249
|
+
const result = handleResult(this, await testWebhook(requireCredentials(), id), "Failed to send test webhook");
|
|
14250
|
+
output(this, result, () => console.log(source_default.green(`Sent test delivery ${result.delivery.id}: ${result.delivery.status}`)));
|
|
14251
|
+
});
|
|
14252
|
+
var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update4).addCommand(remove4).addCommand(deliveries).addCommand(test3);
|
|
12700
14253
|
|
|
12701
14254
|
// src/commands/config-resources.ts
|
|
12702
|
-
var
|
|
12703
|
-
var
|
|
14255
|
+
var import_node_fs22 = __toESM(require("node:fs"));
|
|
14256
|
+
var import_node_path20 = __toESM(require("node:path"));
|
|
12704
14257
|
function show(resource) {
|
|
12705
|
-
|
|
14258
|
+
process.stdout.write(`${JSON.stringify(resource, null, 2)}
|
|
14259
|
+
`);
|
|
12706
14260
|
}
|
|
12707
14261
|
var ASSET_MIME_TYPES = {
|
|
12708
14262
|
".png": "image/png",
|
|
@@ -12732,7 +14286,7 @@ async function confirmAssetDelete(id) {
|
|
|
12732
14286
|
return answer.trim().toLowerCase() === "y";
|
|
12733
14287
|
}
|
|
12734
14288
|
function readFamily(family, label) {
|
|
12735
|
-
const
|
|
14289
|
+
const list15 = new Command("list").alias("ls").description(`List ${label}`).option("-n, --limit <n>", "Max results", "50").option("--cursor <id>", "Pagination cursor").action(async function(o) {
|
|
12736
14290
|
const limit = Number(o.limit);
|
|
12737
14291
|
if (!Number.isInteger(limit) || limit <= 0)
|
|
12738
14292
|
fail(
|
|
@@ -12765,7 +14319,7 @@ function readFamily(family, label) {
|
|
|
12765
14319
|
)
|
|
12766
14320
|
);
|
|
12767
14321
|
});
|
|
12768
|
-
const
|
|
14322
|
+
const get9 = new Command("get").description(`Get one ${label.slice(0, -1)}`).argument("<id>").action(async function(id) {
|
|
12769
14323
|
const r = await getConfigResource(requireCredentials(), family, id);
|
|
12770
14324
|
if (!r.ok || !r.data)
|
|
12771
14325
|
fail(
|
|
@@ -12780,18 +14334,18 @@ function readFamily(family, label) {
|
|
|
12780
14334
|
() => show(r.data)
|
|
12781
14335
|
);
|
|
12782
14336
|
});
|
|
12783
|
-
return { list:
|
|
14337
|
+
return { list: list15, get: get9 };
|
|
12784
14338
|
}
|
|
12785
14339
|
var assetReads = readFamily("assets", "assets");
|
|
12786
14340
|
var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
|
|
12787
|
-
const absolute =
|
|
12788
|
-
if (!
|
|
14341
|
+
const absolute = import_node_path20.default.resolve(process.cwd(), file);
|
|
14342
|
+
if (!import_node_fs22.default.existsSync(absolute))
|
|
12789
14343
|
fail(this, `File not found: ${file}`, 3);
|
|
12790
|
-
const fileStat =
|
|
14344
|
+
const fileStat = import_node_fs22.default.statSync(absolute);
|
|
12791
14345
|
if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
|
|
12792
14346
|
if (fileStat.size > MAX_ASSET_SIZE_BYTES)
|
|
12793
14347
|
fail(this, `${file} is larger than 10 MB`, 3);
|
|
12794
|
-
const mimeType = ASSET_MIME_TYPES[
|
|
14348
|
+
const mimeType = ASSET_MIME_TYPES[import_node_path20.default.extname(absolute).toLowerCase()];
|
|
12795
14349
|
if (!mimeType)
|
|
12796
14350
|
fail(
|
|
12797
14351
|
this,
|
|
@@ -12800,8 +14354,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
|
|
|
12800
14354
|
);
|
|
12801
14355
|
const r = await uploadAssets(requireCredentials(), [
|
|
12802
14356
|
{
|
|
12803
|
-
name:
|
|
12804
|
-
bytes:
|
|
14357
|
+
name: import_node_path20.default.basename(absolute),
|
|
14358
|
+
bytes: import_node_fs22.default.readFileSync(absolute),
|
|
12805
14359
|
type: mimeType
|
|
12806
14360
|
}
|
|
12807
14361
|
]);
|
|
@@ -12814,7 +14368,7 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
|
|
|
12814
14368
|
);
|
|
12815
14369
|
output(this, r.data, () => r.data.assets.forEach(show));
|
|
12816
14370
|
});
|
|
12817
|
-
var
|
|
14371
|
+
var remove5 = new Command("delete").alias("rm").description("Delete an asset").argument("<id>").option("-f, --force", "Skip confirmation").action(async function(id, opts) {
|
|
12818
14372
|
if (!opts.force) {
|
|
12819
14373
|
const confirmed = await confirmAssetDelete(id);
|
|
12820
14374
|
if (!confirmed) {
|
|
@@ -12835,7 +14389,7 @@ var remove3 = new Command("delete").alias("rm").description("Delete an asset").a
|
|
|
12835
14389
|
);
|
|
12836
14390
|
output(this, r.data, () => console.log(`Deleted ${id}`));
|
|
12837
14391
|
});
|
|
12838
|
-
var assetsCommand = new Command("assets").description("Manage assets").addCommand(assetReads.list).addCommand(assetReads.get).addCommand(upload).addCommand(
|
|
14392
|
+
var assetsCommand = new Command("assets").description("Manage assets").addCommand(assetReads.list).addCommand(assetReads.get).addCommand(upload).addCommand(remove5);
|
|
12839
14393
|
var landingReads = readFamily("landing-pages", "landing pages");
|
|
12840
14394
|
var landingPagesCommand = new Command("landing-pages").description("Read landing pages").addCommand(landingReads.list).addCommand(landingReads.get);
|
|
12841
14395
|
var snippet = new Command("snippet").description("Generate an external purchase pixel snippet").option("--provider <name>").option("--handover-param <name>").option("--global-name <name>").action(async function(opts) {
|
|
@@ -12863,12 +14417,14 @@ function addJsonOption(command) {
|
|
|
12863
14417
|
}
|
|
12864
14418
|
var program2 = new Command().name("apex").description(
|
|
12865
14419
|
source_default.bold("Apex CLI") + " \u2014 A/B testing experiments from the command line"
|
|
12866
|
-
).version(CLI_VERSION).showSuggestionAfterError(
|
|
14420
|
+
).version(CLI_VERSION).showSuggestionAfterError(true).showHelpAfterError(true);
|
|
12867
14421
|
program2.addCommand(loginCommand);
|
|
12868
14422
|
program2.addCommand(logoutCommand);
|
|
12869
14423
|
program2.addCommand(whoamiCommand);
|
|
12870
14424
|
program2.addCommand(accountCommand);
|
|
12871
14425
|
program2.addCommand(keysCommand);
|
|
14426
|
+
program2.addCommand(prospectCommand);
|
|
14427
|
+
program2.addCommand(webhooksCommand);
|
|
12872
14428
|
program2.addCommand(experimentsCommand);
|
|
12873
14429
|
program2.addCommand(pushCommand);
|
|
12874
14430
|
program2.addCommand(previewCommand);
|
|
@@ -12877,7 +14433,11 @@ program2.addCommand(harnessCommand);
|
|
|
12877
14433
|
program2.addCommand(operatorCommand);
|
|
12878
14434
|
program2.addCommand(trackingCommand);
|
|
12879
14435
|
program2.addCommand(goalsCommand);
|
|
14436
|
+
program2.addCommand(funnelsCommand);
|
|
12880
14437
|
program2.addCommand(environmentsCommand);
|
|
14438
|
+
program2.addCommand(consentCommand);
|
|
14439
|
+
program2.addCommand(domainsCommand);
|
|
14440
|
+
program2.addCommand(handoverCommand);
|
|
12881
14441
|
program2.addCommand(analyticsCommand);
|
|
12882
14442
|
program2.addCommand(flagsCommand);
|
|
12883
14443
|
program2.addCommand(backlogCommand);
|