@drip-apex/apex-cli 0.2.0 → 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/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 path20 = require("node:path");
1163
- var fs24 = require("node:fs");
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 (fs24.existsSync(executableFile)) return;
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 = path20.resolve(baseDir, baseName);
2163
- if (fs24.existsSync(localBin)) return localBin;
2164
- if (sourceExt.includes(path20.extname(baseName))) return void 0;
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) => fs24.existsSync(`${localBin}${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 = fs24.realpathSync(this._scriptPath);
2178
+ resolvedScriptPath = fs25.realpathSync(this._scriptPath);
2179
2179
  } catch {
2180
2180
  resolvedScriptPath = this._scriptPath;
2181
2181
  }
2182
- executableDir = path20.resolve(
2183
- path20.dirname(resolvedScriptPath),
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 = path20.basename(
2190
+ const legacyName = path21.basename(
2191
2191
  this._scriptPath,
2192
- path20.extname(this._scriptPath)
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(path20.extname(executableFile));
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 = path20.basename(filename, path20.extname(filename));
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(path21) {
3065
- if (path21 === void 0) return this._executableDir;
3066
- this._executableDir = path21;
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 list12 = this.listeners[ev];
4592
- const i = list12.indexOf(fn);
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 && list12.length === 1) {
4597
- list12.length = 0;
4596
+ if (i === 0 && list15.length === 1) {
4597
+ list15.length = 0;
4598
4598
  } else {
4599
- list12.splice(i, 1);
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(action24, options) {
6747
- const actionIsFunction = typeof action24 === "function";
6748
- const actionIsPromise = typeof action24.then === "function";
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 ? action24(spinner) : action24;
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)
@@ -7090,7 +7090,7 @@ init_source();
7090
7090
 
7091
7091
  // src/commands/login.ts
7092
7092
  init_source();
7093
- var import_node_crypto = __toESM(require("node:crypto"));
7093
+ var import_node_crypto2 = __toESM(require("node:crypto"));
7094
7094
  var import_node_http = __toESM(require("node:http"));
7095
7095
  var import_node_os4 = __toESM(require("node:os"));
7096
7096
  var import_node_readline = __toESM(require("node:readline"));
@@ -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();
@@ -7356,8 +7361,11 @@ function findConfigPath() {
7356
7361
  }
7357
7362
 
7358
7363
  // src/api.ts
7364
+ var import_node_crypto = require("node:crypto");
7359
7365
  var mockEnvironmentPatchAttempts = 0;
7360
7366
  var mockEnvironmentReads = 0;
7367
+ var mockDomainsPatchAttempts = 0;
7368
+ var mockScreenshotPolls = 0;
7361
7369
  function extractErrorMessage(json, status2) {
7362
7370
  if (!json) return `HTTP ${status2}`;
7363
7371
  const err = json.error;
@@ -7389,6 +7397,13 @@ function extractErrorDetails(json) {
7389
7397
  return Object.keys(details).length > 0 ? details : void 0;
7390
7398
  }
7391
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
+ }] : [];
7392
7407
  return {
7393
7408
  id: "00000000-0000-4000-8000-000000000001",
7394
7409
  slug: "fixture-test",
@@ -7397,6 +7412,7 @@ function mockExperiment() {
7397
7412
  traffic_allocation: 1,
7398
7413
  store_url: "https://example.com/products/demo",
7399
7414
  created_at: "2026-07-04T00:00:00.000Z",
7415
+ archived_at: null,
7400
7416
  variations: [
7401
7417
  {
7402
7418
  id: "00000000-0000-4000-8000-0000000000c0",
@@ -7406,9 +7422,10 @@ function mockExperiment() {
7406
7422
  is_control: true,
7407
7423
  mutations: []
7408
7424
  },
7425
+ ...collidingVariation,
7409
7426
  {
7410
7427
  id: "00000000-0000-4000-8000-0000000000b1",
7411
- slug: "badge",
7428
+ slug: process.env.APEX_CLI_MOCK_SCREENSHOTS === "collision" ? void 0 : "badge",
7412
7429
  name: "Trust Badge",
7413
7430
  weight: 0.5,
7414
7431
  is_control: false,
@@ -7417,6 +7434,18 @@ function mockExperiment() {
7417
7434
  ]
7418
7435
  };
7419
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
+ }
7420
7449
  function mockGoal() {
7421
7450
  return {
7422
7451
  id: "00000000-0000-4000-8000-0000000000f1",
@@ -7426,11 +7455,24 @@ function mockGoal() {
7426
7455
  selector: null,
7427
7456
  text_match: null,
7428
7457
  url_pattern: null,
7458
+ page_id: null,
7429
7459
  event_name: "purchase",
7430
7460
  capture: null,
7431
7461
  created_at: "2026-07-04T00:00:00.000Z"
7432
7462
  };
7433
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
+ }
7434
7476
  function mockBrandContext() {
7435
7477
  if (process.env.APEX_CLI_MOCK_BRAND_PROFILE === "none") {
7436
7478
  return {
@@ -7480,10 +7522,213 @@ function mockBrandContext() {
7480
7522
  ].join("\n")
7481
7523
  };
7482
7524
  }
7483
- function mockResponse(method, path20, body5) {
7525
+ function mockResponse(method, path21, body5) {
7484
7526
  if (process.env.APEX_CLI_MOCK_API !== "1") return null;
7485
- const url = new URL(path20, "https://mock.apex.local");
7527
+ const url = new URL(path21, "https://mock.apex.local");
7486
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
+ }
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
+ }
7663
+ const fixtures = [
7664
+ ["experiment.json", '{"schemaVersion":1,"name":"Fixture Test"}\n'],
7665
+ ["targeting.json", '{"url":[],"segments":[]}\n'],
7666
+ ["experiment/index.js", "window.__apexExperiment = true;\n"],
7667
+ ["experiment/reset.js", "delete window.__apexExperiment;\n"],
7668
+ ["experiment/index.css", ".apex-experiment { display: block; }\n"],
7669
+ ["metrics.json", '{"primary":null,"secondary":[]}\n'],
7670
+ ["qa.json", '{"status":"pending"}\n'],
7671
+ ["launch.json", '{"status":"draft"}\n'],
7672
+ ["variations/control/variation.json", '{"slug":"control","isControl":true}\n'],
7673
+ ["variations/control/mutations.json", "[]\n"],
7674
+ ["variations/control/url-blocks.json", "[]\n"],
7675
+ ["variations/control/index.css", ".control { color: inherit; }\n"],
7676
+ ["variations/control/index.js", "window.__apexControl = true;\n"],
7677
+ ["variations/control/reset.js", "delete window.__apexControl;\n"],
7678
+ ["variations/control/anti-flicker.json", "[]\n"]
7679
+ ];
7680
+ const invalidPaths = {
7681
+ invalid: "../secret",
7682
+ traversal: "variations/../../x",
7683
+ absolute: "/abs/path"
7684
+ };
7685
+ const invalidPath = invalidPaths[process.env.APEX_CLI_MOCK_FILESYSTEM_PATHS ?? ""];
7686
+ if (invalidPath) {
7687
+ fixtures.push([invalidPath, "must not be written\n"]);
7688
+ }
7689
+ return {
7690
+ ok: true,
7691
+ status: 200,
7692
+ data: {
7693
+ rootName: "fixture-test",
7694
+ files: fixtures.map(([filePath, content]) => ({
7695
+ path: filePath,
7696
+ content,
7697
+ sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex")
7698
+ }))
7699
+ }
7700
+ };
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
+ }
7487
7732
  const configMatch = url.pathname.match(
7488
7733
  /^\/api\/v1\/(assets|landing-pages)(?:\/([^/]+))?$/
7489
7734
  );
@@ -7663,16 +7908,16 @@ function mockResponse(method, path20, body5) {
7663
7908
  /^\/api\/v1\/(flags|personalizations)(?:\/([^/]+))?(?:\/(activate|pause|archive|status))?$/
7664
7909
  );
7665
7910
  if (managedMatch) {
7666
- const [, kind5, id = `${kind5.slice(0, -1)}_1`, action24] = managedMatch;
7911
+ const [, kind5, id = `${kind5.slice(0, -1)}_1`, action68] = managedMatch;
7667
7912
  const input = body5 ?? {};
7668
7913
  const item = {
7669
7914
  id,
7670
7915
  key: String(input.key ?? `fixture-${kind5.slice(0, -1)}`),
7671
7916
  name: String(input.name ?? `Fixture ${kind5.slice(0, -1)}`),
7672
- status: action24 === "activate" ? "active" : action24 === "pause" ? "paused" : action24 === "archive" ? "archived" : "draft",
7917
+ status: action68 === "activate" ? "active" : action68 === "pause" ? "paused" : action68 === "archive" ? "archived" : "draft",
7673
7918
  updated_at: "2026-07-19T00:00:00.000Z"
7674
7919
  };
7675
- if (kind5 === "flags" && action24 === "status")
7920
+ if (kind5 === "flags" && action68 === "status")
7676
7921
  return {
7677
7922
  ok: true,
7678
7923
  status: 200,
@@ -7705,11 +7950,11 @@ function mockResponse(method, path20, body5) {
7705
7950
  /^\/api\/v1\/(backlog|signals)(?:\/([^/]+))?(?:\/(archive|restore))?$/
7706
7951
  );
7707
7952
  if (insightMatch) {
7708
- const [, kind5, id = `${kind5}_1`, action24] = insightMatch;
7953
+ const [, kind5, id = `${kind5}_1`, action68] = insightMatch;
7709
7954
  const input = body5 ?? {};
7710
- if (action24 && (kind5 !== "backlog" || method !== "POST"))
7711
- return { ok: false, status: 405, error: `${action24} requires POST` };
7712
- const archived = action24 === "archive" || method === "DELETE" || url.searchParams.get("include_archived") === "true";
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";
7713
7958
  const item = {
7714
7959
  id,
7715
7960
  ...kind5 === "backlog" ? {
@@ -7735,24 +7980,61 @@ function mockResponse(method, path20, body5) {
7735
7980
  return { ok: true, status: 200, data: { ok: true } };
7736
7981
  return {
7737
7982
  ok: true,
7738
- status: method === "POST" && !action24 ? 201 : 200,
7983
+ status: method === "POST" && !action68 ? 201 : 200,
7739
7984
  data: item
7740
7985
  };
7741
7986
  }
7742
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 };
7743
7989
  return {
7744
7990
  ok: true,
7745
7991
  status: 200,
7746
7992
  data: {
7747
- data: [exp],
7993
+ data: [experiment],
7748
7994
  pagination: { has_more: false, next_cursor: null }
7749
7995
  }
7750
7996
  };
7751
7997
  }
7998
+ if (method === "POST" && url.pathname === `/api/v1/experiments/${exp.id}/start`) {
7999
+ return { ok: true, status: 200, data: { ...exp, status: "running" } };
8000
+ }
8001
+ if (method === "POST" && url.pathname === "/api/v1/experiments") {
8002
+ const input = body5 ?? {};
8003
+ if (input.experiment_type === "redirect" && process.env.APEX_CLI_MOCK_REDIRECT_DISABLED === "1") {
8004
+ return {
8005
+ ok: false,
8006
+ status: 403,
8007
+ error: "Redirect tests are not enabled for this shop"
8008
+ };
8009
+ }
8010
+ const requestedVariations = Array.isArray(input.variations) ? input.variations : exp.variations ?? [];
8011
+ return {
8012
+ ok: true,
8013
+ status: 201,
8014
+ data: {
8015
+ ...exp,
8016
+ name: String(input.name ?? exp.name),
8017
+ slug: typeof input.slug === "string" || input.slug === null ? input.slug : exp.slug,
8018
+ experiment_type: input.experiment_type === "redirect" ? "redirect" : "mutation",
8019
+ traffic_allocation: typeof input.traffic_allocation === "number" ? input.traffic_allocation : exp.traffic_allocation,
8020
+ store_url: typeof input.store_url === "string" ? input.store_url : null,
8021
+ targeting: input.targeting ?? null,
8022
+ variations: requestedVariations.map((variation, index) => ({
8023
+ id: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
8024
+ name: String(variation.name ?? `Variation ${index + 1}`),
8025
+ weight: typeof variation.weight === "number" ? variation.weight : 0,
8026
+ is_control: variation.is_control === true,
8027
+ redirect_url: typeof variation.redirect_url === "string" ? variation.redirect_url : null,
8028
+ mutations: []
8029
+ }))
8030
+ }
8031
+ };
8032
+ }
7752
8033
  if (method === "GET" && (url.pathname === `/api/v1/experiments/${exp.id}` || url.pathname === `/api/v1/experiments/by-slug/${exp.slug}`)) {
7753
8034
  return { ok: true, status: 200, data: exp };
7754
8035
  }
7755
8036
  if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/results`) {
8037
+ const withheld = process.env.APEX_CLI_MOCK_RESULTS_STATE === "untrustworthy";
7756
8038
  return {
7757
8039
  ok: true,
7758
8040
  status: 200,
@@ -7780,14 +8062,62 @@ function mockResponse(method, path20, body5) {
7780
8062
  }
7781
8063
  ],
7782
8064
  decision: {
7783
- state: "collecting",
8065
+ state: withheld ? "untrustworthy" : "collecting",
7784
8066
  treatmentName: "Trust Badge",
7785
8067
  lift: 25,
7786
- 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
7787
8085
  },
7788
8086
  metrics: [],
7789
8087
  series: [],
7790
- 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
+ }
7791
8121
  }
7792
8122
  };
7793
8123
  }
@@ -7822,6 +8152,30 @@ function mockResponse(method, path20, body5) {
7822
8152
  }
7823
8153
  };
7824
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
+ }
8178
+ const observedPaths = files.map((file) => file.path).filter((filePath) => typeof filePath === "string");
7825
8179
  return {
7826
8180
  ok: true,
7827
8181
  status: 200,
@@ -7841,7 +8195,8 @@ function mockResponse(method, path20, body5) {
7841
8195
  errors: [],
7842
8196
  warnings: [],
7843
8197
  invalidatesQa: false,
7844
- precompileRequired: false
8198
+ precompileRequired: false,
8199
+ ...process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "echo-paths" ? { observedPaths } : {}
7845
8200
  }
7846
8201
  };
7847
8202
  }
@@ -7925,6 +8280,49 @@ function mockResponse(method, path20, body5) {
7925
8280
  }
7926
8281
  };
7927
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
+ }
7928
8326
  const analyticsMatch = url.pathname.match(/^\/api\/v1\/analytics\/(stats|timeseries|traffic|pages|geo|sessions|web-vitals|realtime|entry-exit)$/);
7929
8327
  if (method === "GET" && analyticsMatch) {
7930
8328
  const fixtures = {
@@ -7940,12 +8338,119 @@ function mockResponse(method, path20, body5) {
7940
8338
  };
7941
8339
  return { ok: true, status: 200, data: { data: fixtures[analyticsMatch[1]] } };
7942
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
+ }
7943
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
+ }
7944
8449
  return {
7945
8450
  ok: true,
7946
8451
  status: 200,
7947
8452
  data: {
7948
- data: [mockGoal()],
8453
+ data: [mockGoal(), mockPageScopedGoal()],
7949
8454
  pagination: { has_more: false, next_cursor: null }
7950
8455
  }
7951
8456
  };
@@ -8121,8 +8626,8 @@ function getUserKeyIdentity(creds2) {
8121
8626
  "/api/v1/me"
8122
8627
  );
8123
8628
  }
8124
- function userRequest(creds2, method, path20, body5) {
8125
- return request({ ...creds2, apiKey: creds2.userApiKey ?? "", baseUrl: creds2.userBaseUrl ?? creds2.baseUrl }, method, path20, body5);
8629
+ function userRequest(creds2, method, path21, body5) {
8630
+ return request({ ...creds2, apiKey: creds2.userApiKey ?? "", baseUrl: creds2.userBaseUrl ?? creds2.baseUrl }, method, path21, body5);
8126
8631
  }
8127
8632
  function getAccountProfile(creds2) {
8128
8633
  return userRequest(creds2, "GET", "/api/v1/account/profile");
@@ -8139,24 +8644,39 @@ function createUserManagedKey(creds2, body5) {
8139
8644
  function revokeUserManagedKey(creds2, id, shopId) {
8140
8645
  return userRequest(creds2, "DELETE", `/api/v1/keys/${encodeURIComponent(id)}?shop_id=${encodeURIComponent(shopId)}`);
8141
8646
  }
8142
- async function request(creds2, method, path20, body5) {
8143
- const mock = mockResponse(method, path20, body5);
8647
+ async function request(creds2, method, path21, body5, requestTimeoutMs) {
8648
+ const mock = mockResponse(method, path21, body5);
8144
8649
  if (mock) return mock;
8145
- const url = `${creds2.baseUrl}${path20}`;
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;
8146
8654
  const headers = {
8147
8655
  Authorization: `Bearer ${creds2.apiKey}`,
8148
8656
  "Content-Type": "application/json"
8149
8657
  };
8150
- const res = await fetch(url, {
8151
- method,
8152
- headers,
8153
- body: body5 ? JSON.stringify(body5) : void 0
8154
- });
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
+ }
8155
8674
  const json = await res.json().catch(() => null);
8156
8675
  if (!res.ok) {
8157
8676
  return {
8158
8677
  ok: false,
8159
8678
  status: res.status,
8679
+ data: json,
8160
8680
  error: extractErrorMessage(json, res.status),
8161
8681
  errorDetails: extractErrorDetails(json),
8162
8682
  requestId: res.headers.get("x-request-id"),
@@ -8175,15 +8695,28 @@ async function request(creds2, method, path20, body5) {
8175
8695
  data: json
8176
8696
  };
8177
8697
  }
8178
- async function requestForm(creds2, method, path20, body5) {
8179
- const mock = mockResponse(method, path20, body5);
8698
+ async function requestForm(creds2, method, path21, body5) {
8699
+ const mock = mockResponse(method, path21, body5);
8180
8700
  if (mock) return mock;
8181
- const url = `${creds2.baseUrl}${path20}`;
8182
- const res = await fetch(url, {
8183
- method,
8184
- headers: { Authorization: `Bearer ${creds2.apiKey}` },
8185
- body: body5
8186
- });
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
+ }
8187
8720
  const json = await res.json().catch(() => null);
8188
8721
  if (!res.ok) {
8189
8722
  return {
@@ -8206,6 +8739,7 @@ function listExperiments(creds2, opts) {
8206
8739
  if (opts?.status) params.set("status", opts.status);
8207
8740
  if (opts?.limit) params.set("limit", String(opts.limit));
8208
8741
  if (opts?.cursor) params.set("cursor", opts.cursor);
8742
+ if (opts?.includeArchived) params.set("archived", "1");
8209
8743
  const qs = params.toString();
8210
8744
  return request(
8211
8745
  creds2,
@@ -8267,6 +8801,15 @@ function getExperimentResults(creds2, id, opts) {
8267
8801
  `/api/v1/experiments/${id}/results${qs ? `?${qs}` : ""}`
8268
8802
  );
8269
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
+ }
8270
8813
  function planExperimentFilesystem(creds2, id, body5) {
8271
8814
  return request(
8272
8815
  creds2,
@@ -8343,13 +8886,32 @@ function putOperatorWorkspaceFile(creds2, experimentId, variationId, filePath, b
8343
8886
  body5
8344
8887
  );
8345
8888
  }
8346
- function buildOperatorWorkspace(creds2, experimentId, variationId) {
8889
+ function buildOperatorWorkspace(creds2, experimentId, variationId, body5) {
8347
8890
  return request(
8348
8891
  creds2,
8349
8892
  "POST",
8350
- `/api/v1/experiments/${experimentId}/variations/${variationId}/build`
8893
+ `/api/v1/experiments/${experimentId}/variations/${variationId}/build`,
8894
+ body5
8351
8895
  );
8352
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
+ }
8353
8915
  function listGoals(creds2, opts) {
8354
8916
  const params = new URLSearchParams();
8355
8917
  if (opts?.limit) params.set("limit", String(opts.limit));
@@ -8368,6 +8930,14 @@ function updateGoal(creds2, id, body5) {
8368
8930
  body5
8369
8931
  );
8370
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
+ }
8371
8941
  function getBrandContext(creds2) {
8372
8942
  return request(creds2, "GET", "/api/v1/brand-context");
8373
8943
  }
@@ -8382,6 +8952,24 @@ function updateEnvironments(creds2, body5) {
8382
8952
  body5
8383
8953
  );
8384
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
+ }
8385
8973
  function verifyTracking(creds2, opts) {
8386
8974
  const params = new URLSearchParams();
8387
8975
  if (opts?.minutes) params.set("minutes", String(opts.minutes));
@@ -8447,7 +9035,22 @@ function listAuditLogs(creds2, opts) {
8447
9035
  if (opts.entityId) p.set("entity_id", opts.entityId);
8448
9036
  if (opts.limit) p.set("limit", String(opts.limit));
8449
9037
  if (opts.offset) p.set("offset", String(opts.offset));
8450
- return request(creds2, "GET", `/api/v1/audit-logs${p.size ? `?${p}` : ""}`);
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
+ });
8451
9054
  }
8452
9055
  function listInbox(creds2, experimentId) {
8453
9056
  const q = experimentId ? `?experiment_id=${encodeURIComponent(experimentId)}` : "";
@@ -8494,11 +9097,11 @@ function requestTargetingResource(creds2, method, kind5, id, body5, limit, curso
8494
9097
  if (limit) params.set("limit", String(limit));
8495
9098
  if (cursor) params.set("cursor", cursor);
8496
9099
  const query = params.toString();
8497
- const path20 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8498
- return request(creds2, method, path20, body5);
9100
+ const path21 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
9101
+ return request(creds2, method, path21, body5);
8499
9102
  }
8500
- function managedPath(kind5, id, action24) {
8501
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action24 ? `/${action24}` : ""}`;
9103
+ function managedPath(kind5, id, action68) {
9104
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action68 ? `/${action68}` : ""}`;
8502
9105
  }
8503
9106
  function listManagedDeliveries(creds2, kind5, limit) {
8504
9107
  const q = limit ? `?limit=${limit}` : "";
@@ -8520,8 +9123,8 @@ function createManagedDelivery(creds2, kind5, body5) {
8520
9123
  function updateManagedDelivery(creds2, kind5, id, body5) {
8521
9124
  return request(creds2, "PATCH", managedPath(kind5, id), body5);
8522
9125
  }
8523
- function transitionManagedDelivery(creds2, kind5, id, action24) {
8524
- return request(creds2, "POST", managedPath(kind5, id, action24));
9126
+ function transitionManagedDelivery(creds2, kind5, id, action68) {
9127
+ return request(creds2, "POST", managedPath(kind5, id, action68));
8525
9128
  }
8526
9129
  function listConfigResources(creds2, family, opts) {
8527
9130
  const params = new URLSearchParams();
@@ -8689,6 +9292,24 @@ var postShopArea = (c, id, area, body5) => userRequest(
8689
9292
  `/api/v1/shops/${id}/${area}`,
8690
9293
  body5
8691
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
+ }
8692
9313
 
8693
9314
  // src/ui.ts
8694
9315
  init_source();
@@ -8761,8 +9382,8 @@ function prompt(question, hide = false) {
8761
9382
  }
8762
9383
  });
8763
9384
  }
8764
- function printValidationFailure(res) {
8765
- 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}`);
8766
9387
  if (res.status === 429) {
8767
9388
  printWarning(
8768
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."
@@ -8784,7 +9405,7 @@ function parseFormBody(body5) {
8784
9405
  return new URLSearchParams(body5);
8785
9406
  }
8786
9407
  async function waitForBrowserLogin(baseUrl, userMode = false) {
8787
- const state = import_node_crypto.default.randomBytes(24).toString("base64url");
9408
+ const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
8788
9409
  const clientName = import_node_os4.default.hostname() || "Local machine";
8789
9410
  return new Promise((resolve, reject) => {
8790
9411
  let settled = false;
@@ -8947,7 +9568,7 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
8947
9568
  spinner.fail(
8948
9569
  res.status === 401 ? "Authentication failed" : "API key validation failed"
8949
9570
  );
8950
- printValidationFailure(res);
9571
+ printValidationFailure(res, " Run `apex login`.");
8951
9572
  fail(this, res.error ?? "API key validation failed", classifyStatus(res.status), res.errorDetails);
8952
9573
  }
8953
9574
  spinner.succeed("Authenticated successfully");
@@ -8955,7 +9576,13 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
8955
9576
  output(this, { baseUrl, credentialsFile: "~/.drip/credentials.json" }, () => {
8956
9577
  printSuccess(`Credentials saved to ~/.drip/credentials.json`);
8957
9578
  });
8958
- });
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
+ `);
8959
9586
  var logoutCommand = new Command("logout").description("Remove stored credentials").option("--user", "Remove only the stored user API key").action(function action2(opts) {
8960
9587
  const existing = getStoredCredentials();
8961
9588
  if (!existing) {
@@ -8998,7 +9625,7 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
8998
9625
  if (res?.ok) {
8999
9626
  printSuccess("Key is valid");
9000
9627
  } else {
9001
- if (res) printValidationFailure(res);
9628
+ if (res) printValidationFailure(res, " Run `apex login`.");
9002
9629
  }
9003
9630
  } else {
9004
9631
  console.log("API Key: none (user credential only)");
@@ -9006,14 +9633,14 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
9006
9633
  if (creds2.userApiKey) {
9007
9634
  console.log(`User Key: ${source_default.dim(creds2.userApiKey.slice(0, 11) + "..." + creds2.userApiKey.slice(-4))}`);
9008
9635
  if (userRes?.ok) console.log(`User Org: ${source_default.cyan(userRes.data.data.org_id)} (${userRes.data.data.org_role})`);
9009
- else printWarning("User key could not be verified.");
9636
+ else printWarning("User key could not be verified. Run `apex login --user`.");
9010
9637
  }
9011
9638
  });
9012
9639
  });
9013
9640
 
9014
9641
  // src/commands/experiments.ts
9015
9642
  init_source();
9016
- var import_node_crypto2 = __toESM(require("node:crypto"));
9643
+ var import_node_crypto3 = __toESM(require("node:crypto"));
9017
9644
  var import_node_fs6 = __toESM(require("node:fs"));
9018
9645
  var import_node_path4 = __toESM(require("node:path"));
9019
9646
  function formatDate(iso) {
@@ -9045,22 +9672,40 @@ function printExperiment(exp) {
9045
9672
  function formatPct(value, digits = 1) {
9046
9673
  return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}%` : "-";
9047
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
+ }
9048
9683
  function printExperimentResults(results2) {
9049
9684
  const experiment = results2.experiment ?? {};
9050
9685
  const totals = results2.totals ?? {};
9051
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";
9052
9692
  console.log();
9053
9693
  console.log(source_default.bold(String(experiment.name ?? experiment.id ?? "Experiment results")));
9054
9694
  console.log(` Status: ${statusBadge(String(experiment.status ?? "unknown"))}`);
9055
9695
  console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
9056
9696
  console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
9057
9697
  console.log(` Revenue: ${String(totals.revenue ?? 0)}`);
9058
- console.log(` Decision: ${String(decision.state ?? "unknown")}`);
9698
+ console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
9699
+ console.log(
9700
+ ` Statistics: ${statisticsStateLabel(statisticsState)}`
9701
+ );
9059
9702
  if (decision.treatmentName) {
9060
9703
  console.log(` Treatment: ${String(decision.treatmentName)}`);
9061
9704
  }
9062
- console.log(` Lift: ${formatPct(decision.lift)}`);
9063
- console.log(` Confidence: ${formatPct(decision.sequentialConfidence)}`);
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
+ );
9064
9709
  const arms = Array.isArray(results2.arms) ? results2.arms : [];
9065
9710
  if (arms.length > 0) {
9066
9711
  console.log();
@@ -9084,9 +9729,26 @@ function printExperimentResults(results2) {
9084
9729
  }
9085
9730
  console.log();
9086
9731
  }
9087
- var FILESYSTEM_FILE_RE = /^(experiment\.json|targeting\.json|metrics\.json|qa\.json|launch\.json|variations\/[^/]+\/(?:variation\.json|mutations\.json|index\.css|index\.js|reset\.js|anti-flicker\.json))$/;
9732
+ var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
9733
+ topLevel: ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"],
9734
+ experiment: ["index.css", "index.js", "reset.js"],
9735
+ variation: [
9736
+ "variation.json",
9737
+ "mutations.json",
9738
+ "url-blocks.json",
9739
+ "index.css",
9740
+ "index.js",
9741
+ "reset.js",
9742
+ "anti-flicker.json"
9743
+ ]
9744
+ };
9745
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9746
+ var pathAlternation = (paths) => paths.map(escapeRegex).join("|");
9747
+ var FILESYSTEM_FILE_RE = new RegExp(
9748
+ `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
9749
+ );
9088
9750
  function sha256Text(value) {
9089
- return import_node_crypto2.default.createHash("sha256").update(value).digest("hex");
9751
+ return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
9090
9752
  }
9091
9753
  function folderPath(folder) {
9092
9754
  return import_node_path4.default.resolve(process.cwd(), folder);
@@ -9114,6 +9776,10 @@ function writeFilesystemSession(folder, session) {
9114
9776
  function readFilesystemSession(folder) {
9115
9777
  const filePath = filesystemSessionPath(folder);
9116
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
+ }
9117
9783
  throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
9118
9784
  }
9119
9785
  return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
@@ -9126,8 +9792,12 @@ function validateFilesystemPath(relativePath) {
9126
9792
  return normalized;
9127
9793
  }
9128
9794
  function writeExperimentFilesystemFiles(folder, files) {
9129
- for (const file of files) {
9130
- const relativePath = validateFilesystemPath(file.path);
9795
+ const validatedFiles = files.map((file) => ({
9796
+ ...file,
9797
+ relativePath: validateFilesystemPath(file.path)
9798
+ }));
9799
+ for (const file of validatedFiles) {
9800
+ const relativePath = file.relativePath;
9131
9801
  const absPath = import_node_path4.default.join(folder, relativePath);
9132
9802
  import_node_fs6.default.mkdirSync(import_node_path4.default.dirname(absPath), { recursive: true });
9133
9803
  import_node_fs6.default.writeFileSync(absPath, file.content, "utf8");
@@ -9135,26 +9805,25 @@ function writeExperimentFilesystemFiles(folder, files) {
9135
9805
  }
9136
9806
  function listExperimentFilesystemFiles(folder) {
9137
9807
  const files = [];
9138
- const topLevel = ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"];
9139
- for (const fileName of topLevel) {
9808
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel) {
9140
9809
  const absPath = import_node_path4.default.join(folder, fileName);
9141
9810
  if (!import_node_fs6.default.existsSync(absPath)) continue;
9142
9811
  const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9143
9812
  files.push({ path: fileName, content, sha256: sha256Text(content) });
9144
9813
  }
9814
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
9815
+ const relativePath = `experiment/${fileName}`;
9816
+ const absPath = import_node_path4.default.join(folder, relativePath);
9817
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
9818
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9819
+ files.push({ path: relativePath, content, sha256: sha256Text(content) });
9820
+ }
9145
9821
  const variationsRoot = import_node_path4.default.join(folder, "variations");
9146
9822
  if (import_node_fs6.default.existsSync(variationsRoot)) {
9147
9823
  for (const variationDir of import_node_fs6.default.readdirSync(variationsRoot, { withFileTypes: true })) {
9148
9824
  if (!variationDir.isDirectory()) continue;
9149
9825
  const base = import_node_path4.default.join(variationsRoot, variationDir.name);
9150
- for (const fileName of [
9151
- "variation.json",
9152
- "mutations.json",
9153
- "index.css",
9154
- "index.js",
9155
- "reset.js",
9156
- "anti-flicker.json"
9157
- ]) {
9826
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation) {
9158
9827
  const absPath = import_node_path4.default.join(base, fileName);
9159
9828
  if (!import_node_fs6.default.existsSync(absPath)) continue;
9160
9829
  const relativePath = validateFilesystemPath(
@@ -9176,47 +9845,50 @@ function printFilesystemPlan(plan) {
9176
9845
  console.log(source_default.yellow("\nWarnings"));
9177
9846
  plan.warnings.forEach((warning) => console.log(` - ${warning}`));
9178
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
+ }
9179
9855
  console.log(source_default.bold("\nChanges"));
9180
9856
  if (plan.changes.length === 0) {
9181
9857
  console.log(source_default.dim(" No changes."));
9182
9858
  } else {
9183
9859
  plan.changes.forEach((change) => {
9184
9860
  const pathLabel = change.path ? source_default.dim(` ${change.path}`) : "";
9185
- const qa2 = change.invalidatesQa ? source_default.yellow(" invalidates QA") : "";
9186
- console.log(` ${source_default.cyan(change.action)} ${change.entity}${pathLabel} \u2014 ${change.summary}${qa2}`);
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}`);
9187
9863
  });
9188
9864
  }
9189
9865
  console.log();
9190
9866
  console.log(`QA invalidated: ${plan.invalidatesQa ? source_default.yellow("yes") : "no"}`);
9191
9867
  console.log(`Precompile: ${plan.precompileRequired ? "yes" : "no"}`);
9192
9868
  }
9193
- function printQaRun(run2, json = false) {
9194
- if (json) {
9195
- console.log(JSON.stringify({ run: run2 }, null, 2));
9196
- return;
9197
- }
9869
+ function printQaRun(run3) {
9198
9870
  console.log();
9199
- console.log(source_default.bold(`QA run ${run2.id}`));
9200
- console.log(` Status: ${statusBadge(run2.summary.status)}`);
9871
+ console.log(source_default.bold(`QA run ${run3.id}`));
9872
+ console.log(` Status: ${statusBadge(run3.summary.status)}`);
9201
9873
  console.log(
9202
- ` Required: ${run2.summary.completedRequired}/${run2.summary.required}`
9874
+ ` Required: ${run3.summary.completedRequired}/${run3.summary.required}`
9203
9875
  );
9204
- if (run2.approvedAt) console.log(` Approved: ${run2.approvedAt}`);
9205
- if (run2.completedAt) console.log(` Complete: ${run2.completedAt}`);
9206
- if (run2.summary.blockers.length > 0) {
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) {
9207
9879
  console.log(source_default.yellow("\nBlockers"));
9208
- run2.summary.blockers.forEach((blocker) => console.log(` - ${blocker}`));
9880
+ run3.summary.blockers.forEach((blocker) => console.log(` - ${blocker}`));
9209
9881
  }
9210
9882
  console.log(source_default.bold("\nChecklist"));
9211
- for (const item of run2.checklist) {
9883
+ for (const item of run3.checklist) {
9212
9884
  const required = item.required ? source_default.yellow(" required") : "";
9213
9885
  const notes = item.notes ? source_default.dim(` \u2014 ${item.notes}`) : "";
9214
9886
  console.log(` ${statusBadge(item.status)} ${item.id}${required}`);
9215
9887
  console.log(` ${item.title}${notes}`);
9216
9888
  }
9217
- if (run2.urls.length > 0) {
9889
+ if (run3.urls.length > 0) {
9218
9890
  console.log(source_default.bold("\nQA URLs"));
9219
- run2.urls.forEach((url) => {
9891
+ run3.urls.forEach((url) => {
9220
9892
  console.log(` - ${url.label}: ${source_default.cyan(url.url)}`);
9221
9893
  });
9222
9894
  }
@@ -9270,18 +9942,21 @@ async function resolveExperimentForFilesystemOrId(value) {
9270
9942
  }
9271
9943
  async function refreshExperimentFilesystemFolderIfNeeded(creds2, folder, experimentId) {
9272
9944
  if (!folder) return;
9273
- const refreshed = await getExperimentFilesystem(creds2, experimentId);
9274
- if (refreshed.ok && refreshed.data) {
9275
- writeExperimentFilesystemFiles(folder, refreshed.data.files);
9276
- writeFilesystemSession(folder, {
9277
- schemaVersion: 1,
9278
- baseUrl: creds2.baseUrl,
9279
- experimentId,
9280
- pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
9281
- files: Object.fromEntries(
9282
- refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
9283
- )
9284
- });
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 {
9285
9960
  }
9286
9961
  }
9287
9962
  async function resolveExperiment(idOrSlug) {
@@ -9289,20 +9964,29 @@ async function resolveExperiment(idOrSlug) {
9289
9964
  if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
9290
9965
  const res2 = await getExperiment(creds2, idOrSlug);
9291
9966
  if (res2.ok) return res2.data;
9292
- return null;
9967
+ throw new CliError(
9968
+ res2.error ?? `Experiment "${idOrSlug}" not found`,
9969
+ classifyStatus(res2.status),
9970
+ res2.errorDetails
9971
+ );
9293
9972
  }
9294
9973
  const slug = idOrSlug.startsWith("slug:") ? idOrSlug.slice(5) : idOrSlug;
9295
9974
  const res = await getExperimentBySlug(creds2, slug);
9296
9975
  if (res.ok) return res.data;
9297
- return null;
9976
+ throw new CliError(
9977
+ res.error ?? `Experiment "${idOrSlug}" not found`,
9978
+ classifyStatus(res.status),
9979
+ res.errorDetails
9980
+ );
9298
9981
  }
9299
- 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) {
9300
9983
  const creds2 = requireCredentials();
9301
9984
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9302
9985
  const spinner = process.stderr.isTTY ? ora2("Fetching experiments...").start() : null;
9303
9986
  const res = await listExperiments(creds2, {
9304
9987
  status: opts.status,
9305
- limit: parseInt(opts.limit, 10)
9988
+ limit: parseInt(opts.limit, 10),
9989
+ includeArchived: opts.includeArchived
9306
9990
  });
9307
9991
  if (!res.ok) {
9308
9992
  spinner?.fail("Failed to fetch experiments");
@@ -9316,15 +10000,16 @@ var list = new Command("list").alias("ls").description("List experiments").optio
9316
10000
  return;
9317
10001
  }
9318
10002
  printTable(
9319
- ["NAME", "SLUG", "STATUS", "TRAFFIC", "CREATED"],
10003
+ ["NAME", "SLUG", "STATUS", "ARCHIVED", "TRAFFIC", "CREATED"],
9320
10004
  experiments.map((e) => [
9321
10005
  e.name.slice(0, 40),
9322
10006
  e.slug ?? source_default.dim("-"),
9323
10007
  statusBadge(e.status),
10008
+ e.archived_at ? formatDate(e.archived_at) : source_default.dim("-"),
9324
10009
  `${(e.traffic_allocation * 100).toFixed(0)}%`,
9325
10010
  formatDate(e.created_at)
9326
10011
  ]),
9327
- [40, 25, 12, 8, 15]
10012
+ [40, 25, 12, 15, 8, 15]
9328
10013
  );
9329
10014
  if (res.data.pagination.has_more) {
9330
10015
  console.log(source_default.dim(`
@@ -9365,84 +10050,195 @@ var results = new Command("results").description("Read experiment results and ti
9365
10050
  fail(this, error instanceof Error ? error.message : String(error));
9366
10051
  }
9367
10052
  });
9368
- var create = new Command("create").description("Create a new experiment").requiredOption("--name <name>", "Experiment name").option("--slug <slug>", "URL-safe slug for GitOps").option("--description <desc>", "Description").option("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async (opts) => {
10053
+ function collectCreateOption(value, previous) {
10054
+ return [...previous, value];
10055
+ }
10056
+ function parseHttpDestination(value) {
10057
+ const trimmed = value.trim();
10058
+ try {
10059
+ const url = new URL(trimmed);
10060
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error();
10061
+ } catch {
10062
+ throw new CliError(
10063
+ `Invalid redirect destination "${value}": expected an HTTP(S) URL.`,
10064
+ EXIT_CODES.validation
10065
+ );
10066
+ }
10067
+ return trimmed;
10068
+ }
10069
+ function sourceStoreUrl(sourceRules) {
10070
+ for (const rule of sourceRules) {
10071
+ if (rule.includes("*")) continue;
10072
+ try {
10073
+ const url = new URL(rule);
10074
+ if (url.protocol === "http:" || url.protocol === "https:") {
10075
+ return rule;
10076
+ }
10077
+ } catch {
10078
+ }
10079
+ }
10080
+ return null;
10081
+ }
10082
+ var create = new Command("create").description("Create a new experiment").requiredOption("--name <name>", "Experiment name").option("--slug <slug>", "URL-safe slug for GitOps").option("--description <desc>", "Description").option("--type <type>", "Experiment type: mutation or redirect", "mutation").option(
10083
+ "--source-url <pattern>",
10084
+ "Positive source URL rule (repeatable; accepts exact URLs, paths, or wildcards)",
10085
+ collectCreateOption,
10086
+ []
10087
+ ).option(
10088
+ "--destination <url>",
10089
+ "Redirect treatment destination (repeatable; creates one treatment per URL)",
10090
+ collectCreateOption,
10091
+ []
10092
+ ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async function action7(opts) {
10093
+ if (opts.type !== "mutation" && opts.type !== "redirect") {
10094
+ fail(
10095
+ this,
10096
+ "--type must be mutation or redirect",
10097
+ EXIT_CODES.validation
10098
+ );
10099
+ }
10100
+ const traffic = Number(opts.traffic);
10101
+ if (!Number.isFinite(traffic) || traffic < 0 || traffic > 100) {
10102
+ fail(
10103
+ this,
10104
+ "--traffic must be a number from 0 to 100",
10105
+ EXIT_CODES.validation
10106
+ );
10107
+ }
10108
+ const sourceRules = opts.sourceUrl.map((value) => value.trim());
10109
+ if (sourceRules.some((value) => value.length === 0)) {
10110
+ fail(this, "--source-url cannot be empty", EXIT_CODES.validation);
10111
+ }
10112
+ if (opts.type !== "redirect" && opts.destination.length > 0) {
10113
+ fail(
10114
+ this,
10115
+ "--destination is only valid with --type redirect",
10116
+ EXIT_CODES.validation
10117
+ );
10118
+ }
10119
+ if (opts.type === "redirect" && sourceRules.length === 0) {
10120
+ fail(
10121
+ this,
10122
+ "Redirect experiments require at least one --source-url rule",
10123
+ EXIT_CODES.validation
10124
+ );
10125
+ }
10126
+ if (opts.type === "redirect" && opts.destination.length === 0) {
10127
+ fail(
10128
+ this,
10129
+ "Redirect experiments require at least one --destination URL",
10130
+ EXIT_CODES.validation
10131
+ );
10132
+ }
10133
+ const destinations = opts.destination.map(parseHttpDestination);
10134
+ const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
9369
10135
  const creds2 = requireCredentials();
9370
10136
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9371
- const spinner = ora2("Creating experiment...").start();
10137
+ const spinner = process.stderr.isTTY ? ora2("Creating experiment...").start() : null;
9372
10138
  const res = await createExperiment(creds2, {
9373
10139
  name: opts.name,
9374
10140
  slug: opts.slug ?? null,
9375
10141
  description: opts.description ?? null,
9376
- traffic_allocation: parseInt(opts.traffic, 10) / 100
10142
+ experiment_type: opts.type,
10143
+ traffic_allocation: traffic / 100,
10144
+ ...sourceRules.length > 0 ? {
10145
+ store_url: sourceStoreUrl(sourceRules),
10146
+ targeting: {
10147
+ url: sourceRules.map((pattern) => ({
10148
+ type: "simple",
10149
+ pattern,
10150
+ include: true
10151
+ }))
10152
+ }
10153
+ } : {},
10154
+ ...armWeight !== null ? {
10155
+ variations: [
10156
+ {
10157
+ name: "Control",
10158
+ weight: armWeight,
10159
+ is_control: true
10160
+ },
10161
+ ...destinations.map((redirectUrl, index) => ({
10162
+ name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
10163
+ weight: armWeight,
10164
+ is_control: false,
10165
+ redirect_url: redirectUrl
10166
+ }))
10167
+ ]
10168
+ } : {}
9377
10169
  });
9378
- if (!res.ok) {
9379
- spinner.fail("Failed to create experiment");
9380
- printError(res.error);
9381
- process.exit(1);
10170
+ if (!res.ok || !res.data) {
10171
+ spinner?.fail("Failed to create experiment");
10172
+ fail(
10173
+ this,
10174
+ res.error ?? "Failed to create experiment",
10175
+ classifyStatus(res.status),
10176
+ res.errorDetails
10177
+ );
9382
10178
  }
9383
- spinner.succeed("Experiment created");
9384
- printExperiment(res.data);
10179
+ spinner?.succeed("Experiment created");
10180
+ output(this, { experiment: res.data }, () => printExperiment(res.data));
9385
10181
  });
9386
- 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) {
9387
10183
  const creds2 = requireCredentials();
9388
10184
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9389
10185
  const spinner = ora2("Starting experiment...").start();
9390
10186
  const exp = await resolveExperiment(idOrSlug);
9391
10187
  if (!exp) {
9392
10188
  spinner.fail(`Experiment "${idOrSlug}" not found`);
9393
- process.exit(1);
10189
+ fail(this, `Experiment "${idOrSlug}" not found`);
9394
10190
  }
9395
10191
  const res = await startExperiment(creds2, exp.id);
9396
10192
  if (!res.ok) {
9397
10193
  spinner.fail("Failed to start experiment");
9398
- printError(res.error);
9399
- process.exit(1);
10194
+ fail(this, res.error ?? "Failed to start experiment", classifyStatus(res.status), res.errorDetails);
9400
10195
  }
9401
- spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("running")}`);
10196
+ output(this, { experiment: { id: exp.id, name: exp.name, status: "running" } }, () => {
10197
+ spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("running")}`);
10198
+ });
9402
10199
  });
9403
- 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) {
9404
10201
  const creds2 = requireCredentials();
9405
10202
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9406
10203
  const spinner = ora2("Pausing experiment...").start();
9407
10204
  const exp = await resolveExperiment(idOrSlug);
9408
10205
  if (!exp) {
9409
10206
  spinner.fail(`Experiment "${idOrSlug}" not found`);
9410
- process.exit(1);
10207
+ fail(this, `Experiment "${idOrSlug}" not found`);
9411
10208
  }
9412
10209
  const res = await pauseExperiment(creds2, exp.id);
9413
10210
  if (!res.ok) {
9414
10211
  spinner.fail("Failed to pause experiment");
9415
- printError(res.error);
9416
- process.exit(1);
10212
+ fail(this, res.error ?? "Failed to pause experiment", classifyStatus(res.status), res.errorDetails);
9417
10213
  }
9418
- spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
10214
+ output(this, { experiment: { id: exp.id, name: exp.name, status: "paused" } }, () => {
10215
+ spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
10216
+ });
9419
10217
  });
9420
- 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) {
9421
10219
  const creds2 = requireCredentials();
9422
10220
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9423
10221
  const spinner = ora2("Completing experiment...").start();
9424
10222
  const exp = await resolveExperiment(idOrSlug);
9425
10223
  if (!exp) {
9426
10224
  spinner.fail(`Experiment "${idOrSlug}" not found`);
9427
- process.exit(1);
10225
+ fail(this, `Experiment "${idOrSlug}" not found`);
9428
10226
  }
9429
10227
  const res = await completeExperiment(creds2, exp.id);
9430
10228
  if (!res.ok) {
9431
10229
  spinner.fail("Failed to complete experiment");
9432
- printError(res.error);
9433
- process.exit(1);
10230
+ fail(this, res.error ?? "Failed to complete experiment", classifyStatus(res.status), res.errorDetails);
9434
10231
  }
9435
- spinner.succeed(
9436
- `Experiment "${exp.name}" is now ${statusBadge("completed")}`
9437
- );
10232
+ output(this, { experiment: { id: exp.id, name: exp.name, status: "completed" } }, () => {
10233
+ spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
10234
+ });
9438
10235
  });
9439
- 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 action7(idOrSlug, opts) {
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) {
9440
10237
  const creds2 = requireCredentials();
9441
10238
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9442
10239
  const exp = await resolveExperiment(idOrSlug);
9443
10240
  if (!exp) {
9444
- printError(`Experiment "${idOrSlug}" not found.`);
9445
- process.exit(1);
10241
+ fail(this, `Experiment "${idOrSlug}" not found.`);
9446
10242
  }
9447
10243
  if (!opts.force) {
9448
10244
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -9475,22 +10271,20 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9475
10271
  const res = await deleteExperiment(creds2, exp.id);
9476
10272
  if (!res.ok) {
9477
10273
  spinner.fail("Failed to delete experiment");
9478
- printError(res.error);
9479
- process.exit(1);
10274
+ fail(this, res.error ?? "Failed to delete experiment", classifyStatus(res.status), res.errorDetails);
9480
10275
  }
9481
- spinner.succeed(`Experiment "${exp.name}" deleted.`);
10276
+ output(this, { experiment: { id: exp.id, name: exp.name }, deleted: true }, () => spinner.succeed(`Experiment "${exp.name}" deleted.`));
9482
10277
  });
9483
- var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--dir <folder>", "Target folder").action(async (idOrSlug, opts) => {
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) {
9484
10279
  const creds2 = requireCredentials();
9485
10280
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9486
10281
  const spinner = ora2("Pulling experiment filesystem...").start();
9487
10282
  try {
9488
10283
  const exp = await resolveExperiment(idOrSlug);
9489
- if (!exp) throw new Error(`Experiment "${idOrSlug}" not found.`);
10284
+ if (!exp) throw new CliError(`Experiment "${idOrSlug}" not found.`);
9490
10285
  const res = await getExperimentFilesystem(creds2, exp.id);
9491
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to pull experiment filesystem");
9492
- const folder = folderPath(opts.dir ?? res.data.rootName);
9493
- import_node_fs6.default.mkdirSync(folder, { recursive: true });
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);
9494
10288
  writeExperimentFilesystemFiles(folder, res.data.files);
9495
10289
  writeFilesystemSession(folder, {
9496
10290
  schemaVersion: 1,
@@ -9501,15 +10295,16 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9501
10295
  res.data.files.map((file) => [file.path, { sha256: file.sha256 }])
9502
10296
  )
9503
10297
  });
9504
- spinner.succeed(`Pulled ${exp.name}`);
9505
- console.log(source_default.cyan(folder));
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
+ });
9506
10302
  } catch (error) {
9507
10303
  spinner.fail("Failed to pull experiment filesystem");
9508
- printError(error instanceof Error ? error.message : String(error));
9509
- process.exit(1);
10304
+ throw error;
9510
10305
  }
9511
10306
  });
9512
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action8(folderArg) {
10307
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action13(folderArg) {
9513
10308
  const creds2 = requireCredentials();
9514
10309
  try {
9515
10310
  const folder = folderPath(folderArg);
@@ -9520,7 +10315,9 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9520
10315
  fail(this, res.error ?? "Failed to plan experiment filesystem", classifyStatus(res.status), res.errorDetails);
9521
10316
  }
9522
10317
  if (!res.data.ok) {
9523
- fail(this, "Experiment filesystem plan has errors.", EXIT_CODES.validation, {
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, {
9524
10321
  errors: res.data.errors,
9525
10322
  plan: res.data
9526
10323
  });
@@ -9531,7 +10328,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9531
10328
  fail(this, error instanceof Error ? error.message : String(error));
9532
10329
  }
9533
10330
  });
9534
- 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 action9(folderArg, opts) {
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) {
9535
10332
  const creds2 = requireCredentials();
9536
10333
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9537
10334
  try {
@@ -9543,7 +10340,9 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9543
10340
  fail(this, planRes.error ?? "Failed to plan experiment filesystem", classifyStatus(planRes.status), planRes.errorDetails);
9544
10341
  }
9545
10342
  if (!planRes.data.ok) {
9546
- fail(this, "Experiment filesystem plan has errors.", EXIT_CODES.validation, {
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, {
9547
10346
  errors: planRes.data.errors,
9548
10347
  plan: planRes.data
9549
10348
  });
@@ -9582,19 +10381,11 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9582
10381
  });
9583
10382
  }
9584
10383
  spinner?.succeed("Applied experiment filesystem");
9585
- const refreshed = await getExperimentFilesystem(creds2, session.experimentId);
9586
- if (refreshed.ok && refreshed.data) {
9587
- writeExperimentFilesystemFiles(folder, refreshed.data.files);
9588
- writeFilesystemSession(folder, {
9589
- schemaVersion: 1,
9590
- baseUrl: creds2.baseUrl,
9591
- experimentId: session.experimentId,
9592
- pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
9593
- files: Object.fromEntries(
9594
- refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
9595
- )
9596
- });
9597
- }
10384
+ await refreshExperimentFilesystemFolderIfNeeded(
10385
+ creds2,
10386
+ folder,
10387
+ session.experimentId
10388
+ );
9598
10389
  output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data }, () => {
9599
10390
  printFilesystemPlan(planRes.data);
9600
10391
  });
@@ -9603,29 +10394,28 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9603
10394
  fail(this, error instanceof Error ? error.message : String(error));
9604
10395
  }
9605
10396
  });
9606
- 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 raw QA run JSON").action(async (target, opts) => {
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) {
9607
10398
  const creds2 = requireCredentials();
9608
10399
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9609
10400
  const spinner = ora2("Fetching QA run...").start();
9610
10401
  try {
9611
10402
  const resolved = await resolveExperimentForFilesystemOrId(target);
9612
10403
  const res = await getExperimentQaRun(creds2, resolved.experimentId);
9613
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to fetch QA run");
10404
+ if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to fetch QA run", classifyStatus(res.status), res.errorDetails);
9614
10405
  spinner.stop();
9615
- printQaRun(res.data.run, opts.json === true);
9616
10406
  await refreshExperimentFilesystemFolderIfNeeded(
9617
10407
  creds2,
9618
10408
  resolved.folder,
9619
10409
  resolved.experimentId
9620
10410
  );
10411
+ output(this, { run: res.data.run }, () => printQaRun(res.data.run));
9621
10412
  } catch (error) {
9622
10413
  spinner.fail("Failed to fetch QA run");
9623
- printError(error instanceof Error ? error.message : String(error));
9624
- process.exit(1);
10414
+ throw error;
9625
10415
  }
9626
10416
  });
9627
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(
9628
- async (target, opts) => {
10418
+ async function action16(target, opts) {
9629
10419
  const creds2 = requireCredentials();
9630
10420
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9631
10421
  const spinner = ora2("Updating QA checklist...").start();
@@ -9646,23 +10436,25 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
9646
10436
  notes: opts.notes,
9647
10437
  evidence
9648
10438
  });
9649
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to update QA run");
9650
- spinner.succeed("Updated QA checklist");
9651
- 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;
9652
10441
  await refreshExperimentFilesystemFolderIfNeeded(
9653
10442
  creds2,
9654
10443
  resolved.folder,
9655
10444
  resolved.experimentId
9656
10445
  );
10446
+ output(this, { run: run3 }, () => {
10447
+ spinner.succeed("Updated QA checklist");
10448
+ printQaRun(run3);
10449
+ });
9657
10450
  } catch (error) {
9658
10451
  spinner.fail("Failed to update QA checklist");
9659
- printError(error instanceof Error ? error.message : String(error));
9660
- process.exit(1);
10452
+ throw error;
9661
10453
  }
9662
10454
  }
9663
10455
  );
9664
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(
9665
- async (target, opts) => {
10457
+ async function action17(target, opts) {
9666
10458
  const creds2 = requireCredentials();
9667
10459
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9668
10460
  const spinner = ora2("Updating QA notes...").start();
@@ -9680,63 +10472,174 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
9680
10472
  acceptanceCriteriaSummary: opts.acceptance ?? null
9681
10473
  };
9682
10474
  const res = await updateExperimentQaRun(creds2, resolved.experimentId, body5);
9683
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to update QA notes");
9684
- spinner.succeed("Updated QA notes");
9685
- 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;
9686
10477
  await refreshExperimentFilesystemFolderIfNeeded(
9687
10478
  creds2,
9688
10479
  resolved.folder,
9689
10480
  resolved.experimentId
9690
10481
  );
10482
+ output(this, { run: run3 }, () => {
10483
+ spinner.succeed("Updated QA notes");
10484
+ printQaRun(run3);
10485
+ });
9691
10486
  } catch (error) {
9692
10487
  spinner.fail("Failed to update QA notes");
9693
- printError(error instanceof Error ? error.message : String(error));
9694
- process.exit(1);
10488
+ throw error;
9695
10489
  }
9696
10490
  }
9697
10491
  );
9698
- 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) {
9699
10493
  const creds2 = requireCredentials();
9700
10494
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9701
10495
  const spinner = ora2("Approving QA...").start();
9702
10496
  try {
9703
10497
  const resolved = await resolveExperimentForFilesystemOrId(target);
9704
10498
  const res = await approveExperimentQa(creds2, resolved.experimentId);
9705
- if (!res.ok) throw new Error(res.error ?? "Failed to approve QA");
9706
- 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);
9707
10500
  await refreshExperimentFilesystemFolderIfNeeded(
9708
10501
  creds2,
9709
10502
  resolved.folder,
9710
10503
  resolved.experimentId
9711
10504
  );
10505
+ output(this, { experimentId: resolved.experimentId, qaStatus: "done" }, () => spinner.succeed(`QA marked done for ${resolved.label}`));
9712
10506
  } catch (error) {
9713
10507
  spinner.fail("Failed to approve QA");
9714
- printError(error instanceof Error ? error.message : String(error));
9715
- process.exit(1);
10508
+ throw error;
9716
10509
  }
9717
10510
  });
9718
- 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) {
9719
10512
  const creds2 = requireCredentials();
9720
10513
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9721
10514
  const spinner = ora2("Reopening QA...").start();
9722
10515
  try {
9723
10516
  const resolved = await resolveExperimentForFilesystemOrId(target);
9724
10517
  const res = await reopenExperimentQa(creds2, resolved.experimentId);
9725
- if (!res.ok) throw new Error(res.error ?? "Failed to reopen QA");
9726
- 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);
9727
10519
  await refreshExperimentFilesystemFolderIfNeeded(
9728
10520
  creds2,
9729
10521
  resolved.folder,
9730
10522
  resolved.experimentId
9731
10523
  );
10524
+ output(this, { experimentId: resolved.experimentId, qaStatus: "open" }, () => spinner.succeed(`QA reopened for ${resolved.label}`));
9732
10525
  } catch (error) {
9733
10526
  spinner.fail("Failed to reopen QA");
9734
- printError(error instanceof Error ? error.message : String(error));
9735
- process.exit(1);
10527
+ throw error;
9736
10528
  }
9737
10529
  });
9738
10530
  var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen);
9739
- 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(qa);
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
+ `);
9740
10643
 
9741
10644
  // src/commands/push.ts
9742
10645
  init_source();
@@ -9811,7 +10714,7 @@ function isRecord(value) {
9811
10714
  }
9812
10715
  var pushCommand = new Command("push").description(
9813
10716
  "Push experiment definitions from config file to the API (idempotent by slug)"
9814
- ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action10(opts) {
10717
+ ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action21(opts) {
9815
10718
  const creds2 = requireCredentials();
9816
10719
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9817
10720
  const configPath = opts.config ?? findConfigPath();
@@ -9951,7 +10854,7 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
9951
10854
  }
9952
10855
  const data = created.data;
9953
10856
  output(previewCommand, data, () => console.log(source_default.cyan(data.preview_url)));
9954
- if (opts.open !== false) {
10857
+ if (opts.open !== false && !shouldUseJson(previewCommand)) {
9955
10858
  try {
9956
10859
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
9957
10860
  await open2(data.preview_url);
@@ -9967,7 +10870,7 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
9967
10870
  const res = await getExperiment(creds2, idOrSlug);
9968
10871
  if (!res.ok) {
9969
10872
  spinner.fail(`Experiment "${idOrSlug}" not found`);
9970
- process.exit(1);
10873
+ fail(previewCommand, res.error ?? `Experiment "${idOrSlug}" not found`, classifyStatus(res.status), res.errorDetails);
9971
10874
  }
9972
10875
  exp = res.data;
9973
10876
  } else {
@@ -9975,13 +10878,13 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
9975
10878
  const res = await getExperimentBySlug(creds2, slug);
9976
10879
  if (!res.ok) {
9977
10880
  spinner.fail(`Experiment "${slug}" not found`);
9978
- process.exit(1);
10881
+ fail(previewCommand, res.error ?? `Experiment "${slug}" not found`, classifyStatus(res.status), res.errorDetails);
9979
10882
  }
9980
10883
  exp = res.data;
9981
10884
  }
9982
10885
  if (!exp.variations || exp.variations.length === 0) {
9983
10886
  spinner.fail("Experiment has no variations");
9984
- process.exit(1);
10887
+ fail(previewCommand, "Experiment has no variations");
9985
10888
  }
9986
10889
  let variation;
9987
10890
  if (variationId) {
@@ -9990,12 +10893,9 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
9990
10893
  );
9991
10894
  if (!variation) {
9992
10895
  spinner.fail(`Variation "${variationId}" not found`);
9993
- console.log(
9994
- source_default.dim(
9995
- `Available: ${exp.variations.map((v) => `${v.name} (${v.id})`).join(", ")}`
9996
- )
9997
- );
9998
- 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
+ });
9999
10899
  }
10000
10900
  } else {
10001
10901
  variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
@@ -10003,30 +10903,28 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
10003
10903
  spinner.stop();
10004
10904
  const storeUrl = exp.store_url ?? opts?.storeUrl;
10005
10905
  if (!storeUrl) {
10006
- printError(
10007
- 'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.'
10008
- );
10009
- process.exit(1);
10906
+ fail(previewCommand, 'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.');
10010
10907
  }
10011
10908
  let previewUrl;
10012
10909
  try {
10013
10910
  previewUrl = new URL(storeUrl);
10014
10911
  } catch {
10015
- printError('Invalid store URL. Pass an absolute URL like "https://example.com/products/item".');
10016
- process.exit(1);
10912
+ fail(previewCommand, 'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".');
10017
10913
  }
10018
10914
  previewUrl.searchParams.set("drip_force", `${exp.id}:${variation.id}`);
10019
10915
  previewUrl.searchParams.set("drip_experiment_id", exp.id);
10020
10916
  previewUrl.searchParams.set("drip_variation_id", variation.id);
10021
10917
  previewUrl.searchParams.set("drip_qa", "1");
10022
10918
  previewUrl.searchParams.set("drip_debug", "1");
10023
- console.log();
10024
- console.log(source_default.bold(`Preview: ${exp.name}`));
10025
- console.log(`Variation: ${variation.name}${variation.is_control ? " (control)" : ""}`);
10026
- console.log();
10027
- console.log(source_default.cyan(previewUrl.toString()));
10028
- console.log();
10029
- if (opts?.open !== false) {
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)) {
10030
10928
  try {
10031
10929
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
10032
10930
  await open2(previewUrl.toString());
@@ -10141,7 +11039,7 @@ async function uploadVersion(params) {
10141
11039
  })
10142
11040
  );
10143
11041
  const res = await uploadHarnessVersion(creds2, params.session.sessionId, form);
10144
- if (!res.ok) throw new Error(res.error ?? "Failed to upload harness version");
11042
+ if (!res.ok) throw new CliError(res.error ?? "Failed to upload harness version", classifyStatus(res.status), res.errorDetails);
10145
11043
  return res.data;
10146
11044
  }
10147
11045
  async function createSession(folder) {
@@ -10159,40 +11057,43 @@ async function createSession(folder) {
10159
11057
  resetJs: cleanupResetJs(experiment),
10160
11058
  antiFlickerSelectors: apex.antiFlickerSelectors ?? []
10161
11059
  });
10162
- if (!res.ok) throw new Error(res.error ?? "Failed to create harness session");
11060
+ if (!res.ok) throw new CliError(res.error ?? "Failed to create harness session", classifyStatus(res.status), res.errorDetails);
10163
11061
  return { session: res.data, apex, experiment };
10164
11062
  }
10165
11063
  var harnessCommand = new Command("harness").description("Develop AB Test Harness projects through Apex live QA sessions");
10166
- 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) {
10167
11065
  const folder = resolveFolder(folderArg);
10168
11066
  const targetPath = apexConfigPath(folder);
10169
11067
  if (import_node_fs8.default.existsSync(targetPath) && !opts.force) {
10170
- printError(`${targetPath} already exists. Use --force to overwrite.`);
10171
- process.exit(1);
11068
+ fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
10172
11069
  }
10173
11070
  const { apex } = loadHarnessConfig(folder);
10174
11071
  import_node_fs8.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
10175
11072
  `, "utf8");
10176
- printSuccess(`Created ${targetPath}`);
11073
+ output(this, { folder, configPath: targetPath, experiment: apex.experimentSlug, variation: apex.variationSlug }, () => printSuccess(`Created ${targetPath}`));
10177
11074
  });
10178
- 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) {
10179
11076
  const folder = resolveFolder(folderArg);
10180
11077
  const created = await createSession(folder);
10181
11078
  await runAbHarness(["build", folder], folder);
10182
11079
  const version = await uploadVersion({ ...created, folder });
10183
- console.log(source_default.bold("\nApex Harness Session"));
10184
- console.log(`Experiment: ${created.session.experimentId}`);
10185
- console.log(`Variation: ${created.session.variationId}`);
10186
- console.log(`Version: ${version.version}`);
10187
- console.log(source_default.cyan(created.session.previewUrl));
10188
- console.log();
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
+ });
10189
11088
  if (opts.open !== false) {
10190
11089
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
10191
11090
  await open2(created.session.previewUrl);
10192
11091
  }
10193
11092
  let uploading = false;
10194
11093
  let queued = false;
11094
+ let closing = false;
10195
11095
  const rebuild = async () => {
11096
+ if (closing) return;
10196
11097
  if (uploading) {
10197
11098
  queued = true;
10198
11099
  return;
@@ -10201,12 +11102,12 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
10201
11102
  try {
10202
11103
  await runAbHarness(["build", folder], folder);
10203
11104
  const next = await uploadVersion({ ...created, folder });
10204
- console.log(source_default.dim(`[harness] uploaded version ${next.version}`));
11105
+ console.error(source_default.dim(`[harness] uploaded version ${next.version}`));
10205
11106
  } catch (error) {
10206
11107
  printWarning(error instanceof Error ? error.message : String(error));
10207
11108
  } finally {
10208
11109
  uploading = false;
10209
- if (queued) {
11110
+ if (queued && !closing) {
10210
11111
  queued = false;
10211
11112
  void rebuild();
10212
11113
  }
@@ -10220,29 +11121,73 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
10220
11121
  debounce = setTimeout(() => void rebuild(), 300);
10221
11122
  });
10222
11123
  process.on("SIGINT", () => {
11124
+ closing = true;
11125
+ queued = false;
11126
+ if (debounce) clearTimeout(debounce);
10223
11127
  watcher.close();
10224
- process.exit(0);
11128
+ process.exitCode = 0;
10225
11129
  });
10226
11130
  });
10227
- 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) {
10228
11132
  const folder = resolveFolder(folderArg);
10229
11133
  const created = await createSession(folder);
10230
11134
  await runAbHarness(["verify", folder], folder);
10231
11135
  await runAbHarness(["build", folder], folder);
10232
11136
  await uploadVersion({ ...created, folder });
10233
11137
  const res = await publishHarnessSession(requireCredentials(), created.session.sessionId);
10234
- if (!res.ok) throw new Error(res.error ?? "Failed to publish harness session");
10235
- 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."));
10236
11140
  });
10237
11141
 
10238
11142
  // src/commands/operator.ts
10239
11143
  init_source();
10240
- var import_node_crypto3 = __toESM(require("node:crypto"));
11144
+ var import_node_crypto4 = __toESM(require("node:crypto"));
10241
11145
  var import_node_fs9 = __toESM(require("node:fs"));
10242
11146
  var import_node_path7 = __toESM(require("node:path"));
10243
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
+ }
10244
11189
  function sha256Text2(value) {
10245
- return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
11190
+ return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
10246
11191
  }
10247
11192
  function byteLength(value) {
10248
11193
  return Buffer.byteLength(value, "utf8");
@@ -10272,9 +11217,11 @@ function writeJsonFile(filePath, value) {
10272
11217
  function readSession(folder) {
10273
11218
  const session = readJsonFile(sessionPath(folder));
10274
11219
  if (!session) {
10275
- throw new Error(
10276
- `Missing ${sessionPath(folder)}. Run apex workspace checkout first.`
10277
- );
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.`);
10278
11225
  }
10279
11226
  return session;
10280
11227
  }
@@ -10408,7 +11355,7 @@ async function createExactHarnessSession(folder, session) {
10408
11355
  targetUrl,
10409
11356
  antiFlickerSelectors: []
10410
11357
  });
10411
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to create harness session");
11358
+ if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to create harness session", classifyStatus(res.status), res.errorDetails);
10412
11359
  return res.data;
10413
11360
  }
10414
11361
  async function uploadCurrentArtifactVersion(folder, sessionId) {
@@ -10435,7 +11382,7 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
10435
11382
  }
10436
11383
  form.append("manifest", JSON.stringify(manifest));
10437
11384
  const res = await uploadHarnessVersion(creds2, sessionId, form);
10438
- if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to upload harness artifact");
11385
+ if (!res.ok || !res.data) throw new CliError(res.error ?? "Failed to upload harness artifact", classifyStatus(res.status), res.errorDetails);
10439
11386
  return res.data;
10440
11387
  }
10441
11388
  async function syncChangedFiles(folder, session) {
@@ -10459,18 +11406,28 @@ async function syncChangedFiles(folder, session) {
10459
11406
  title: `CLI sync ${file.path}`
10460
11407
  }
10461
11408
  );
10462
- if (!res.ok) throw new Error(res.error ?? `Failed to upload ${file.path}`);
11409
+ if (!res.ok) throw new CliError(res.error ?? `Failed to upload ${file.path}`, classifyStatus(res.status), res.errorDetails);
10463
11410
  }
10464
11411
  return { localFiles, changed };
10465
11412
  }
10466
- async function runPublish(folderArg) {
11413
+ async function runPublish(folderArg, opts = {}) {
10467
11414
  const folder = folderPath2(folderArg);
10468
11415
  const session = readSession(folder);
10469
11416
  const { localFiles, changed } = await syncChangedFiles(folder, session);
10470
11417
  const creds2 = requireCredentials();
10471
- const build = await buildOperatorWorkspace(creds2, session.experimentId, session.variationId);
11418
+ const build = await buildOperatorWorkspace(
11419
+ creds2,
11420
+ session.experimentId,
11421
+ session.variationId,
11422
+ opts.allowGoalWrites ? { allowGoalWrites: true } : void 0
11423
+ );
10472
11424
  if (!build.ok || !build.data?.ok) {
10473
- throw new Error(build.error ?? JSON.stringify(build.data ?? { error: "Workspace build failed" }, null, 2));
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
+ );
10474
11431
  }
10475
11432
  let external = null;
10476
11433
  if (shouldUseExternalDelivery(folder)) {
@@ -10495,32 +11452,50 @@ async function runPublish(folderArg) {
10495
11452
  }
10496
11453
  return { build: build.data, changed, external };
10497
11454
  }
10498
- var operatorCommand = new Command("workspace").alias("operator").description("Check out, preview, and publish Apex draft workspace files");
10499
- operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--dir <folder>", "Target folder").action(async (experimentId, opts) => {
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) {
10500
11464
  try {
10501
11465
  const creds2 = requireCredentials();
10502
11466
  const expRes = await getExperiment(creds2, experimentId);
10503
- if (!expRes.ok || !expRes.data) throw new Error(expRes.error ?? "Experiment not found");
10504
- const variation = resolveVariation(expRes.data, opts.variation);
10505
- const folder = folderPath2(opts.dir ?? slugify2(`${expRes.data.name}-${variation.name}`, "apex-operator"));
10506
- const workspace = await getOperatorWorkspace(creds2, expRes.data.id, variation.id);
10507
- if (!workspace.ok || !workspace.data) throw new Error(workspace.error ?? "Could not load workspace");
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);
10508
11482
  import_node_fs9.default.mkdirSync(folder, { recursive: true });
10509
11483
  writeWorkspaceFiles(folder, workspace.data);
10510
11484
  writeSession(folder, sessionFromWorkspace({
10511
11485
  credsBaseUrl: creds2.baseUrl,
10512
- experimentId: expRes.data.id,
11486
+ experimentId: experiment.id,
10513
11487
  variationId: variation.id,
10514
11488
  workspace: workspace.data
10515
11489
  }));
10516
- printSuccess(`Checked out ${expRes.data.name} / ${variation.name}`);
10517
- console.log(source_default.cyan(folder));
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
+ });
10518
11494
  } catch (error) {
10519
- printError(error instanceof Error ? error.message : String(error));
10520
- process.exit(1);
11495
+ throw error;
10521
11496
  }
10522
11497
  });
10523
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action11(folderArg) {
11498
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action26(folderArg) {
10524
11499
  try {
10525
11500
  const folder = folderPath2(folderArg);
10526
11501
  const session = readSession(folder);
@@ -10548,18 +11523,12 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
10548
11523
  fail(this, error instanceof Error ? error.message : String(error));
10549
11524
  }
10550
11525
  });
10551
- 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) {
10552
11527
  try {
10553
11528
  const folder = folderPath2(folderArg);
10554
11529
  const session = readSession(folder);
10555
11530
  const harnessSession = await createExactHarnessSession(folder, session);
10556
11531
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
10557
- console.log(source_default.bold("\nApex Workspace Preview"));
10558
- console.log(`Experiment: ${harnessSession.experimentId}`);
10559
- console.log(`Variation: ${harnessSession.variationId}`);
10560
- console.log(`Version: ${version.version}`);
10561
- console.log(source_default.cyan(harnessSession.previewUrl));
10562
- console.log();
10563
11532
  import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10564
11533
  writeJsonFile(import_node_path7.default.join(apexDir(folder), "harness-session.json"), {
10565
11534
  sessionId: harnessSession.sessionId,
@@ -10569,6 +11538,14 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
10569
11538
  previewUrl: harnessSession.previewUrl,
10570
11539
  lastVersion: version.version
10571
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
+ });
10572
11549
  if (opts.open !== false) {
10573
11550
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
10574
11551
  await open2(harnessSession.previewUrl);
@@ -10576,7 +11553,9 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
10576
11553
  if (opts.once) return;
10577
11554
  let uploading = false;
10578
11555
  let queued = false;
11556
+ let closing = false;
10579
11557
  const upload2 = async () => {
11558
+ if (closing) return;
10580
11559
  if (uploading) {
10581
11560
  queued = true;
10582
11561
  return;
@@ -10584,12 +11563,12 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
10584
11563
  uploading = true;
10585
11564
  try {
10586
11565
  const next = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
10587
- console.log(source_default.dim(`[operator] uploaded preview version ${next.version}`));
11566
+ console.error(source_default.dim(`[operator] uploaded preview version ${next.version}`));
10588
11567
  } catch (error) {
10589
11568
  printWarning(error instanceof Error ? error.message : String(error));
10590
11569
  } finally {
10591
11570
  uploading = false;
10592
- if (queued) {
11571
+ if (queued && !closing) {
10593
11572
  queued = false;
10594
11573
  void upload2();
10595
11574
  }
@@ -10603,28 +11582,29 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
10603
11582
  timer = setTimeout(() => void upload2(), 300);
10604
11583
  });
10605
11584
  process.on("SIGINT", () => {
11585
+ closing = true;
11586
+ queued = false;
11587
+ if (timer) clearTimeout(timer);
10606
11588
  watcher.close();
10607
- process.exit(0);
11589
+ process.exitCode = 0;
10608
11590
  });
10609
11591
  } catch (error) {
10610
- printError(error instanceof Error ? error.message : String(error));
10611
- process.exit(1);
11592
+ throw error;
10612
11593
  }
10613
11594
  });
10614
- 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) {
10615
11596
  try {
10616
- const result = await runPublish(folderArg);
10617
- printSuccess(`Published ${result.changed.length} changed file(s) to Apex`);
11597
+ const result = await runPublish(folderArg, opts);
10618
11598
  const build = result.build;
10619
- console.log(`Mutations: ${build.mutationCount ?? 0}`);
10620
- console.log(`CSS bytes: ${build.customCssBytes ?? 0}`);
10621
- console.log(`JS bytes: ${build.customJsBytes ?? 0}`);
10622
- if (result.external) {
10623
- console.log(`External artifacts: version ${result.external.version}`);
10624
- }
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
+ });
10625
11606
  } catch (error) {
10626
- printError(error instanceof Error ? error.message : String(error));
10627
- process.exit(1);
11607
+ throw error;
10628
11608
  }
10629
11609
  });
10630
11610
 
@@ -10655,7 +11635,7 @@ function briefLine(item) {
10655
11635
  ].filter(Boolean);
10656
11636
  return parts.join("\n");
10657
11637
  }
10658
- 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) {
10659
11639
  const creds2 = requireCredentials();
10660
11640
  const limit = parseInt(opts.limit, 10);
10661
11641
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -10665,63 +11645,56 @@ var approved = new Command("approved").description("List approved roadmap ideas
10665
11645
  q: opts.query,
10666
11646
  hasExperiment
10667
11647
  });
10668
- if (!res.ok) {
10669
- printError(res.error ?? "Failed to fetch approved roadmap items");
10670
- process.exit(1);
10671
- }
10672
- if (opts.json) {
10673
- console.log(JSON.stringify(res.data, null, 2));
10674
- return;
10675
- }
11648
+ if (!res.ok) fail(this, res.error ?? "Failed to fetch approved roadmap items", classifyStatus(res.status), res.errorDetails);
10676
11649
  const items = res.data?.data ?? [];
10677
- if (items.length === 0) {
10678
- console.log(source_default.dim("No approved roadmap items found."));
10679
- return;
10680
- }
10681
- printTable(
10682
- ["ID", "SCORE", "TYPE", "EXPERIMENT", "TITLE"],
10683
- items.map((item) => [
10684
- item.id,
10685
- score(item.computed_score),
10686
- truncate(item.action_type, 14),
10687
- truncate(experimentState(item), 18),
10688
- truncate(item.title, 54)
10689
- ]),
10690
- [36, 6, 14, 18, 54]
10691
- );
10692
- if (opts.brief) {
10693
- console.log();
10694
- for (const item of items) {
10695
- console.log(source_default.bold(item.title));
10696
- const detail5 = briefLine(item);
10697
- if (detail5) console.log(detail5);
10698
- 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) {
10699
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
+ }
10700
11675
  }
10701
- }
10702
- if (res.data?.pagination.has_more) {
10703
- console.log(source_default.dim(`
11676
+ if (res.data?.pagination.has_more) {
11677
+ console.log(source_default.dim(`
10704
11678
  More results: --cursor ${res.data.pagination.next_cursor}`));
10705
- }
11679
+ }
11680
+ });
10706
11681
  });
10707
- var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("-o, --output <path>", "Write handoff JSON to a file").option("--instructions", "Print only the build instructions").action(async (id, opts) => {
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) {
10708
11683
  const creds2 = requireCredentials();
10709
11684
  const res = await getRoadmapItemHandoff(creds2, id);
10710
- if (!res.ok) {
10711
- printError(res.error ?? "Failed to fetch roadmap handoff");
10712
- process.exit(1);
10713
- }
11685
+ if (!res.ok) fail(this, res.error ?? "Failed to fetch roadmap handoff", classifyStatus(res.status), res.errorDetails);
10714
11686
  const payload = res.data ?? {};
10715
- const output2 = opts.instructions ? String(payload.instructions ?? "") : `${JSON.stringify(payload, null, 2)}
11687
+ const rendered = opts.instructions ? String(payload.instructions ?? "") : `${JSON.stringify(payload, null, 2)}
10716
11688
  `;
10717
- if (opts.output) {
10718
- const target = import_node_path8.default.resolve(process.cwd(), opts.output);
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);
10719
11692
  import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(target), { recursive: true });
10720
- import_node_fs10.default.writeFileSync(target, output2, "utf8");
10721
- printSuccess(`Wrote ${target}`);
11693
+ import_node_fs10.default.writeFileSync(target, rendered, "utf8");
11694
+ output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
10722
11695
  return;
10723
11696
  }
10724
- console.log(output2);
11697
+ output(this, outputData, () => console.log(rendered));
10725
11698
  });
10726
11699
  var roadmapCommand = new Command("roadmap").description("Work with approved roadmap ideas and build handoffs");
10727
11700
  roadmapCommand.addCommand(approved);
@@ -10901,7 +11874,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
10901
11874
  console.log(`${index + 1}. ${check}`);
10902
11875
  });
10903
11876
  });
10904
- 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 action12(opts) {
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) {
10905
11878
  const creds2 = requireCredentials();
10906
11879
  const res = await verifyTracking(creds2, {
10907
11880
  minutes: Number.parseInt(opts.minutes, 10),
@@ -10941,6 +11914,151 @@ trackingCommand.command("verify").description("Verify live tracking events for t
10941
11914
  init_source();
10942
11915
  var import_node_fs12 = __toESM(require("node:fs"));
10943
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
+ }
10944
12062
  function parseCapture(command, value) {
10945
12063
  if (!value) return void 0;
10946
12064
  const source = value.startsWith("@") ? value.slice(1) : value;
@@ -10969,7 +12087,7 @@ function printGoal(goal) {
10969
12087
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
10970
12088
  console.log();
10971
12089
  }
10972
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action13(opts) {
12090
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action32(opts) {
10973
12091
  const creds2 = requireCredentials();
10974
12092
  const res = await listGoals(creds2, { limit: Number.parseInt(opts.limit, 10) });
10975
12093
  if (!res.ok || !res.data) {
@@ -10991,7 +12109,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
10991
12109
  );
10992
12110
  });
10993
12111
  });
10994
- 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 action14(opts) {
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) {
10995
12113
  const creds2 = requireCredentials();
10996
12114
  const body5 = cleanBody({
10997
12115
  name: opts.name,
@@ -11009,7 +12127,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
11009
12127
  }
11010
12128
  output(this, { goal: res.data }, () => printGoal(res.data));
11011
12129
  });
11012
- 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 action15(id, opts) {
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) {
11013
12131
  const creds2 = requireCredentials();
11014
12132
  const body5 = cleanBody({
11015
12133
  name: opts.name,
@@ -11030,7 +12148,137 @@ var update = new Command("update").description("Update a goal").argument("<id>",
11030
12148
  }
11031
12149
  output(this, { goal: res.data }, () => printGoal(res.data));
11032
12150
  });
11033
- var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update);
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);
11034
12282
 
11035
12283
  // src/commands/environments.ts
11036
12284
  init_source();
@@ -11083,11 +12331,11 @@ async function load2(command) {
11083
12331
  }
11084
12332
  return result.data;
11085
12333
  }
11086
- var list3 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action16() {
12334
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action43() {
11087
12335
  const { environments } = await load2(this);
11088
12336
  output(this, { environments }, () => printEnvironments(environments.items));
11089
12337
  });
11090
- 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 action17(domain, opts) {
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) {
11091
12339
  const normalizedDomain = normalizeEnvironmentDomain(domain);
11092
12340
  if (!normalizedDomain) {
11093
12341
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -11130,7 +12378,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
11130
12378
  () => printEnvironments(result.data.environments.items)
11131
12379
  );
11132
12380
  });
11133
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action18(domain) {
12381
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action45(domain) {
11134
12382
  const normalizedDomain = normalizeEnvironmentDomain(domain);
11135
12383
  if (!normalizedDomain) {
11136
12384
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -11163,7 +12411,200 @@ var removeDomain = new Command("remove-domain").description("Remove a domain map
11163
12411
  () => printEnvironments(result.data.environments.items)
11164
12412
  );
11165
12413
  });
11166
- var environmentsCommand = new Command("environments").description("Manage runtime environments and domain mappings").addCommand(list3).addCommand(setDomain).addCommand(removeDomain);
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);
11167
12608
 
11168
12609
  // src/commands/analytics.ts
11169
12610
  function display(value) {
@@ -11198,7 +12639,7 @@ function analyticsSubcommand(name, description, defaultDays) {
11198
12639
  command.option("--planning-days <n>", "Planning window in days", "180");
11199
12640
  command.option("--limit <n>", "Maximum page rows", "50");
11200
12641
  }
11201
- return command.action(async function action24(opts) {
12642
+ return command.action(async function action68(opts) {
11202
12643
  const result = await getAnalytics(requireCredentials(), name, {
11203
12644
  days: positiveInteger(this, opts.days, "--days"),
11204
12645
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -11210,12 +12651,17 @@ function analyticsSubcommand(name, description, defaultDays) {
11210
12651
  output(this, { data: result.data.data }, () => printAnalytics(result.data.data));
11211
12652
  });
11212
12653
  }
11213
- 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
+ `);
11214
12660
 
11215
12661
  // src/commands/flags.ts
11216
12662
  init_source();
11217
- var import_node_fs13 = __toESM(require("node:fs"));
11218
- var import_node_path11 = __toESM(require("node:path"));
12663
+ var import_node_fs14 = __toESM(require("node:fs"));
12664
+ var import_node_path12 = __toESM(require("node:path"));
11219
12665
 
11220
12666
  // src/edge-status.ts
11221
12667
  function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 420) {
@@ -11257,7 +12703,7 @@ function body(command, value, opts) {
11257
12703
  let parsed = {};
11258
12704
  if (value) {
11259
12705
  const source = value.startsWith("@") ? value.slice(1) : value;
11260
- const text = value.startsWith("@") || import_node_fs13.default.existsSync(source) ? import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), source), "utf8") : value;
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;
11261
12707
  try {
11262
12708
  parsed = JSON.parse(text);
11263
12709
  } catch (e) {
@@ -11287,7 +12733,7 @@ function detail(x) {
11287
12733
  console.log(` State: ${x.status}`);
11288
12734
  console.log();
11289
12735
  }
11290
- var list4 = new Command("list").alias("ls").description("List flags").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
12736
+ var list6 = new Command("list").alias("ls").description("List flags").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
11291
12737
  const r = await listManagedDeliveries(
11292
12738
  requireCredentials(),
11293
12739
  kind,
@@ -11314,7 +12760,7 @@ var list4 = new Command("list").alias("ls").description("List flags").option("-n
11314
12760
  )
11315
12761
  );
11316
12762
  });
11317
- var get2 = new Command("get").description("Get a flag").argument("<id>").action(async function(id) {
12763
+ var get4 = new Command("get").description("Get a flag").argument("<id>").action(async function(id) {
11318
12764
  const r = await getManagedDelivery(requireCredentials(), kind, id);
11319
12765
  if (!r.ok || !r.data)
11320
12766
  fail(
@@ -11385,29 +12831,35 @@ function write(name) {
11385
12831
  });
11386
12832
  return c;
11387
12833
  }
11388
- function lifecycle(action24) {
11389
- return new Command(action24).description(`${action24[0].toUpperCase()}${action24.slice(1)} a flag`).argument("<id>").action(async function(id) {
12834
+ function lifecycle(action68) {
12835
+ return new Command(action68).description(`${action68[0].toUpperCase()}${action68.slice(1)} a flag`).argument("<id>").action(async function(id) {
11390
12836
  const r = await transitionManagedDelivery(
11391
12837
  requireCredentials(),
11392
12838
  kind,
11393
12839
  id,
11394
- action24
12840
+ action68
11395
12841
  );
11396
12842
  if (!r.ok || !r.data)
11397
12843
  fail(
11398
12844
  this,
11399
- r.error ?? `Failed to ${action24}`,
12845
+ r.error ?? `Failed to ${action68}`,
11400
12846
  classifyStatus(r.status),
11401
12847
  r.errorDetails
11402
12848
  );
11403
12849
  output(this, { flag: r.data }, () => detail(r.data));
11404
12850
  });
11405
12851
  }
11406
- var flagsCommand = new Command("flags").description("Manage flags").addCommand(list4).addCommand(get2).addCommand(status).addCommand(write("create")).addCommand(write("update")).addCommand(lifecycle("activate")).addCommand(lifecycle("pause")).addCommand(lifecycle("archive"));
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
+ `);
11407
12859
 
11408
12860
  // src/commands/backlog.ts
11409
- var import_node_fs14 = __toESM(require("node:fs"));
11410
- var import_node_path12 = __toESM(require("node:path"));
12861
+ var import_node_fs15 = __toESM(require("node:fs"));
12862
+ var import_node_path13 = __toESM(require("node:path"));
11411
12863
  init_source();
11412
12864
  var kind2 = "backlog";
11413
12865
  var singular = "backlog item";
@@ -11415,7 +12867,7 @@ function parse(command, value) {
11415
12867
  if (!value) return {};
11416
12868
  try {
11417
12869
  const source = value.startsWith("@") ? value.slice(1) : value;
11418
- 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;
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;
11419
12871
  const body5 = JSON.parse(text);
11420
12872
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
11421
12873
  return body5;
@@ -11430,28 +12882,28 @@ function detail2(x) {
11430
12882
  if (x.status) console.log(` State: ${x.status}`);
11431
12883
  console.log();
11432
12884
  }
11433
- var list5 = 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) {
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) {
11434
12886
  const r = await listInsights(requireCredentials(), kind2, Number(o.limit), o.cursor, o.includeArchived);
11435
12887
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
11436
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]));
11437
12889
  });
11438
- var get3 = new Command("get").description("Get a backlog item").argument("<id>", "backlog item ID").action(async function(id) {
12890
+ var get5 = new Command("get").description("Get a backlog item").argument("<id>", "backlog item ID").action(async function(id) {
11439
12891
  const r = await getInsight(requireCredentials(), kind2, id);
11440
12892
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
11441
12893
  output(this, { backlog_item: r.data }, () => detail2(r.data));
11442
12894
  });
11443
- function write2(action24) {
11444
- const c = new Command(action24).description(`${action24 === "create" ? "Create" : "Update"} a ${singular}`);
11445
- if (action24 === "update") c.argument("<id>", `${singular} ID`);
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`);
11446
12898
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
11447
12899
  c.action(async function(...args) {
11448
12900
  const o = this.opts();
11449
- const id = action24 === "update" ? String(args[0]) : void 0;
11450
- const raw = o.file ? `@${o.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
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;
11451
12903
  const body5 = parse(this, raw);
11452
12904
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
11453
- const r = action24 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
11454
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action24}`, classifyStatus(r.status), r.errorDetails);
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);
11455
12907
  output(this, { backlog_item: r.data }, () => detail2(r.data));
11456
12908
  });
11457
12909
  return c;
@@ -11467,7 +12919,7 @@ var restore = new Command("restore").description("Restore an archived backlog it
11467
12919
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to restore", classifyStatus(r.status), r.errorDetails);
11468
12920
  output(this, { restored: true, id }, () => console.log(`Restored ${id}`));
11469
12921
  });
11470
- var remove = new Command("delete").description("Delete a backlog item").argument("<id>", "backlog item ID").option("-f, --force", "Skip confirmation").action(async function(id, opts) {
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) {
11471
12923
  if (!opts.force) {
11472
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`);
11473
12925
  const readline2 = await import("node:readline");
@@ -11483,11 +12935,11 @@ var remove = new Command("delete").description("Delete a backlog item").argument
11483
12935
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
11484
12936
  output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
11485
12937
  });
11486
- var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list5).addCommand(get3).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove);
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);
11487
12939
 
11488
12940
  // src/commands/signals.ts
11489
- var import_node_fs15 = __toESM(require("node:fs"));
11490
- var import_node_path13 = __toESM(require("node:path"));
12941
+ var import_node_fs16 = __toESM(require("node:fs"));
12942
+ var import_node_path14 = __toESM(require("node:path"));
11491
12943
  init_source();
11492
12944
  var kind3 = "signals";
11493
12945
  var singular2 = "signal";
@@ -11495,7 +12947,7 @@ function parse2(command, value) {
11495
12947
  if (!value) return {};
11496
12948
  try {
11497
12949
  const source = value.startsWith("@") ? value.slice(1) : value;
11498
- 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;
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;
11499
12951
  const body5 = JSON.parse(text);
11500
12952
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
11501
12953
  return body5;
@@ -11510,49 +12962,49 @@ function detail3(x) {
11510
12962
  if (x.status) console.log(` State: ${x.status}`);
11511
12963
  console.log();
11512
12964
  }
11513
- var list6 = 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) {
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) {
11514
12966
  const r = await listInsights(requireCredentials(), kind3, Number(o.limit), o.cursor, o.includeArchived);
11515
12967
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
11516
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]));
11517
12969
  });
11518
- var get4 = new Command("get").description("Get a signal").argument("<id>", "signal ID").action(async function(id) {
12970
+ var get6 = new Command("get").description("Get a signal").argument("<id>", "signal ID").action(async function(id) {
11519
12971
  const r = await getInsight(requireCredentials(), kind3, id);
11520
12972
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
11521
12973
  output(this, { signal: r.data }, () => detail3(r.data));
11522
12974
  });
11523
- function write3(action24) {
11524
- const c = new Command(action24).description(`${action24 === "create" ? "Create" : "Update"} a ${singular2}`);
11525
- if (action24 === "update") c.argument("<id>", `${singular2} ID`);
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`);
11526
12978
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
11527
12979
  c.action(async function(...args) {
11528
12980
  const o = this.opts();
11529
- const id = action24 === "update" ? String(args[0]) : void 0;
11530
- const raw = o.file ? `@${o.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
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;
11531
12983
  const body5 = parse2(this, raw);
11532
12984
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
11533
- const r = action24 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
11534
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action24}`, classifyStatus(r.status), r.errorDetails);
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);
11535
12987
  output(this, { signal: r.data }, () => detail3(r.data));
11536
12988
  });
11537
12989
  return c;
11538
12990
  }
11539
- var remove2 = new Command("delete").alias("archive").description("Delete or archive a signal").argument("<id>", "signal ID").action(async function(id) {
12991
+ var remove3 = new Command("delete").alias("archive").description("Delete or archive a signal").argument("<id>", "signal ID").action(async function(id) {
11540
12992
  const r = await deleteInsight(requireCredentials(), kind3, id);
11541
12993
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
11542
12994
  output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
11543
12995
  });
11544
- var signalsCommand = new Command("signals").description("Manage signals").addCommand(list6).addCommand(get4).addCommand(write3("create")).addCommand(write3("update")).addCommand(remove2);
12996
+ var signalsCommand = new Command("signals").description("Manage signals").addCommand(list8).addCommand(get6).addCommand(write3("create")).addCommand(write3("update")).addCommand(remove3);
11545
12997
 
11546
12998
  // src/commands/personalizations.ts
11547
12999
  init_source();
11548
- var import_node_fs16 = __toESM(require("node:fs"));
11549
- var import_node_path14 = __toESM(require("node:path"));
13000
+ var import_node_fs17 = __toESM(require("node:fs"));
13001
+ var import_node_path15 = __toESM(require("node:path"));
11550
13002
  var kind4 = "personalizations";
11551
13003
  function body2(command, value, opts) {
11552
13004
  let parsed = {};
11553
13005
  if (value) {
11554
13006
  const source = value.startsWith("@") ? value.slice(1) : value;
11555
- 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;
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;
11556
13008
  try {
11557
13009
  parsed = JSON.parse(text);
11558
13010
  } catch (e) {
@@ -11582,7 +13034,7 @@ function detail4(x) {
11582
13034
  console.log(` State: ${x.status}`);
11583
13035
  console.log();
11584
13036
  }
11585
- var list7 = new Command("list").alias("ls").description("List personalizations").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
13037
+ var list9 = new Command("list").alias("ls").description("List personalizations").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
11586
13038
  const r = await listManagedDeliveries(
11587
13039
  requireCredentials(),
11588
13040
  kind4,
@@ -11609,7 +13061,7 @@ var list7 = new Command("list").alias("ls").description("List personalizations")
11609
13061
  )
11610
13062
  );
11611
13063
  });
11612
- var get5 = new Command("get").description("Get a personalization").argument("<id>").action(async function(id) {
13064
+ var get7 = new Command("get").description("Get a personalization").argument("<id>").action(async function(id) {
11613
13065
  const r = await getManagedDelivery(requireCredentials(), kind4, id);
11614
13066
  if (!r.ok || !r.data)
11615
13067
  fail(
@@ -11649,37 +13101,37 @@ function write4(name) {
11649
13101
  });
11650
13102
  return c;
11651
13103
  }
11652
- function lifecycle2(action24) {
11653
- return new Command(action24).description(
11654
- `${action24[0].toUpperCase()}${action24.slice(1)} a personalization`
13104
+ function lifecycle2(action68) {
13105
+ return new Command(action68).description(
13106
+ `${action68[0].toUpperCase()}${action68.slice(1)} a personalization`
11655
13107
  ).argument("<id>").action(async function(id) {
11656
13108
  const r = await transitionManagedDelivery(
11657
13109
  requireCredentials(),
11658
13110
  kind4,
11659
13111
  id,
11660
- action24
13112
+ action68
11661
13113
  );
11662
13114
  if (!r.ok || !r.data)
11663
13115
  fail(
11664
13116
  this,
11665
- r.error ?? `Failed to ${action24}`,
13117
+ r.error ?? `Failed to ${action68}`,
11666
13118
  classifyStatus(r.status),
11667
13119
  r.errorDetails
11668
13120
  );
11669
13121
  output(this, { personalization: r.data }, () => detail4(r.data));
11670
13122
  });
11671
13123
  }
11672
- var personalizationsCommand = new Command("personalizations").description("Manage personalizations").addCommand(list7).addCommand(get5).addCommand(write4("create")).addCommand(write4("update")).addCommand(lifecycle2("activate")).addCommand(lifecycle2("pause")).addCommand(lifecycle2("archive"));
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"));
11673
13125
 
11674
13126
  // src/commands/targeting-resources.ts
11675
13127
  init_source();
11676
- var import_node_fs17 = __toESM(require("node:fs"));
11677
- var import_node_path15 = __toESM(require("node:path"));
13128
+ var import_node_fs18 = __toESM(require("node:fs"));
13129
+ var import_node_path16 = __toESM(require("node:path"));
11678
13130
  function body3(command, value) {
11679
13131
  if (!value) return {};
11680
13132
  const source = value.startsWith("@") ? value.slice(1) : value;
11681
13133
  try {
11682
- 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;
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;
11683
13135
  return JSON.parse(text);
11684
13136
  } catch (error) {
11685
13137
  fail(
@@ -11758,17 +13210,17 @@ function targetingResourceCommand(kind5) {
11758
13210
  );
11759
13211
  })
11760
13212
  );
11761
- for (const action24 of ["create", "update"]) {
11762
- const command = new Command(action24).description(
11763
- `${action24 === "create" ? "Create" : "Update"} a ${singular3}`
13213
+ for (const action68 of ["create", "update"]) {
13214
+ const command = new Command(action68).description(
13215
+ `${action68 === "create" ? "Create" : "Update"} a ${singular3}`
11764
13216
  );
11765
- if (action24 === "update") command.argument("<id>");
13217
+ if (action68 === "update") command.argument("<id>");
11766
13218
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
11767
13219
  const opts = this.opts();
11768
- const id = action24 === "update" ? String(args[0]) : void 0;
11769
- const value = opts.file ? `@${opts.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
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;
11770
13222
  const requestBody = body3(this, value);
11771
- if (action24 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
13223
+ if (action68 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
11772
13224
  fail(
11773
13225
  this,
11774
13226
  `Updating a ${singular3} requires at least one field.`,
@@ -11776,7 +13228,7 @@ function targetingResourceCommand(kind5) {
11776
13228
  );
11777
13229
  const response = await requestTargetingResource(
11778
13230
  requireCredentials(),
11779
- action24 === "create" ? "POST" : "PATCH",
13231
+ action68 === "create" ? "POST" : "PATCH",
11780
13232
  kind5,
11781
13233
  id,
11782
13234
  requestBody
@@ -11784,7 +13236,7 @@ function targetingResourceCommand(kind5) {
11784
13236
  if (!response.ok || !response.data)
11785
13237
  fail(
11786
13238
  this,
11787
- response.error ?? `Failed to ${action24}`,
13239
+ response.error ?? `Failed to ${action68}`,
11788
13240
  classifyStatus(response.status),
11789
13241
  response.errorDetails
11790
13242
  );
@@ -11793,7 +13245,7 @@ function targetingResourceCommand(kind5) {
11793
13245
  {
11794
13246
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
11795
13247
  },
11796
- () => console.log(source_default.green(`${action24}d ${singular3}`))
13248
+ () => console.log(source_default.green(`${action68}d ${singular3}`))
11797
13249
  );
11798
13250
  });
11799
13251
  root.addCommand(command);
@@ -11880,7 +13332,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
11880
13332
  // src/commands/brand.ts
11881
13333
  init_source();
11882
13334
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
11883
- var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action19() {
13335
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action57() {
11884
13336
  const creds2 = requireCredentials();
11885
13337
  const res = await getBrandContext(creds2);
11886
13338
  if (res.status === 404) {
@@ -11913,7 +13365,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
11913
13365
 
11914
13366
  // src/commands/targeting.ts
11915
13367
  init_source();
11916
- var test = 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 action20(url, opts) {
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) {
11917
13369
  const creds2 = requireCredentials();
11918
13370
  const res = await testTargeting(creds2, url, { status: opts.status });
11919
13371
  if (!res.ok || !res.data) {
@@ -11949,17 +13401,17 @@ var test = new Command("test").description("Evaluate experiment URL targeting fo
11949
13401
  }
11950
13402
  });
11951
13403
  });
11952
- var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test);
13404
+ var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
11953
13405
 
11954
13406
  // src/commands/sos.ts
11955
13407
  init_source();
11956
- var import_node_fs18 = __toESM(require("node:fs"));
11957
- var import_node_path16 = __toESM(require("node:path"));
13408
+ var import_node_fs19 = __toESM(require("node:fs"));
13409
+ var import_node_path17 = __toESM(require("node:path"));
11958
13410
  var SOS_TYPES = /* @__PURE__ */ new Set(["full_handover", "design", "development", "qa", "config", "bug"]);
11959
13411
  function readText(value) {
11960
13412
  if (!value) return void 0;
11961
13413
  if (!value.startsWith("@")) return value;
11962
- return import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), value.slice(1)), "utf8");
13414
+ return import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), value.slice(1)), "utf8");
11963
13415
  }
11964
13416
  function printTicket(ticket, reused) {
11965
13417
  console.log();
@@ -11970,7 +13422,7 @@ function printTicket(ticket, reused) {
11970
13422
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
11971
13423
  console.log();
11972
13424
  }
11973
- var create3 = 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 action21(descriptionArg, opts) {
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) {
11974
13426
  if (!SOS_TYPES.has(opts.type)) {
11975
13427
  fail(this, "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.", 3);
11976
13428
  }
@@ -11988,7 +13440,7 @@ var create3 = new Command("create").description("Create an Apex SOS ticket").opt
11988
13440
  }
11989
13441
  output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
11990
13442
  });
11991
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action22() {
13443
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action60() {
11992
13444
  const creds2 = requireCredentials();
11993
13445
  const res = await listMySos(creds2);
11994
13446
  if (!res.ok || !res.data) {
@@ -12012,18 +13464,18 @@ var mine = new Command("mine").description("List SOS tickets created by this API
12012
13464
  );
12013
13465
  });
12014
13466
  });
12015
- var sosCommand = new Command("sos").description("Create and inspect Apex SOS tickets").addCommand(create3).addCommand(mine);
13467
+ var sosCommand = new Command("sos").description("Create and inspect Apex SOS tickets").addCommand(create4).addCommand(mine);
12016
13468
 
12017
13469
  // src/commands/audit-log.ts
12018
- var list8 = new Command("list").alias("ls").option("--entity-type <type>").option("--entity-id <id>").option("-n, --limit <n>", "Max results", "50").option("--offset <n>", "Pagination offset", "0").action(async function(opts) {
12019
- 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) });
12020
13472
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list audit logs", classifyStatus(res.status));
12021
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])));
12022
13474
  });
12023
- var auditLogCommand = new Command("audit-log").description("Inspect audit logs").addCommand(list8);
13475
+ var auditLogCommand = new Command("audit-log").description("Inspect audit logs").addCommand(list10);
12024
13476
 
12025
13477
  // src/commands/inbox.ts
12026
- var list9 = new Command("list").option("--experiment <id>").action(async function(opts) {
13478
+ var list11 = new Command("list").option("--experiment <id>").action(async function(opts) {
12027
13479
  const r = await listInbox(requireCredentials(), opts.experiment);
12028
13480
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to list inbox", classifyStatus(r.status));
12029
13481
  output(this, r.data, () => printTable(["TIME", "ROLE", "MESSAGE"], r.data.messages.map((m) => [m.created_at, m.role, m.body])));
@@ -12038,16 +13490,16 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
12038
13490
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to mark inbox read", classifyStatus(r.status));
12039
13491
  output(this, r.data, () => console.log(`Marked ${r.data.channelKeys.length} channel(s) read`));
12040
13492
  });
12041
- var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list9).addCommand(send).addCommand(markRead);
13493
+ var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send).addCommand(markRead);
12042
13494
 
12043
13495
  // src/commands/shops.ts
12044
- var import_node_fs19 = __toESM(require("node:fs"));
12045
- var import_node_path17 = __toESM(require("node:path"));
13496
+ var import_node_fs20 = __toESM(require("node:fs"));
13497
+ var import_node_path18 = __toESM(require("node:path"));
12046
13498
  function body4(command, raw) {
12047
13499
  if (!raw) return {};
12048
13500
  try {
12049
13501
  const source = raw.startsWith("@") ? raw.slice(1) : raw;
12050
- const text = raw.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(source), "utf8") : raw;
13502
+ const text = raw.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(source), "utf8") : raw;
12051
13503
  const value = JSON.parse(text);
12052
13504
  if (!value || typeof value !== "object" || Array.isArray(value))
12053
13505
  throw new Error("body must be an object");
@@ -12068,9 +13520,10 @@ function done(command, r, label) {
12068
13520
  classifyStatus(r.status),
12069
13521
  r.errorDetails
12070
13522
  );
12071
- output(command, r.data, () => console.log(JSON.stringify(r.data, null, 2)));
13523
+ output(command, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13524
+ `));
12072
13525
  }
12073
- var list10 = new Command("list").alias("ls").description("List organization shops").action(async function() {
13526
+ var list12 = new Command("list").alias("ls").description("List organization shops").action(async function() {
12074
13527
  const r = await listShops(requireUserCredentials());
12075
13528
  if (!r.ok || !r.data)
12076
13529
  fail(
@@ -12094,10 +13547,10 @@ var list10 = new Command("list").alias("ls").description("List organization shop
12094
13547
  )
12095
13548
  );
12096
13549
  });
12097
- var get6 = new Command("get").description("Get a shop").argument("<id>").action(async function(id) {
13550
+ var get8 = new Command("get").description("Get a shop").argument("<id>").action(async function(id) {
12098
13551
  done(this, await getShop(requireUserCredentials(), id), "get shop");
12099
13552
  });
12100
- var create4 = 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) {
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) {
12101
13554
  const flags = {
12102
13555
  ...o.name ? { name: o.name } : {},
12103
13556
  ...o.domain ? { domain: o.domain } : {},
@@ -12110,10 +13563,19 @@ var create4 = new Command("create").description("Create a shop").argument("[json
12110
13563
  const r = await createShop(requireUserCredentials(), payload);
12111
13564
  done(this, r, "create shop");
12112
13565
  });
12113
- var update2 = new Command("update").description("Update a shop").argument("<id>").argument("<json>", "Inline JSON, @file, or file path").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
12114
- const b = body4(this, raw);
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) {
13567
+ const b = raw === void 0 ? {} : body4(this, raw);
13568
+ if (o.holdoutEnabled && o.holdoutDisabled)
13569
+ fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
13570
+ if (o.holdoutPct !== void 0) {
13571
+ const pct = Number(o.holdoutPct);
13572
+ if (!Number.isFinite(pct) || pct < 0 || pct > 10)
13573
+ fail(this, "--holdout-pct must be between 0 and 10", EXIT_CODES.validation);
13574
+ b.holdout_pct = pct;
13575
+ }
13576
+ if (o.holdoutEnabled || o.holdoutDisabled) b.holdout_v1_enabled = Boolean(o.holdoutEnabled);
12115
13577
  if (!Object.keys(b).length)
12116
- fail(this, "Provide a JSON body.", EXIT_CODES.validation);
13578
+ fail(this, "Provide a JSON body or holdout flags.", EXIT_CODES.validation);
12117
13579
  if (Object.hasOwn(b, "tier") && !o.confirmTier)
12118
13580
  fail(
12119
13581
  this,
@@ -12140,10 +13602,10 @@ function settings(name, description, toggle) {
12140
13602
  async function(id, raw) {
12141
13603
  const opts = this.opts();
12142
13604
  const on = toggle ? Boolean(opts[toggle.on]) : false;
12143
- const off = toggle ? Boolean(opts[toggle.off]) : false;
12144
- if (on && off)
13605
+ const off3 = toggle ? Boolean(opts[toggle.off]) : false;
13606
+ if (on && off3)
12145
13607
  fail(this, `Use only one of --${toggle.on} or --${toggle.off}`, EXIT_CODES.validation);
12146
- const flagBody = on || off ? { enabled: on } : void 0;
13608
+ const flagBody = on || off3 ? { enabled: on } : void 0;
12147
13609
  const patchBody = raw !== void 0 || flagBody !== void 0 ? { ...raw !== void 0 ? body4(this, raw) : {}, ...flagBody ?? {} } : void 0;
12148
13610
  const r = patchBody !== void 0 ? await patchShopArea(requireUserCredentials(), id, name, patchBody) : await getShopArea(requireUserCredentials(), id, name);
12149
13611
  if (!r.ok || !r.data)
@@ -12153,7 +13615,8 @@ function settings(name, description, toggle) {
12153
13615
  classifyStatus(r.status),
12154
13616
  r.errorDetails
12155
13617
  );
12156
- output(this, r.data, () => console.log(JSON.stringify(r.data, null, 2)));
13618
+ output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13619
+ `));
12157
13620
  }
12158
13621
  );
12159
13622
  }
@@ -12194,7 +13657,8 @@ var reviewFact = new Command("review-fact").argument("<id>").argument("<fact-id>
12194
13657
  classifyStatus(r.status),
12195
13658
  r.errorDetails
12196
13659
  );
12197
- output(this, r.data, () => console.log(JSON.stringify(r.data, null, 2)));
13660
+ output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13661
+ `));
12198
13662
  });
12199
13663
  var reviewFinding = new Command("review-finding").argument("<id>").argument("<json>").action(async function(id, raw) {
12200
13664
  const r = await postShopArea(
@@ -12210,7 +13674,8 @@ var reviewFinding = new Command("review-finding").argument("<id>").argument("<js
12210
13674
  classifyStatus(r.status),
12211
13675
  r.errorDetails
12212
13676
  );
12213
- output(this, r.data, () => console.log(JSON.stringify(r.data, null, 2)));
13677
+ output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13678
+ `));
12214
13679
  });
12215
13680
  var startLearningRun = new Command("start-learning-run").argument("<id>").argument("[json]").action(async function(id, raw) {
12216
13681
  const r = await postShopArea(
@@ -12226,7 +13691,8 @@ var startLearningRun = new Command("start-learning-run").argument("<id>").argume
12226
13691
  classifyStatus(r.status),
12227
13692
  r.errorDetails
12228
13693
  );
12229
- output(this, r.data, () => console.log(JSON.stringify(r.data, null, 2)));
13694
+ output(this, r.data, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13695
+ `));
12230
13696
  });
12231
13697
  profile.addCommand(reviewFact).addCommand(reviewFinding).addCommand(startLearningRun);
12232
13698
  var shopware = new Command("shopware").description("Manage Shopware bindings");
@@ -12243,7 +13709,7 @@ shopware.addCommand(
12243
13709
  output(this, result.data, () => console.log(`Pre-bound ${options.shopUrl} to ${id}`));
12244
13710
  })
12245
13711
  );
12246
- var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list10).addCommand(get6).addCommand(create4).addCommand(update2).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);
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);
12247
13713
 
12248
13714
  // src/commands/schema.ts
12249
13715
  function optionSchema(option) {
@@ -12274,25 +13740,26 @@ function commandSchema(command) {
12274
13740
  };
12275
13741
  }
12276
13742
  function createSchemaCommand(program3) {
12277
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action24() {
13743
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action68() {
12278
13744
  const schema = commandSchema(program3);
12279
13745
  output(this, schema, () => {
12280
- console.log(JSON.stringify(schema, null, 2));
13746
+ process.stdout.write(`${JSON.stringify(schema, null, 2)}
13747
+ `);
12281
13748
  });
12282
13749
  });
12283
13750
  }
12284
13751
 
12285
13752
  // src/commands/skills.ts
12286
- var import_node_fs20 = __toESM(require("node:fs"));
12287
- var import_node_path18 = __toESM(require("node:path"));
13753
+ var import_node_fs21 = __toESM(require("node:fs"));
13754
+ var import_node_path19 = __toESM(require("node:path"));
12288
13755
  function packageRoot() {
12289
13756
  const candidates = [
12290
- import_node_path18.default.resolve(__dirname, ".."),
12291
- import_node_path18.default.resolve(import_node_path18.default.dirname(process.argv[1] ?? process.cwd()), ".."),
12292
- import_node_path18.default.resolve(process.cwd(), "packages/cli")
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")
12293
13760
  ];
12294
13761
  for (const candidate of candidates) {
12295
- if (import_node_fs20.default.existsSync(import_node_path18.default.join(candidate, "AGENTS.md"))) return candidate;
13762
+ if (import_node_fs21.default.existsSync(import_node_path19.default.join(candidate, "AGENTS.md"))) return candidate;
12296
13763
  }
12297
13764
  throw new Error(
12298
13765
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -12301,24 +13768,24 @@ function packageRoot() {
12301
13768
  function skillPaths() {
12302
13769
  const root = packageRoot();
12303
13770
  return {
12304
- agents: import_node_path18.default.join(root, "AGENTS.md"),
12305
- skill: import_node_path18.default.join(root, "skills", "apex-cli", "SKILL.md")
13771
+ agents: import_node_path19.default.join(root, "AGENTS.md"),
13772
+ skill: import_node_path19.default.join(root, "skills", "apex-cli", "SKILL.md")
12306
13773
  };
12307
13774
  }
12308
13775
  function installSkill(paths, targetRoot) {
12309
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs20.default.existsSync(filePath));
13776
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs21.default.existsSync(filePath));
12310
13777
  if (missing.length > 0) {
12311
13778
  throw new Error(
12312
13779
  `Apex CLI package files not found. Missing: ${missing.join(", ")}`
12313
13780
  );
12314
13781
  }
12315
- const target = import_node_path18.default.resolve(targetRoot, "apex-cli");
12316
- import_node_fs20.default.mkdirSync(target, { recursive: true });
12317
- import_node_fs20.default.copyFileSync(paths.skill, import_node_path18.default.join(target, "SKILL.md"));
12318
- import_node_fs20.default.copyFileSync(paths.agents, import_node_path18.default.join(target, "AGENTS.md"));
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"));
12319
13786
  return target;
12320
13787
  }
12321
- 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 action23(opts) {
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) {
12322
13789
  let paths;
12323
13790
  try {
12324
13791
  paths = skillPaths();
@@ -12352,7 +13819,7 @@ var settingsGet = new Command("settings").description("Show platform operator se
12352
13819
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
12353
13820
  output(this, res.data, () => printTable(["SETTING", "VALUE"], Object.entries(res.data.data).map(([k, v]) => [k, v ? "on" : "off"])));
12354
13821
  });
12355
- var set = 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) {
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) {
12356
13823
  if (!opts.guardian && !opts.srmCheck) fail(this, "Provide --guardian or --srm-check", 3);
12357
13824
  let body5 = {};
12358
13825
  try {
@@ -12369,7 +13836,7 @@ var set = new Command("set").description("Set platform kill switches").option("-
12369
13836
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
12370
13837
  output(this, res.data, () => console.log("Platform settings updated."));
12371
13838
  });
12372
- settingsGet.addCommand(set);
13839
+ settingsGet.addCommand(set4);
12373
13840
  var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
12374
13841
  const res = await getAdminQuarantine(requireCredentials());
12375
13842
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
@@ -12379,10 +13846,11 @@ var adminCommand = new Command("admin").description("Operate allowlisted platfor
12379
13846
 
12380
13847
  // src/commands/membership.ts
12381
13848
  init_source();
12382
- async function run(c, p, label) {
13849
+ async function run2(c, p, label) {
12383
13850
  const r = await p;
12384
13851
  if (!r.ok) fail(c, r.error ?? `Failed to ${label}`, classifyStatus(r.status), r.errorDetails);
12385
- output(c, r.data ?? {}, () => console.log(JSON.stringify(r.data, null, 2)));
13852
+ output(c, r.data ?? {}, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
13853
+ `));
12386
13854
  }
12387
13855
  async function confirm(c, force, label, retry) {
12388
13856
  if (force) return true;
@@ -12399,43 +13867,43 @@ async function confirm(c, force, label, retry) {
12399
13867
  }
12400
13868
  var orgCommand = new Command("org").description("Manage the current organization");
12401
13869
  orgCommand.command("get").action(async function() {
12402
- await run(this, getOrg(requireUserCredentials()), "get organization");
13870
+ await run2(this, getOrg(requireUserCredentials()), "get organization");
12403
13871
  });
12404
13872
  orgCommand.command("update").option("--name <name>").option("--slug <slug>").option("--billing-email <email>").action(async function(o) {
12405
- await run(this, updateOrg(requireUserCredentials(), { ...o.name ? { name: o.name } : {}, ...o.slug ? { slug: o.slug } : {}, ...o.billingEmail ? { billing_email: o.billingEmail } : {} }), "update organization");
13873
+ await run2(this, updateOrg(requireUserCredentials(), { ...o.name ? { name: o.name } : {}, ...o.slug ? { slug: o.slug } : {}, ...o.billingEmail ? { billing_email: o.billingEmail } : {} }), "update organization");
12406
13874
  });
12407
13875
  orgCommand.command("members").action(async function() {
12408
- await run(this, listOrgMembers(requireUserCredentials()), "list members");
13876
+ await run2(this, listOrgMembers(requireUserCredentials()), "list members");
12409
13877
  });
12410
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) {
12411
- await run(this, inviteOrg(requireUserCredentials(), email, o.role ?? o.orgRole, o.defaultShopRole), "invite member");
13879
+ await run2(this, inviteOrg(requireUserCredentials(), email, o.role ?? o.orgRole, o.defaultShopRole), "invite member");
12412
13880
  });
12413
13881
  orgCommand.command("revoke-invite").argument("<id>").action(async function(id) {
12414
- await run(this, revokeOrgInvite(requireUserCredentials(), id), "revoke invitation");
13882
+ await run2(this, revokeOrgInvite(requireUserCredentials(), id), "revoke invitation");
12415
13883
  });
12416
13884
  orgCommand.command("set-role").argument("<memberId>").argument("<role>").option("--default-shop-role <role>").action(async function(id, role, o) {
12417
- await run(this, setOrgMember(requireUserCredentials(), id, { role, ...o.defaultShopRole ? { default_shop_role: o.defaultShopRole } : {} }), "set role");
13885
+ await run2(this, setOrgMember(requireUserCredentials(), id, { role, ...o.defaultShopRole ? { default_shop_role: o.defaultShopRole } : {} }), "set role");
12418
13886
  });
12419
13887
  orgCommand.command("remove-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
12420
13888
  if (!await confirm(this, o.force, `Remove organization member ${id}`, `apex org remove-member ${id} --force`)) return;
12421
- await run(this, removeOrgMember(requireUserCredentials(), id), "remove member");
13889
+ await run2(this, removeOrgMember(requireUserCredentials(), id), "remove member");
12422
13890
  });
12423
13891
  var teamCommand = new Command("team").description("Manage a shop team");
12424
13892
  teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
12425
- await run(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
13893
+ await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
12426
13894
  });
12427
13895
  teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").requiredOption("--role <role>").action(async function(email, o) {
12428
- await run(this, inviteTeam(requireUserCredentials(), email, o.shop, o.role), "invite team member");
13896
+ await run2(this, inviteTeam(requireUserCredentials(), email, o.shop, o.role), "invite team member");
12429
13897
  });
12430
13898
  teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id>").action(async function(id, o) {
12431
- await run(this, revokeTeamInvite(requireUserCredentials(), id, o.shop), "revoke invitation");
13899
+ await run2(this, revokeTeamInvite(requireUserCredentials(), id, o.shop), "revoke invitation");
12432
13900
  });
12433
13901
  teamCommand.command("set-role").argument("<memberId>").argument("<role>").requiredOption("--shop <id>").action(async function(id, role, o) {
12434
- await run(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
13902
+ await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
12435
13903
  });
12436
13904
  teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
12437
13905
  if (!await confirm(this, o.force, `Remove team member ${id}`, `apex team remove ${id} --shop ${o.shop} --force`)) return;
12438
- await run(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
13906
+ await run2(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
12439
13907
  });
12440
13908
 
12441
13909
  // src/commands/account.ts
@@ -12444,7 +13912,7 @@ function userCredentials2() {
12444
13912
  if (!creds2.userApiKey) throw new CliError('User authentication required. Run "apex login --user".', EXIT_CODES.auth);
12445
13913
  return creds2;
12446
13914
  }
12447
- var set2 = new Command("set").option("--name <text>", "Display name").option("--avatar <url>", "Avatar data URL").action(async function(opts) {
13915
+ var set5 = new Command("set").option("--name <text>", "Display name").option("--avatar <url>", "Avatar data URL").action(async function(opts) {
12448
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);
12449
13917
  const res = await setAccountProfile(userCredentials2(), { ...opts.name !== void 0 ? { displayName: opts.name } : {}, ...opts.avatar !== void 0 ? { avatarDataUrl: opts.avatar } : {} });
12450
13918
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update account profile", classifyStatus(res.status));
@@ -12455,7 +13923,7 @@ var profile2 = new Command("profile").description("Show the current account prof
12455
13923
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to get account profile", classifyStatus(res.status));
12456
13924
  output(this, res.data, () => console.log(`${res.data.data.displayName ?? "(no name)"}
12457
13925
  ${res.data.data.avatarDataUrl ?? "(no avatar)"}`));
12458
- }).addCommand(set2);
13926
+ }).addCommand(set5);
12459
13927
  var accountCommand = new Command("account").description("Manage your Apex account").addCommand(profile2);
12460
13928
 
12461
13929
  // src/commands/keys.ts
@@ -12464,12 +13932,12 @@ function creds() {
12464
13932
  if (!value.userApiKey) throw new CliError('User authentication required. Run "apex login --user".', EXIT_CODES.auth);
12465
13933
  return value;
12466
13934
  }
12467
- var list11 = new Command("list").requiredOption("--shop <id>").action(async function(opts) {
13935
+ var list13 = new Command("list").requiredOption("--shop <id>").action(async function(opts) {
12468
13936
  const res = await listUserManagedKeys(creds(), opts.shop);
12469
13937
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list API keys", classifyStatus(res.status));
12470
13938
  output(this, res.data, () => printTable(["ID", "NAME", "PREFIX", "TYPE"], res.data.data.map((key) => [key.id, key.name, key.key_prefix, key.type])));
12471
13939
  });
12472
- var create5 = new Command("create").requiredOption("--shop <id>").requiredOption("--name <name>").requiredOption("--type <type>").option("--permissions <csv>").action(async function(opts) {
13940
+ var create6 = new Command("create").requiredOption("--shop <id>").requiredOption("--name <name>").requiredOption("--type <type>").option("--permissions <csv>").action(async function(opts) {
12473
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);
12474
13942
  const permissions = opts.permissions?.split(",").map((item) => item.trim()).filter(Boolean);
12475
13943
  const res = await createUserManagedKey(creds(), { shop_id: opts.shop, name: opts.name, type: opts.type, ...permissions?.length ? { permissions } : {} });
@@ -12484,13 +13952,311 @@ var revoke = new Command("revoke").argument("<keyId>").requiredOption("--shop <i
12484
13952
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to revoke API key", classifyStatus(res.status));
12485
13953
  output(this, res.data, () => console.log(`Revoked ${keyId}`));
12486
13954
  });
12487
- var keysCommand = new Command("keys").description("Manage shop API keys").addCommand(list11).addCommand(create5).addCommand(revoke);
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);
12488
14253
 
12489
14254
  // src/commands/config-resources.ts
12490
- var import_node_fs21 = __toESM(require("node:fs"));
12491
- var import_node_path19 = __toESM(require("node:path"));
14255
+ var import_node_fs22 = __toESM(require("node:fs"));
14256
+ var import_node_path20 = __toESM(require("node:path"));
12492
14257
  function show(resource) {
12493
- console.log(JSON.stringify(resource, null, 2));
14258
+ process.stdout.write(`${JSON.stringify(resource, null, 2)}
14259
+ `);
12494
14260
  }
12495
14261
  var ASSET_MIME_TYPES = {
12496
14262
  ".png": "image/png",
@@ -12520,7 +14286,7 @@ async function confirmAssetDelete(id) {
12520
14286
  return answer.trim().toLowerCase() === "y";
12521
14287
  }
12522
14288
  function readFamily(family, label) {
12523
- const list12 = new Command("list").alias("ls").description(`List ${label}`).option("-n, --limit <n>", "Max results", "50").option("--cursor <id>", "Pagination cursor").action(async function(o) {
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) {
12524
14290
  const limit = Number(o.limit);
12525
14291
  if (!Number.isInteger(limit) || limit <= 0)
12526
14292
  fail(
@@ -12553,7 +14319,7 @@ function readFamily(family, label) {
12553
14319
  )
12554
14320
  );
12555
14321
  });
12556
- const get7 = new Command("get").description(`Get one ${label.slice(0, -1)}`).argument("<id>").action(async function(id) {
14322
+ const get9 = new Command("get").description(`Get one ${label.slice(0, -1)}`).argument("<id>").action(async function(id) {
12557
14323
  const r = await getConfigResource(requireCredentials(), family, id);
12558
14324
  if (!r.ok || !r.data)
12559
14325
  fail(
@@ -12568,18 +14334,18 @@ function readFamily(family, label) {
12568
14334
  () => show(r.data)
12569
14335
  );
12570
14336
  });
12571
- return { list: list12, get: get7 };
14337
+ return { list: list15, get: get9 };
12572
14338
  }
12573
14339
  var assetReads = readFamily("assets", "assets");
12574
14340
  var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
12575
- const absolute = import_node_path19.default.resolve(process.cwd(), file);
12576
- if (!import_node_fs21.default.existsSync(absolute))
14341
+ const absolute = import_node_path20.default.resolve(process.cwd(), file);
14342
+ if (!import_node_fs22.default.existsSync(absolute))
12577
14343
  fail(this, `File not found: ${file}`, 3);
12578
- const fileStat = import_node_fs21.default.statSync(absolute);
14344
+ const fileStat = import_node_fs22.default.statSync(absolute);
12579
14345
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
12580
14346
  if (fileStat.size > MAX_ASSET_SIZE_BYTES)
12581
14347
  fail(this, `${file} is larger than 10 MB`, 3);
12582
- const mimeType = ASSET_MIME_TYPES[import_node_path19.default.extname(absolute).toLowerCase()];
14348
+ const mimeType = ASSET_MIME_TYPES[import_node_path20.default.extname(absolute).toLowerCase()];
12583
14349
  if (!mimeType)
12584
14350
  fail(
12585
14351
  this,
@@ -12588,8 +14354,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
12588
14354
  );
12589
14355
  const r = await uploadAssets(requireCredentials(), [
12590
14356
  {
12591
- name: import_node_path19.default.basename(absolute),
12592
- bytes: import_node_fs21.default.readFileSync(absolute),
14357
+ name: import_node_path20.default.basename(absolute),
14358
+ bytes: import_node_fs22.default.readFileSync(absolute),
12593
14359
  type: mimeType
12594
14360
  }
12595
14361
  ]);
@@ -12602,7 +14368,7 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
12602
14368
  );
12603
14369
  output(this, r.data, () => r.data.assets.forEach(show));
12604
14370
  });
12605
- var remove3 = new Command("delete").alias("rm").description("Delete an asset").argument("<id>").option("-f, --force", "Skip confirmation").action(async function(id, opts) {
14371
+ var remove5 = new Command("delete").alias("rm").description("Delete an asset").argument("<id>").option("-f, --force", "Skip confirmation").action(async function(id, opts) {
12606
14372
  if (!opts.force) {
12607
14373
  const confirmed = await confirmAssetDelete(id);
12608
14374
  if (!confirmed) {
@@ -12623,7 +14389,7 @@ var remove3 = new Command("delete").alias("rm").description("Delete an asset").a
12623
14389
  );
12624
14390
  output(this, r.data, () => console.log(`Deleted ${id}`));
12625
14391
  });
12626
- var assetsCommand = new Command("assets").description("Manage assets").addCommand(assetReads.list).addCommand(assetReads.get).addCommand(upload).addCommand(remove3);
14392
+ var assetsCommand = new Command("assets").description("Manage assets").addCommand(assetReads.list).addCommand(assetReads.get).addCommand(upload).addCommand(remove5);
12627
14393
  var landingReads = readFamily("landing-pages", "landing pages");
12628
14394
  var landingPagesCommand = new Command("landing-pages").description("Read landing pages").addCommand(landingReads.list).addCommand(landingReads.get);
12629
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) {
@@ -12651,12 +14417,14 @@ function addJsonOption(command) {
12651
14417
  }
12652
14418
  var program2 = new Command().name("apex").description(
12653
14419
  source_default.bold("Apex CLI") + " \u2014 A/B testing experiments from the command line"
12654
- ).version(CLI_VERSION).showSuggestionAfterError(false).showHelpAfterError(false);
14420
+ ).version(CLI_VERSION).showSuggestionAfterError(true).showHelpAfterError(true);
12655
14421
  program2.addCommand(loginCommand);
12656
14422
  program2.addCommand(logoutCommand);
12657
14423
  program2.addCommand(whoamiCommand);
12658
14424
  program2.addCommand(accountCommand);
12659
14425
  program2.addCommand(keysCommand);
14426
+ program2.addCommand(prospectCommand);
14427
+ program2.addCommand(webhooksCommand);
12660
14428
  program2.addCommand(experimentsCommand);
12661
14429
  program2.addCommand(pushCommand);
12662
14430
  program2.addCommand(previewCommand);
@@ -12665,7 +14433,11 @@ program2.addCommand(harnessCommand);
12665
14433
  program2.addCommand(operatorCommand);
12666
14434
  program2.addCommand(trackingCommand);
12667
14435
  program2.addCommand(goalsCommand);
14436
+ program2.addCommand(funnelsCommand);
12668
14437
  program2.addCommand(environmentsCommand);
14438
+ program2.addCommand(consentCommand);
14439
+ program2.addCommand(domainsCommand);
14440
+ program2.addCommand(handoverCommand);
12669
14441
  program2.addCommand(analyticsCommand);
12670
14442
  program2.addCommand(flagsCommand);
12671
14443
  program2.addCommand(backlogCommand);