@drip-apex/apex-cli 0.15.3 → 0.16.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
@@ -6564,11 +6564,11 @@ var init_is_wsl = __esm({
6564
6564
  });
6565
6565
 
6566
6566
  // node_modules/wsl-utils/index.js
6567
- var import_node_process9, import_promises2, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath;
6567
+ var import_node_process9, import_promises, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath;
6568
6568
  var init_wsl_utils = __esm({
6569
6569
  "node_modules/wsl-utils/index.js"() {
6570
6570
  import_node_process9 = __toESM(require("node:process"), 1);
6571
- import_promises2 = __toESM(require("node:fs/promises"), 1);
6571
+ import_promises = __toESM(require("node:fs/promises"), 1);
6572
6572
  init_is_wsl();
6573
6573
  init_is_wsl();
6574
6574
  wslDrivesMountPoint = /* @__PURE__ */ (() => {
@@ -6581,14 +6581,14 @@ var init_wsl_utils = __esm({
6581
6581
  const configFilePath = "/etc/wsl.conf";
6582
6582
  let isConfigFileExists = false;
6583
6583
  try {
6584
- await import_promises2.default.access(configFilePath, import_promises2.constants.F_OK);
6584
+ await import_promises.default.access(configFilePath, import_promises.constants.F_OK);
6585
6585
  isConfigFileExists = true;
6586
6586
  } catch {
6587
6587
  }
6588
6588
  if (!isConfigFileExists) {
6589
6589
  return defaultMountPoint;
6590
6590
  }
6591
- const configContent = await import_promises2.default.readFile(configFilePath, { encoding: "utf8" });
6591
+ const configContent = await import_promises.default.readFile(configFilePath, { encoding: "utf8" });
6592
6592
  const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
6593
6593
  if (!configMountPoint) {
6594
6594
  return defaultMountPoint;
@@ -6823,7 +6823,7 @@ function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
6823
6823
  }
6824
6824
  return detectArchBinary(platformBinary);
6825
6825
  }
6826
- var import_node_process13, import_node_buffer, import_node_path4, import_node_url, import_node_util5, import_node_child_process5, import_promises3, import_meta, execFile5, __dirname2, localXdgOpenPath, platform, arch, pTryEach, baseOpen, open, openApp, apps, open_default;
6826
+ var import_node_process13, import_node_buffer, import_node_path4, import_node_url, import_node_util5, import_node_child_process5, import_promises2, import_meta, execFile5, __dirname2, localXdgOpenPath, platform, arch, pTryEach, baseOpen, open, openApp, apps, open_default;
6827
6827
  var init_open = __esm({
6828
6828
  "node_modules/open/index.js"() {
6829
6829
  import_node_process13 = __toESM(require("node:process"), 1);
@@ -6832,7 +6832,7 @@ var init_open = __esm({
6832
6832
  import_node_url = require("node:url");
6833
6833
  import_node_util5 = require("node:util");
6834
6834
  import_node_child_process5 = __toESM(require("node:child_process"), 1);
6835
- import_promises3 = __toESM(require("node:fs/promises"), 1);
6835
+ import_promises2 = __toESM(require("node:fs/promises"), 1);
6836
6836
  init_wsl_utils();
6837
6837
  init_define_lazy_prop();
6838
6838
  init_default_browser();
@@ -6965,7 +6965,7 @@ var init_open = __esm({
6965
6965
  const isBundled = !__dirname2 || __dirname2 === "/";
6966
6966
  let exeLocalXdgOpen = false;
6967
6967
  try {
6968
- await import_promises3.default.access(localXdgOpenPath, import_promises3.constants.X_OK);
6968
+ await import_promises2.default.access(localXdgOpenPath, import_promises2.constants.X_OK);
6969
6969
  exeLocalXdgOpen = true;
6970
6970
  } catch {
6971
6971
  }
@@ -7085,6 +7085,9 @@ var {
7085
7085
  Help
7086
7086
  } = import_index.default;
7087
7087
 
7088
+ // src/telemetry.ts
7089
+ var import_node_crypto2 = require("node:crypto");
7090
+
7088
7091
  // ../api-client/src/client-identity.ts
7089
7092
  var import_node_crypto = require("node:crypto");
7090
7093
  var invocationId = (0, import_node_crypto.randomUUID)();
@@ -7137,18 +7140,25 @@ function createHttpClient(creds, options) {
7137
7140
  warnedAboutVersionSkew = true;
7138
7141
  warn3(version > 1 ? "CLI is behind the server \u2014 update.\n" : "Server API is older than this CLI supports.\n");
7139
7142
  }
7140
- async function request2(method, path26, body5, requestTimeoutMs, form = false) {
7143
+ async function request2(method, path26, body5, requestTimeoutMs, form = false, requestOptions) {
7141
7144
  const started2 = performance.now();
7142
- const observe = (status4, requestId2, errorCode) => {
7145
+ const observe = (status4, requestId2, errorCode, responseHeaders) => {
7146
+ const semver = (name) => {
7147
+ const value = responseHeaders?.get(name) ?? void 0;
7148
+ return value && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value) ? value : void 0;
7149
+ };
7150
+ const serverTiming = responseHeaders?.get("server-timing");
7143
7151
  try {
7144
- options.onResponse?.({ method, path: path26, status: status4, requestId: requestId2, errorCode: typeof errorCode === "string" ? errorCode : void 0, durationMs: Math.max(0, Math.round(performance.now() - started2)) });
7152
+ options.onResponse?.({ method, path: path26, status: status4, requestId: requestId2, errorCode: typeof errorCode === "string" ? errorCode : void 0, durationMs: Math.max(0, Math.round(performance.now() - started2)), serverTiming: serverTiming?.slice(0, 300) || void 0, cliLatest: semver("x-apex-cli-latest"), cliMinimum: semver("x-apex-cli-minimum") });
7145
7153
  } catch {
7146
7154
  }
7147
7155
  };
7148
7156
  const fallback2 = form ? 12e4 : 3e4;
7157
+ const hasConfiguredTimeout = process.env.APEX_TIMEOUT_MS !== void 0;
7149
7158
  const configured = Number(process.env.APEX_TIMEOUT_MS ?? fallback2);
7150
7159
  const defaultMs = Number.isFinite(configured) && configured > 0 ? configured : fallback2;
7151
- const timeoutMs = requestTimeoutMs === void 0 ? defaultMs : Math.max(1, Math.min(defaultMs, requestTimeoutMs));
7160
+ const minimumMs = !hasConfiguredTimeout && requestOptions?.minTimeoutMs !== void 0 ? Math.max(defaultMs, requestOptions.minTimeoutMs) : defaultMs;
7161
+ const timeoutMs = requestTimeoutMs === void 0 ? minimumMs : Math.max(1, Math.min(minimumMs, requestTimeoutMs));
7152
7162
  const headers = { ...clientIdentityHeaders(), Authorization: `Bearer ${creds.apiKey}` };
7153
7163
  if (!form) headers["Content-Type"] = "application/json";
7154
7164
  if (creds.credentialKind === "user" || creds.apiKey.startsWith("apx_uk_")) {
@@ -7168,6 +7178,7 @@ function createHttpClient(creds, options) {
7168
7178
  } catch (error) {
7169
7179
  const outer = error && typeof error === "object" ? error : void 0;
7170
7180
  const nested = outer?.cause && typeof outer.cause === "object" ? outer.cause : void 0;
7181
+ if (options.observeNetworkErrors) observe(0);
7171
7182
  return options.onNetworkError({
7172
7183
  baseUrl: creds.baseUrl,
7173
7184
  timeoutMs,
@@ -7180,11 +7191,11 @@ function createHttpClient(creds, options) {
7180
7191
  inspectVersion(response.headers);
7181
7192
  const json = await response.json().catch(() => null);
7182
7193
  const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-vercel-id") ?? response.headers.get("cf-ray");
7183
- observe(response.status, requestId, extractErrorDetails(json)?.code);
7194
+ observe(response.status, requestId, extractErrorDetails(json)?.code, response.headers);
7184
7195
  if (!response.ok) return { ok: false, status: response.status, data: json, error: extractErrorMessage(json, response.status), errorDetails: extractErrorDetails(json), requestId, retryAfter: response.headers.get("retry-after"), rateLimit: { limit: response.headers.get("x-ratelimit-limit"), remaining: response.headers.get("x-ratelimit-remaining"), reset: response.headers.get("x-ratelimit-reset") } };
7185
7196
  return { ok: true, status: response.status, data: json };
7186
7197
  }
7187
- return { request: (method, path26, body5, timeoutMs) => request2(method, path26, body5, timeoutMs), requestForm: (method, path26, body5) => request2(method, path26, body5, void 0, true) };
7198
+ return { request: (method, path26, body5, timeoutMs, requestOptions) => request2(method, path26, body5, timeoutMs, false, requestOptions), requestForm: (method, path26, body5) => request2(method, path26, body5, void 0, true) };
7188
7199
  }
7189
7200
 
7190
7201
  // ../api-client/src/credentials.ts
@@ -7233,90 +7244,177 @@ var import_node_os4 = __toESM(require("node:os"));
7233
7244
 
7234
7245
  // src/update.ts
7235
7246
  var import_node_fs2 = require("node:fs");
7236
- var import_promises = require("node:fs/promises");
7237
7247
  var import_node_os2 = require("node:os");
7238
7248
  var import_node_path2 = __toESM(require("node:path"));
7249
+ var UPGRADE_COMMAND = "npm install -g @drip-apex/apex-cli@latest";
7250
+ var NOTES_URL = "https://docs.drip-apex.com/docs/cli/commands-output";
7239
7251
  var TTL = 24 * 60 * 60 * 1e3;
7240
7252
  var FAILURE_TTL = 60 * 60 * 1e3;
7253
+ var SERVER_TTL = 7 * 24 * 60 * 60 * 1e3;
7254
+ var cachePath = () => import_node_path2.default.join((0, import_node_os2.homedir)(), ".drip", "update-check.json");
7255
+ var npmLatest;
7256
+ var serverLatest;
7257
+ var serverMinimum;
7258
+ var VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
7241
7259
  function versionParts(version) {
7242
- return /^\d+\.\d+\.\d+$/.test(version) ? version.split(".").map(Number) : void 0;
7260
+ const match2 = VERSION_PATTERN.exec(version);
7261
+ return match2 ? match2.slice(1, 4).map(Number) : void 0;
7262
+ }
7263
+ function prereleaseOf(version) {
7264
+ return VERSION_PATTERN.exec(version)?.[4]?.split(".") ?? [];
7265
+ }
7266
+ function comparePrerelease(left, right) {
7267
+ if (!left.length || !right.length) return right.length - left.length;
7268
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
7269
+ const a = left[index];
7270
+ const b = right[index];
7271
+ if (a === b) continue;
7272
+ const aNumeric = /^\d+$/.test(a);
7273
+ const bNumeric = /^\d+$/.test(b);
7274
+ if (aNumeric && bNumeric) return Number(a) - Number(b);
7275
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
7276
+ return a < b ? -1 : 1;
7277
+ }
7278
+ return left.length - right.length;
7279
+ }
7280
+ function compareVersions(left, right) {
7281
+ const a = versionParts(left);
7282
+ const b = versionParts(right);
7283
+ if (!a || !b) return 0;
7284
+ for (let index = 0; index < 3; index++) if (a[index] !== b[index]) return a[index] - b[index];
7285
+ return comparePrerelease(prereleaseOf(left), prereleaseOf(right));
7286
+ }
7287
+ function readCache() {
7288
+ try {
7289
+ const value = JSON.parse((0, import_node_fs2.readFileSync)(cachePath(), "utf8"));
7290
+ return value && typeof value === "object" ? value : {};
7291
+ } catch {
7292
+ return {};
7293
+ }
7294
+ }
7295
+ function writeCache(patch, remove5 = []) {
7296
+ try {
7297
+ const value = { ...readCache(), ...patch };
7298
+ for (const key of remove5) delete value[key];
7299
+ (0, import_node_fs2.mkdirSync)(import_node_path2.default.dirname(cachePath()), { recursive: true });
7300
+ (0, import_node_fs2.writeFileSync)(cachePath(), JSON.stringify(value), { mode: 384 });
7301
+ } catch {
7302
+ }
7303
+ }
7304
+ function serverFresh(checkedAt) {
7305
+ if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt)) return false;
7306
+ const age = Date.now() - checkedAt;
7307
+ return age >= 0 && age < SERVER_TTL;
7308
+ }
7309
+ function serverAges(cache) {
7310
+ return {
7311
+ latest: cache.server_latest_checked_at ?? cache.server_checked_at,
7312
+ minimum: cache.server_minimum_checked_at ?? cache.server_checked_at
7313
+ };
7314
+ }
7315
+ function loadServerCache() {
7316
+ const cache = readCache();
7317
+ const ages = serverAges(cache);
7318
+ if (serverFresh(ages.latest) && cache.server_latest && versionParts(cache.server_latest))
7319
+ serverLatest = cache.server_latest;
7320
+ if (serverFresh(ages.minimum) && cache.server_minimum && versionParts(cache.server_minimum))
7321
+ serverMinimum = cache.server_minimum;
7322
+ }
7323
+ loadServerCache();
7324
+ function recordServerCliPolicy(observation) {
7325
+ const latestSeen = Boolean(observation.cliLatest && versionParts(observation.cliLatest));
7326
+ const minimumSeen = Boolean(observation.cliMinimum && versionParts(observation.cliMinimum));
7327
+ if (!latestSeen && !minimumSeen) return;
7328
+ if (latestSeen && (!serverLatest || compareVersions(observation.cliLatest, serverLatest) > 0))
7329
+ serverLatest = observation.cliLatest;
7330
+ if (minimumSeen) serverMinimum = observation.cliMinimum;
7331
+ const now = Date.now();
7332
+ const ages = serverAges(readCache());
7333
+ const patch = {};
7334
+ const remove5 = ["server_checked_at"];
7335
+ if (latestSeen)
7336
+ Object.assign(patch, { server_latest: serverLatest, server_latest_checked_at: now });
7337
+ else if (serverLatest && serverFresh(ages.latest))
7338
+ Object.assign(patch, { server_latest: serverLatest, server_latest_checked_at: ages.latest });
7339
+ else remove5.push("server_latest", "server_latest_checked_at");
7340
+ if (minimumSeen)
7341
+ Object.assign(patch, { server_minimum: serverMinimum, server_minimum_checked_at: now });
7342
+ else if (serverMinimum && serverFresh(ages.minimum))
7343
+ Object.assign(patch, {
7344
+ server_minimum: serverMinimum,
7345
+ server_minimum_checked_at: ages.minimum
7346
+ });
7347
+ else remove5.push("server_minimum", "server_minimum_checked_at");
7348
+ writeCache(patch, remove5);
7349
+ }
7350
+ function clearUpdateCache() {
7351
+ writeCache({}, [
7352
+ "latest",
7353
+ "checked_at",
7354
+ "server_latest",
7355
+ "server_minimum",
7356
+ "server_latest_checked_at",
7357
+ "server_minimum_checked_at",
7358
+ "server_checked_at"
7359
+ ]);
7360
+ npmLatest = serverLatest = serverMinimum = void 0;
7361
+ }
7362
+ function currentUpdateState(current, minimumOverride) {
7363
+ if (minimumOverride && versionParts(minimumOverride))
7364
+ recordServerCliPolicy({ cliMinimum: minimumOverride });
7365
+ const latest = [npmLatest, serverLatest, serverMinimum, current].filter((value) => Boolean(value)).reduce((highest, value) => compareVersions(value, highest) > 0 ? value : highest, current);
7366
+ const minimum = serverMinimum;
7367
+ const required = Boolean(minimum && compareVersions(current, minimum) < 0);
7368
+ if (!required && process.env.APEX_NO_UPDATE_CHECK === "1") return;
7369
+ if (!required && compareVersions(latest, current) <= 0) return;
7370
+ const installed = versionParts(current);
7371
+ const available = versionParts(latest);
7372
+ return {
7373
+ current,
7374
+ latest,
7375
+ ...minimum ? { minimum } : {},
7376
+ required,
7377
+ breaking: available[0] !== installed[0] || installed[0] === 0 && available[1] !== installed[1],
7378
+ command: UPGRADE_COMMAND,
7379
+ notes_url: NOTES_URL
7380
+ };
7243
7381
  }
7244
7382
  async function checkForUpdate(current) {
7245
7383
  if (process.env.APEX_NO_UPDATE_CHECK === "1") return;
7246
7384
  if (process.env[["APEX", "CLI", "MOCK", "API"].join("_")] === "1" || process.env.CI) return;
7247
- const installed = versionParts(current);
7248
- if (!installed) return;
7385
+ if (!versionParts(current)) return;
7386
+ const cache = readCache();
7387
+ const age = Date.now() - (cache.checked_at ?? 0);
7388
+ if (age >= 0 && cache.latest === null && age < FAILURE_TTL) return;
7389
+ if (age >= 0 && age < TTL && cache.latest && versionParts(cache.latest)) {
7390
+ npmLatest = cache.latest;
7391
+ return;
7392
+ }
7249
7393
  const controller = new AbortController();
7250
7394
  let timer;
7251
- const cachePath = import_node_path2.default.join((0, import_node_os2.homedir)(), ".drip", "update-check.json");
7252
- const lookup2 = async () => {
7253
- let latest;
7254
- const remember = async (value) => {
7255
- try {
7256
- await (0, import_promises.mkdir)(import_node_path2.default.dirname(cachePath), { recursive: true });
7257
- await (0, import_promises.writeFile)(cachePath, JSON.stringify({ latest: value, checked_at: Date.now() }), {
7258
- mode: 384
7259
- });
7260
- } catch {
7261
- }
7262
- };
7263
- try {
7264
- const cache = JSON.parse(await (0, import_promises.readFile)(cachePath, "utf8"));
7265
- const age = Date.now() - cache.checked_at;
7266
- if (cache.latest === null && typeof cache.checked_at === "number" && age >= 0 && age < FAILURE_TTL)
7267
- return;
7268
- if (typeof cache.checked_at === "number" && age >= 0 && age < TTL && typeof cache.latest === "string" && versionParts(cache.latest))
7269
- latest = cache.latest;
7270
- } catch {
7271
- }
7272
- if (!latest && !controller.signal.aborted) {
7273
- let body5;
7274
- try {
7275
- const response = await fetch("https://registry.npmjs.org/@drip-apex%2Fapex-cli/latest", {
7276
- headers: { Accept: "application/json" },
7277
- signal: controller.signal
7278
- });
7279
- body5 = response.ok ? await response.json() : void 0;
7280
- } catch {
7281
- body5 = void 0;
7282
- }
7283
- if (typeof body5?.version !== "string" || !versionParts(body5.version)) {
7284
- await remember(null);
7285
- return;
7286
- }
7287
- latest = body5.version;
7288
- await remember(latest);
7289
- }
7290
- if (!latest) return;
7291
- const available = versionParts(latest);
7292
- const difference = available.findIndex((part, index) => part !== installed[index]);
7293
- if (difference < 0 || available[difference] < installed[difference]) return;
7294
- return {
7295
- current,
7296
- latest,
7297
- breaking: available[0] !== installed[0] || installed[0] === 0 && available[1] !== installed[1],
7298
- notes_url: "https://docs.drip-apex.com/docs/cli/commands-output"
7299
- };
7300
- };
7301
7395
  try {
7302
- return await Promise.race([
7303
- lookup2(),
7396
+ const body5 = await Promise.race([
7397
+ fetch("https://registry.npmjs.org/@drip-apex%2Fapex-cli/latest", {
7398
+ headers: { Accept: "application/json" },
7399
+ signal: controller.signal
7400
+ }).then(
7401
+ async (response) => response.ok ? await response.json() : void 0
7402
+ ).catch(() => void 0),
7304
7403
  new Promise((resolve2) => {
7305
7404
  timer = setTimeout(() => {
7306
7405
  controller.abort();
7307
- try {
7308
- (0, import_node_fs2.mkdirSync)(import_node_path2.default.dirname(cachePath), { recursive: true });
7309
- (0, import_node_fs2.writeFileSync)(cachePath, JSON.stringify({ latest: null, checked_at: Date.now() }), {
7310
- mode: 384
7311
- });
7312
- } catch {
7313
- }
7314
7406
  resolve2(void 0);
7315
7407
  }, 900);
7316
7408
  })
7317
7409
  ]);
7410
+ if (!body5?.version || !versionParts(body5.version)) {
7411
+ writeCache({ latest: null, checked_at: Date.now() });
7412
+ return;
7413
+ }
7414
+ npmLatest = body5.version;
7415
+ writeCache({ latest: npmLatest, checked_at: Date.now() });
7318
7416
  } catch {
7319
- return;
7417
+ writeCache({ latest: null, checked_at: Date.now() });
7320
7418
  } finally {
7321
7419
  if (timer) clearTimeout(timer);
7322
7420
  }
@@ -7325,7 +7423,7 @@ async function checkForUpdate(current) {
7325
7423
  // package.json
7326
7424
  var package_default = {
7327
7425
  name: "@drip-apex/apex-cli",
7328
- version: "0.15.3",
7426
+ version: "0.16.0",
7329
7427
  license: "MIT",
7330
7428
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7331
7429
  private: false,
@@ -7383,7 +7481,9 @@ var EXIT_CODES = {
7383
7481
  auth: 2,
7384
7482
  validation: 3,
7385
7483
  confirmationRequired: 4,
7386
- conflict: 5
7484
+ conflict: 5,
7485
+ // A check command ran and reported a definite failing verdict.
7486
+ checkFailed: 6
7387
7487
  };
7388
7488
  var MAX_RECORDED_API_FAILURES = 32;
7389
7489
  var recentApiFailures = [];
@@ -7495,7 +7595,13 @@ function configureRuntime(command) {
7495
7595
  };
7496
7596
  }
7497
7597
  function writeJsonDocument(envelope) {
7498
- const text2 = `${JSON.stringify({ ...envelope, ...updateNotice ? { update: updateNotice } : {} }, null, 2)}
7598
+ const update6 = currentUpdateState(
7599
+ CLI_VERSION,
7600
+ envelope.ok ? void 0 : minimumFromError(envelope.error, envelope.error.reason)
7601
+ );
7602
+ const warnings = update6 ? [updateWarning(update6)] : [];
7603
+ writeUpdateNotice(update6);
7604
+ const text2 = `${JSON.stringify({ ...envelope, ...update6 ? { update: update6 } : {}, ...warnings.length ? { warnings } : {} }, null, 2)}
7499
7605
  `;
7500
7606
  if (wroteEnvelope) {
7501
7607
  process.stderr.write(text2);
@@ -7600,6 +7706,7 @@ var liveSpinners = /* @__PURE__ */ new Set();
7600
7706
  var exitFallback;
7601
7707
  function drainAndExit(code) {
7602
7708
  for (const spinner of liveSpinners) spinner.stop();
7709
+ writePendingUpdateNotice();
7603
7710
  process.exitCode = code;
7604
7711
  if (exitFallback) clearTimeout(exitFallback);
7605
7712
  exitFallback = setTimeout(() => process.exit(code), 500);
@@ -7638,13 +7745,35 @@ async function createSpinner(message) {
7638
7745
  }
7639
7746
  return spinner.start();
7640
7747
  }
7641
- var updateNotice;
7642
7748
  async function maybeCheckForUpdate() {
7643
- updateNotice = await checkForUpdate(CLI_VERSION);
7644
- if (updateNotice)
7645
- process.stderr.write(`Apex CLI ${updateNotice.latest} is available; current ${CLI_VERSION}.
7749
+ await checkForUpdate(CLI_VERSION);
7750
+ }
7751
+ var wroteUpdateNotice = false;
7752
+ function updateWarning(update6) {
7753
+ return update6.required ? {
7754
+ code: "cli_upgrade_required",
7755
+ message: `Apex CLI ${update6.current} is below the minimum supported version ${update6.minimum}. Run: ${UPGRADE_COMMAND} (or apex upgrade), then retry.`
7756
+ } : {
7757
+ code: "cli_update_available",
7758
+ message: `Apex CLI ${update6.latest} is available (current ${update6.current}). Run: ${UPGRADE_COMMAND}`
7759
+ };
7760
+ }
7761
+ function writeUpdateNotice(update6) {
7762
+ if (!update6 || wroteUpdateNotice) return;
7763
+ wroteUpdateNotice = true;
7764
+ process.stderr.write(`${updateWarning(update6).message}
7646
7765
  `);
7647
7766
  }
7767
+ function writePendingUpdateNotice() {
7768
+ writeUpdateNotice(currentUpdateState(CLI_VERSION));
7769
+ }
7770
+ process.on?.("beforeExit", writePendingUpdateNotice);
7771
+ function minimumFromError(error, reason2) {
7772
+ if (reason2 !== "cli_upgrade_required") return;
7773
+ const api = error.api?.body;
7774
+ const value = error.details?.minimum ?? api?.error?.details?.minimum;
7775
+ return typeof value === "string" ? value : void 0;
7776
+ }
7648
7777
  async function handleCliError(err, command) {
7649
7778
  if (err && typeof err === "object" && "exitCode" in err && err.exitCode === 0) {
7650
7779
  flushFallbackEnvelope();
@@ -7676,6 +7805,9 @@ async function handleCliError(err, command) {
7676
7805
  process.stderr.write(`Retry: ${String(cliError.details.retry)}
7677
7806
  `);
7678
7807
  }
7808
+ writeUpdateNotice(
7809
+ currentUpdateState(CLI_VERSION, minimumFromError(cliError, errorReason(cliError)))
7810
+ );
7679
7811
  }
7680
7812
  await finishInvocation({ exitCode: cliError.exitCode, error: err });
7681
7813
  drainAndExit(cliError.exitCode);
@@ -7687,9 +7819,20 @@ function errorReason(error) {
7687
7819
  const body5 = error.api?.body;
7688
7820
  const code = body5?.code ?? body5?.error?.code ?? (error.api ? details?.code : void 0);
7689
7821
  if (typeof code === "string" && code) return code;
7690
- if (["shop_context_required", "local_file_error", "validation_failed", "member_suspended"].includes(
7691
- String(details?.code)
7692
- ))
7822
+ if ([
7823
+ "shop_context_required",
7824
+ "shop_context_mismatch",
7825
+ "install_check_failed",
7826
+ "local_file_error",
7827
+ "validation_failed",
7828
+ "member_suspended",
7829
+ "upgrade_failed",
7830
+ "conflicting_context_input",
7831
+ "context_not_persistable",
7832
+ "context_selection_required",
7833
+ "invalid_window",
7834
+ "output_exists"
7835
+ ].includes(String(details?.code)))
7693
7836
  return String(details?.code);
7694
7837
  const status4 = error.api?.status;
7695
7838
  if (status4 === 401) return "auth_required";
@@ -7705,6 +7848,14 @@ function errorReason(error) {
7705
7848
  function errorNextSteps(error) {
7706
7849
  const reason2 = errorReason(error);
7707
7850
  const steps = [];
7851
+ if (reason2 === "cli_upgrade_required")
7852
+ steps.push(
7853
+ {
7854
+ command: UPGRADE_COMMAND,
7855
+ why: "This CLI version is below the server's minimum for changes; upgrade, then retry the same command."
7856
+ },
7857
+ { command: "apex upgrade", why: "Runs the same install for you." }
7858
+ );
7708
7859
  if (reason2 === "member_suspended")
7709
7860
  return [
7710
7861
  {
@@ -7712,6 +7863,14 @@ function errorNextSteps(error) {
7712
7863
  why: "Ask an organization owner or admin to reinstate your membership."
7713
7864
  }
7714
7865
  ];
7866
+ if (reason2 === "feature_disabled") {
7867
+ const label = typeof error.details?.label === "string" ? error.details.label : "this feature";
7868
+ const setting = typeof error.details?.setting === "string" ? ` (shop setting ${error.details.setting})` : "";
7869
+ steps.push({
7870
+ command: "ask your Apex contact",
7871
+ why: `Turn on ${label} for this shop${setting}.`
7872
+ });
7873
+ }
7715
7874
  if (reason2 === "auth_required" || error.api?.status === 401) {
7716
7875
  steps.push({ command: "apex login", why: "Authenticate with a valid API key." });
7717
7876
  if (!error.api)
@@ -7751,8 +7910,10 @@ function errorNextSteps(error) {
7751
7910
  const hints = error.details?.next_steps;
7752
7911
  if (Array.isArray(hints))
7753
7912
  for (const hint of hints) {
7754
- if (hint && typeof hint.command === "string" && typeof hint.why === "string")
7755
- steps.push({ command: hint.command, why: hint.why });
7913
+ if (hint && typeof hint.command === "string" && typeof hint.why === "string") {
7914
+ if (!steps.some((step) => step.command === hint.command))
7915
+ steps.push({ command: hint.command, why: hint.why });
7916
+ }
7756
7917
  }
7757
7918
  if (reason2 === "validation_failed" && error.details?.plan)
7758
7919
  steps.push({
@@ -7869,7 +8030,7 @@ function clearCredentials() {
7869
8030
  } catch {
7870
8031
  }
7871
8032
  }
7872
- function requireCredentials() {
8033
+ function requireCredentials(fallbackShopId) {
7873
8034
  const creds = getCredentials();
7874
8035
  if (!creds) {
7875
8036
  throw new CliError(
@@ -7878,6 +8039,7 @@ function requireCredentials() {
7878
8039
  );
7879
8040
  }
7880
8041
  const context = resolveContext(creds, void 0, defaultCredentialKind(creds));
8042
+ if (!context.shopId) context.shopId = fallbackShopId?.trim() || void 0;
7881
8043
  if (defaultCredentialKind(creds) === "user") {
7882
8044
  setEnvelopeContext({
7883
8045
  credential: "user",
@@ -7917,6 +8079,53 @@ function requireCredentials() {
7917
8079
  });
7918
8080
  return { ...creds, credentialKind: "shop", resolvedContext: context };
7919
8081
  }
8082
+ function resolveCheckShop(explicitShopId, fallbackShopId) {
8083
+ const explicit = explicitShopId?.trim() || null;
8084
+ const fallback2 = fallbackShopId?.trim() || null;
8085
+ const creds = requireCredentials(explicit ?? fallback2 ?? void 0);
8086
+ const selectedFlag = argvOption("shop")?.trim() || null;
8087
+ if (explicit && selectedFlag && explicit.toLowerCase() !== selectedFlag.toLowerCase()) {
8088
+ throw new CliError(
8089
+ `--shop-id ${explicit} does not match --shop ${selectedFlag}; pass the same shop to both or drop one.`,
8090
+ EXIT_CODES.validation,
8091
+ {
8092
+ code: "shop_context_mismatch",
8093
+ field: "shop-id",
8094
+ requested_shop_id: explicit,
8095
+ selected_shop_id: selectedFlag
8096
+ }
8097
+ );
8098
+ }
8099
+ const previous = creds.resolvedContext?.shopId?.trim() || null;
8100
+ const shopId = explicit ?? previous ?? fallback2;
8101
+ if (creds.credentialKind !== "user" || !shopId) return { creds, shopId };
8102
+ const resolvedContext = { ...creds.resolvedContext, shopId };
8103
+ setEnvelopeContext({
8104
+ credential: "user",
8105
+ org_id: resolvedContext.orgId ?? null,
8106
+ shop_id: shopId
8107
+ });
8108
+ return {
8109
+ creds: { ...creds, resolvedContext },
8110
+ shopId,
8111
+ ...previous && previous.toLowerCase() !== shopId.toLowerCase() ? { overriddenShopId: previous } : {}
8112
+ };
8113
+ }
8114
+ function assertResponseShop(apiShopId, requestedShopId, credentialKind) {
8115
+ const api = apiShopId?.trim();
8116
+ const requested = requestedShopId?.trim();
8117
+ if (!requested || api && api.toLowerCase() === requested.toLowerCase()) return;
8118
+ throw new CliError(
8119
+ !api ? `The API returned no shop identity; cannot verify that it answered for the requested shop ${requested}.` : credentialKind === "shop" ? `This API key is bound to shop ${api}, not the requested shop ${requested}; use a key for that shop or drop --shop-id.` : `The API answered for shop ${api}, not the requested shop ${requested}.`,
8120
+ EXIT_CODES.validation,
8121
+ {
8122
+ code: "shop_context_mismatch",
8123
+ field: "shop-id",
8124
+ requested_shop_id: requested,
8125
+ api_shop_id: api || null
8126
+ }
8127
+ );
8128
+ }
7920
8129
  function requireUserCredentials() {
7921
8130
  const creds = getCredentials();
7922
8131
  const environmentUserKey = creds?.credentialKind === "user" ? creds.apiKey : void 0;
@@ -7977,7 +8186,65 @@ var invocation;
7977
8186
  var firstError;
7978
8187
  var apiCalls = 0;
7979
8188
  var finished = false;
8189
+ var resolvedPath = "";
8190
+ var helpPath;
8191
+ var attempted;
8192
+ var apiCallTimings = [];
8193
+ var entity;
8194
+ function registerTelemetryProgram(program3) {
8195
+ program3.hook("preAction", (_command, actionCommand) => {
8196
+ resolvedPath = telemetryCommandPath(actionCommand);
8197
+ });
8198
+ const registerHelp = (command) => {
8199
+ command.on("beforeHelp", () => {
8200
+ helpPath = telemetryCommandPath(command);
8201
+ });
8202
+ for (const child of command.commands) registerHelp(child);
8203
+ };
8204
+ registerHelp(program3);
8205
+ }
8206
+ function telemetryCommandPath(command) {
8207
+ return commandPath(command).replace(/^apex(?:\s+|$)/, "");
8208
+ }
8209
+ function recordCommanderExit(error, command) {
8210
+ resolvedPath = telemetryCommandPath(command);
8211
+ attempted = error.code === "commander.unknownCommand" && command.args[0] ? [resolvedPath, redactAttempted(command.args[0])].filter(Boolean).join(" ").slice(0, 200) : resolvedPath || void 0;
8212
+ }
8213
+ var agentHosts = /* @__PURE__ */ new Set([
8214
+ "claude-code",
8215
+ "codex",
8216
+ "cursor",
8217
+ "grok",
8218
+ "gemini",
8219
+ "copilot",
8220
+ "other"
8221
+ ]);
8222
+ function detectAgentHost(env2, tty2) {
8223
+ if (env2.APEX_AGENT_HOST && agentHosts.has(env2.APEX_AGENT_HOST)) return env2.APEX_AGENT_HOST;
8224
+ if (env2.CLAUDECODE === "1") return "claude-code";
8225
+ if (env2.CODEX_THREAD_ID || env2.CODEX_SANDBOX) return "codex";
8226
+ if (env2.CURSOR_AGENT || env2.CURSOR_TRACE_ID) return "cursor";
8227
+ if (Object.keys(env2).some((name) => name.startsWith("GROK_"))) return "grok";
8228
+ if (env2.AI_AGENT) {
8229
+ const token = env2.AI_AGENT.match(/^[a-z-]+/)?.[0];
8230
+ return token && agentHosts.has(token) ? token : "other";
8231
+ }
8232
+ return tty2 ? "terminal" : void 0;
8233
+ }
8234
+ function detectSessionId(env2) {
8235
+ if (/^[A-Za-z0-9._:-]{1,128}$/.test(env2.APEX_SESSION_ID ?? "")) return env2.APEX_SESSION_ID;
8236
+ const hostId = env2.CLAUDE_CODE_SESSION_ID || env2.CODEX_THREAD_ID;
8237
+ return hostId ? `h_${(0, import_node_crypto2.createHash)("sha256").update(hostId).digest("hex").slice(0, 32)}` : void 0;
8238
+ }
7980
8239
  function beginInvocation(command, context = {}) {
8240
+ resolvedPath = telemetryCommandPath(command);
8241
+ helpPath = void 0;
8242
+ attempted = void 0;
8243
+ firstError = void 0;
8244
+ apiCalls = 0;
8245
+ finished = false;
8246
+ apiCallTimings.length = 0;
8247
+ entity = void 0;
7981
8248
  const names = /* @__PURE__ */ new Set();
7982
8249
  for (let current = command; current; current = current.parent) {
7983
8250
  for (const option of current.options) {
@@ -8030,7 +8297,9 @@ var staticRouteSegments = /* @__PURE__ */ new Set([
8030
8297
  "duplicate",
8031
8298
  "entry-exit",
8032
8299
  "environments",
8300
+ "events",
8033
8301
  "evidence",
8302
+ "export",
8034
8303
  "exclusion-groups",
8035
8304
  "experiments",
8036
8305
  "external-pixels",
@@ -8124,6 +8393,24 @@ function normalizeRoute(path26) {
8124
8393
  }
8125
8394
  function recordApiResponse2(response) {
8126
8395
  apiCalls++;
8396
+ if (apiCallTimings.length < 25)
8397
+ apiCallTimings.push({
8398
+ method: response.method,
8399
+ route: normalizeRoute(response.path),
8400
+ status: response.status,
8401
+ duration_ms: response.durationMs,
8402
+ ...response.serverTiming ? { server_timing: response.serverTiming } : {}
8403
+ });
8404
+ if (!entity) {
8405
+ const match2 = response.path.match(
8406
+ /^\/api\/v1\/(experiments|landing-pages)\/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})(?=\/|\?|$)/i
8407
+ );
8408
+ if (match2)
8409
+ entity = {
8410
+ entity_type: match2[1] === "experiments" ? "experiment" : "landing_page",
8411
+ entity_id: match2[2]
8412
+ };
8413
+ }
8127
8414
  if (response.status >= 400 && !firstError)
8128
8415
  firstError = { ...response, path: normalizeRoute(response.path) };
8129
8416
  }
@@ -8133,20 +8420,62 @@ function classifyError(exitCode, error, status4) {
8133
8420
  if (detail5.code === "commander.unknownCommand") return "unknown_command";
8134
8421
  if (detail5.code?.startsWith("commander.")) return "validation";
8135
8422
  if (exitCode === 4) return "confirmation_required";
8423
+ if (exitCode === EXIT_CODES.checkFailed) return "check_failed";
8136
8424
  if (status4 === 401 || status4 === 403 || exitCode === 2) return "auth";
8137
- if (status4 === 404) return "not_found";
8425
+ if (status4 === 404 || status4 === 410) return "not_found";
8138
8426
  if (status4 === 429) return "rate_limited";
8139
8427
  if (status4 === 409 || exitCode === 5) return "conflict";
8140
8428
  if (status4 && status4 >= 500) return "server";
8141
8429
  if (status4 === 400 || status4 === 422 || exitCode === 3) return "validation";
8430
+ if (detail5.details?.reason === "timeout" || detail5.details?.reason === "connect_timeout" || detail5.name === "TimeoutError")
8431
+ return "timeout";
8142
8432
  if (Boolean(
8143
- detail5.details?.reason && ["network", "dns", "timeout", "tls", "refused", "reset", "unknown"].includes(
8144
- detail5.details.reason
8145
- )
8146
- ) || detail5.details?.code === "network_error" || detail5.name === "TimeoutError")
8433
+ detail5.details?.reason && ["network", "dns", "tls", "refused", "reset", "unknown"].includes(detail5.details.reason)
8434
+ ) || detail5.details?.code === "network_error")
8147
8435
  return "network";
8148
8436
  return "internal";
8149
8437
  }
8438
+ function redactAttempted(token) {
8439
+ return /^(?:apx_|sk_|pk_|rk_|ghp_|gho_|xox|Bearer)/.test(token) || /^[A-Za-z0-9_-]{24,}$/.test(token) || /:\/\/|[/\\@=]/.test(token) || token.length > 40 ? "(redacted)" : token;
8440
+ }
8441
+ function normalizeErrorCode(value, exitCode) {
8442
+ const normalized = value.toLowerCase();
8443
+ return normalized.length <= 64 && /^[a-z0-9._-]+$/.test(normalized) ? normalized : `exit_${exitCode}`;
8444
+ }
8445
+ function invocationErrorCode(exitCode, error, commanderExit) {
8446
+ if (exitCode === 0) return void 0;
8447
+ let value = firstError?.errorCode;
8448
+ if (!value && firstError)
8449
+ value = firstError.status === 410 ? "gone" : `http_${firstError.status}`;
8450
+ const detail5 = error && typeof error === "object" ? error : {};
8451
+ if (!value && typeof detail5.details?.reason === "string" && !detail5.api) {
8452
+ const reason2 = detail5.details.reason.toLowerCase();
8453
+ value = [
8454
+ "network",
8455
+ "dns",
8456
+ "timeout",
8457
+ "connect_timeout",
8458
+ "tls",
8459
+ "refused",
8460
+ "reset",
8461
+ "unknown"
8462
+ ].includes(reason2) ? `network_${reason2}` : reason2 || void 0;
8463
+ }
8464
+ if (!value && commanderExit && typeof detail5.code === "string")
8465
+ value = detail5.code.slice("commander.".length).replace(/([a-z])([A-Z])/g, "$1_$2");
8466
+ if (!value) {
8467
+ const apiCode = detail5.api?.body?.code ?? detail5.api?.body?.error?.code ?? detail5.details?.code;
8468
+ if (typeof apiCode === "string" && apiCode) value = apiCode;
8469
+ else if (detail5.api?.status === 401 || exitCode === 2) value = "auth_required";
8470
+ else if (detail5.api?.status === 403) value = "auth_forbidden";
8471
+ else if (detail5.api?.status === 404) value = "not_found";
8472
+ else if (detail5.api?.status === 429) value = "rate_limited";
8473
+ else if (detail5.api?.status === 409 || exitCode === 5) value = "conflict";
8474
+ else if (exitCode === 4) value = "confirmation_required";
8475
+ else if (exitCode === 3) value = "validation_failed";
8476
+ }
8477
+ return normalizeErrorCode(value ?? `exit_${exitCode}`, exitCode);
8478
+ }
8150
8479
  async function finishInvocation({
8151
8480
  exitCode: rawExitCode,
8152
8481
  error
@@ -8168,10 +8497,8 @@ async function finishInvocation({
8168
8497
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
8169
8498
  const commanderExit = code.startsWith("commander.");
8170
8499
  const parse3 = commanderExit && exitCode !== 0;
8171
- const token = commanderExit ? process.argv.slice(2).find((token2) => !token2.startsWith("-")) : void 0;
8172
- const attempted = token && (/^(?:apx_|sk_|pk_|rk_|ghp_|gho_|xox|Bearer)/.test(token) || /^[A-Za-z0-9_-]{24,}$/.test(token) || /:\/\/|[/\\@=]/.test(token) || token.length > 40) ? "(redacted)" : token;
8173
- if (attempted === "login" || attempted === "logout") return;
8174
- const command = parse3 ? "(parse)" : invocation?.command ?? (commanderExit && exitCode === 0 ? attempted ?? (code === "commander.version" ? "(version)" : "(help)") : void 0);
8500
+ if (resolvedPath === "login" || resolvedPath === "logout") return;
8501
+ const command = parse3 ? "(parse)" : invocation?.command ?? (commanderExit && exitCode === 0 ? code === "commander.version" ? "(version)" : ["(help)", helpPath].filter(Boolean).join(" ") : void 0);
8175
8502
  if (!command || command === "login" || command === "logout") return;
8176
8503
  if (!getClientIdentity()) setClientIdentity({ client: "apex-cli", version: CLI_VERSION });
8177
8504
  const payload = {
@@ -8185,8 +8512,12 @@ async function finishInvocation({
8185
8512
  exit_code: exitCode,
8186
8513
  error_class: classifyError(exitCode, error, firstError?.status),
8187
8514
  api_calls: apiCalls,
8515
+ ...apiCallTimings.length ? { api_call_timings: [...apiCallTimings] } : {},
8516
+ ...entity,
8517
+ ...detectAgentHost(process.env, process.stdout.isTTY === true) ? { agent_host: detectAgentHost(process.env, process.stdout.isTTY === true) } : {},
8518
+ ...detectSessionId(process.env) ? { session_id: detectSessionId(process.env) } : {},
8188
8519
  first_error_status: firstError?.status,
8189
- first_error_code: firstError?.errorCode,
8520
+ first_error_code: invocationErrorCode(exitCode, error, commanderExit),
8190
8521
  first_error_request_id: firstError?.requestId,
8191
8522
  first_error_route: firstError?.path,
8192
8523
  duration_ms: Math.max(0, Math.round(performance.now() - started))
@@ -8207,13 +8538,13 @@ init_source();
8207
8538
 
8208
8539
  // src/commands/login.ts
8209
8540
  init_source();
8210
- var import_node_crypto3 = __toESM(require("node:crypto"));
8541
+ var import_node_crypto4 = __toESM(require("node:crypto"));
8211
8542
  var import_node_http = __toESM(require("node:http"));
8212
8543
  var import_node_os6 = __toESM(require("node:os"));
8213
8544
  var import_node_readline2 = __toESM(require("node:readline"));
8214
8545
 
8215
8546
  // src/api.ts
8216
- var import_node_crypto2 = require("node:crypto");
8547
+ var import_node_crypto3 = require("node:crypto");
8217
8548
 
8218
8549
  // ../api-client/src/landing-pages.ts
8219
8550
  var LANDING_PAGE_MANIFEST_FILE = "apex-landing-page.json";
@@ -8378,7 +8709,11 @@ function sharedClient(creds) {
8378
8709
  let client = sharedClients.get(creds);
8379
8710
  if (!client) {
8380
8711
  client = createHttpClient(creds, {
8381
- onResponse: recordApiResponse2,
8712
+ onResponse: (observation) => {
8713
+ recordApiResponse2(observation);
8714
+ recordServerCliPolicy?.(observation);
8715
+ },
8716
+ observeNetworkErrors: true,
8382
8717
  transport: testTransport,
8383
8718
  testTransportVersion: () => testTransport ? process.env[["APEX", "CLI", "MOCK", "API", "VERSION"].join("_")] : void 0,
8384
8719
  onNetworkError: (info) => {
@@ -8428,8 +8763,14 @@ function sharedClient(creds) {
8428
8763
  }
8429
8764
  return client;
8430
8765
  }
8431
- async function request(creds, method, path26, body5, requestTimeoutMs) {
8432
- const response = await sharedClient(creds).request(method, path26, body5, requestTimeoutMs);
8766
+ async function request(creds, method, path26, body5, requestTimeoutMs, options) {
8767
+ const response = await sharedClient(creds).request(
8768
+ method,
8769
+ path26,
8770
+ body5,
8771
+ requestTimeoutMs,
8772
+ options
8773
+ );
8433
8774
  recordApiResponse(response);
8434
8775
  return response;
8435
8776
  }
@@ -8534,6 +8875,7 @@ function planExperimentFilesystem(creds, id, body5) {
8534
8875
  body5
8535
8876
  );
8536
8877
  }
8878
+ var APPLY_TIMEOUT_MS = 9e4;
8537
8879
  function applyExperimentFilesystem(creds, id, body5) {
8538
8880
  return request(
8539
8881
  creds,
@@ -8543,7 +8885,9 @@ function applyExperimentFilesystem(creds, id, body5) {
8543
8885
  files: body5.files,
8544
8886
  ...body5.accept_qa_invalidation !== void 0 ? { accept_qa_invalidation: body5.accept_qa_invalidation } : {},
8545
8887
  ...body5.confirmTargetingReplace === true ? { confirmTargetingReplace: true } : {}
8546
- }
8888
+ },
8889
+ void 0,
8890
+ { minTimeoutMs: APPLY_TIMEOUT_MS }
8547
8891
  );
8548
8892
  }
8549
8893
  function getExperimentQaRun(creds, id) {
@@ -8560,6 +8904,23 @@ function getExperimentQaEvents(creds, id, options = {}) {
8560
8904
  `/api/v1/experiments/${id}/qa/events${params.size ? `?${params}` : ""}`
8561
8905
  );
8562
8906
  }
8907
+ async function runExperimentCommerceProbes(creds, id, kinds) {
8908
+ const response = await request(
8909
+ creds,
8910
+ "POST",
8911
+ `/api/v1/experiments/${id}/qa/commerce-probes`,
8912
+ kinds?.length ? { kinds } : {}
8913
+ );
8914
+ return { ...response, data: response.data?.data };
8915
+ }
8916
+ async function getExperimentCommerceProbes(creds, id) {
8917
+ const response = await request(
8918
+ creds,
8919
+ "GET",
8920
+ `/api/v1/experiments/${id}/qa/commerce-probes`
8921
+ );
8922
+ return { ...response, data: response.data?.data };
8923
+ }
8563
8924
  function updateExperimentQaRun(creds, id, body5) {
8564
8925
  return request(creds, "PATCH", `/api/v1/experiments/${id}/qa/run`, body5);
8565
8926
  }
@@ -8569,8 +8930,9 @@ function attachExperimentQaEvidence(creds, id, body5) {
8569
8930
  function uploadExperimentQaEvidence(creds, id, form) {
8570
8931
  return requestForm(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, form);
8571
8932
  }
8572
- function approveExperimentQa(creds, id) {
8573
- return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
8933
+ function approveExperimentQa(creds, id, acknowledgement) {
8934
+ if (!acknowledgement) return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
8935
+ return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`, acknowledgement);
8574
8936
  }
8575
8937
  function reopenExperimentQa(creds, id) {
8576
8938
  return request(creds, "DELETE", `/api/v1/experiments/${id}/qa`);
@@ -8770,7 +9132,7 @@ function testTargeting(creds, url, opts) {
8770
9132
  return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
8771
9133
  }
8772
9134
  function createSos(creds, body5, options = {}) {
8773
- const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto2.createHash)("sha256").update(
9135
+ const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto3.createHash)("sha256").update(
8774
9136
  [
8775
9137
  body5.type,
8776
9138
  body5.experiment_id ?? "",
@@ -9267,6 +9629,16 @@ function resolveSelection(tree, input) {
9267
9629
  const shop = shopValue ? match(shopValue, org.shops, "shop") : void 0;
9268
9630
  return { orgId: org.id, ...shop ? { shopId: shop.id } : {} };
9269
9631
  }
9632
+ function resolveFlagSelection(tree, values) {
9633
+ if (values.org) {
9634
+ const org = match(values.org, tree.orgs, "organization");
9635
+ const shop2 = values.shop ? match(values.shop, org.shops, "shop") : void 0;
9636
+ return { orgId: org.id, ...shop2 ? { shopId: shop2.id } : {} };
9637
+ }
9638
+ const shops2 = tree.orgs.flatMap((org) => org.shops.map((shop2) => ({ ...shop2, orgId: org.id })));
9639
+ const shop = match(values.shop, shops2, "shop");
9640
+ return { orgId: shop.orgId, shopId: shop.id };
9641
+ }
9270
9642
  function ask(question) {
9271
9643
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
9272
9644
  return new Promise(
@@ -9279,8 +9651,23 @@ function ask(question) {
9279
9651
  async function pickContext(tree) {
9280
9652
  if (!process.stdin.isTTY || !process.stdout.isTTY)
9281
9653
  throw new CliError(
9282
- "Interactive context selection requires a TTY. Pass an org/shop to apex use, or use --org/--shop.",
9283
- EXIT_CODES.validation
9654
+ "Interactive context selection requires a TTY. Pass <org>[/<shop>] to apex use, or use --org/--shop.",
9655
+ EXIT_CODES.validation,
9656
+ {
9657
+ code: "context_selection_required",
9658
+ candidates: tree.orgs.slice(0, 20).map((org2) => ({
9659
+ org_id: org2.id,
9660
+ org_name: org2.name,
9661
+ shops: org2.shops.slice(0, 20).map((shop2) => ({ id: shop2.id, name: shop2.name }))
9662
+ })),
9663
+ truncated: tree.orgs.length > 20 || tree.orgs.some((org2) => org2.shops.length > 20),
9664
+ next_steps: [
9665
+ {
9666
+ command: "apex use <org-id>/<shop-id>",
9667
+ why: "Choose one of error.details.candidates."
9668
+ }
9669
+ ]
9670
+ }
9284
9671
  );
9285
9672
  let org = tree.orgs.length === 1 ? tree.orgs[0] : void 0;
9286
9673
  if (!org) {
@@ -9400,7 +9787,7 @@ function parseFormBody(body5) {
9400
9787
  return new URLSearchParams(body5);
9401
9788
  }
9402
9789
  async function waitForBrowserLogin(baseUrl, userMode = false) {
9403
- const state = import_node_crypto3.default.randomBytes(24).toString("base64url");
9790
+ const state = import_node_crypto4.default.randomBytes(24).toString("base64url");
9404
9791
  const clientName = import_node_os6.default.hostname() || "Local machine";
9405
9792
  return new Promise((resolve2, reject) => {
9406
9793
  let settled = false;
@@ -9710,6 +10097,14 @@ Context: ${current.orgId ?? "none"}${current.shopId ? ` / ${current.shopId}` : "
9710
10097
 
9711
10098
  // src/commands/use.ts
9712
10099
  var useCommand = new Command("use").description("Set the current Apex organization and shop context").argument("[context]", "<org-id-or-name>[/shop-id-or-name]").action(async function(input) {
10100
+ const flags = this.optsWithGlobals();
10101
+ if (input && (flags.org || flags.shop))
10102
+ fail(
10103
+ this,
10104
+ "Pass the context either as <org>[/<shop>] or with --org/--shop, not both.",
10105
+ EXIT_CODES.validation,
10106
+ { code: "conflicting_context_input" }
10107
+ );
9713
10108
  const creds = requireUserCredentials();
9714
10109
  const response = await getAccountContext(creds);
9715
10110
  if (!response.ok || !response.data)
@@ -9719,9 +10114,32 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9719
10114
  classifyApiError(response.status, response.errorDetails),
9720
10115
  response.errorDetails
9721
10116
  );
9722
- const selected2 = input ? resolveSelection(response.data.data, input) : await pickContext(response.data.data);
10117
+ const selected2 = input ? resolveSelection(response.data.data, input) : flags.org || flags.shop ? resolveFlagSelection(response.data.data, flags) : await pickContext(response.data.data);
9723
10118
  const stored = getStoredCredentials();
9724
- if (!stored) fail(this, "No stored credential to update. Run apex login first.");
10119
+ if (!stored) {
10120
+ const env2 = {
10121
+ DRIP_ORG: selected2.orgId,
10122
+ ...selected2.shopId ? { DRIP_SHOP: selected2.shopId } : {}
10123
+ };
10124
+ const exportCommand = `export DRIP_ORG=${selected2.orgId}${selected2.shopId ? ` DRIP_SHOP=${selected2.shopId}` : " && unset DRIP_SHOP"}`;
10125
+ fail(
10126
+ this,
10127
+ `apex use cannot save a context: this run authenticates with an API key from the environment (APEX_API_KEY or DRIP_API_KEY) and no apex login credential file exists. Set DRIP_ORG=${selected2.orgId}${selected2.shopId ? ` and DRIP_SHOP=${selected2.shopId}` : " and unset DRIP_SHOP"} in the environment, or pass --org/--shop on each command.`,
10128
+ EXIT_CODES.validation,
10129
+ {
10130
+ code: "context_not_persistable",
10131
+ org_id: selected2.orgId,
10132
+ shop_id: selected2.shopId ?? null,
10133
+ env: env2,
10134
+ next_steps: [
10135
+ {
10136
+ command: exportCommand,
10137
+ why: "Selects this context for every later apex command in this environment."
10138
+ }
10139
+ ]
10140
+ }
10141
+ );
10142
+ }
9725
10143
  saveCredentials({ ...stored, context: selected2 });
9726
10144
  setEnvelopeContext({
9727
10145
  credential: "user",
@@ -9737,7 +10155,7 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9737
10155
 
9738
10156
  // src/commands/experiments.ts
9739
10157
  init_source();
9740
- var import_node_crypto5 = __toESM(require("node:crypto"));
10158
+ var import_node_crypto6 = __toESM(require("node:crypto"));
9741
10159
  var import_node_fs8 = __toESM(require("node:fs"));
9742
10160
  var import_node_path6 = __toESM(require("node:path"));
9743
10161
 
@@ -9979,14 +10397,17 @@ function summarizeResults(payload) {
9979
10397
  const decision = record(data.decision);
9980
10398
  const outcome = record(data.effective_outcome);
9981
10399
  const commerce = record(data.commerce);
10400
+ const commerceResults = data.commerce_results == null ? null : record(data.commerce_results);
9982
10401
  const rawArms = Array.isArray(data.arms) ? data.arms.map(record) : [];
9983
10402
  const degradedFlags = {
9984
10403
  explicit: data.degraded === true,
9985
10404
  commerceUnavailable: commerce.unavailable === true,
10405
+ commerceResultsDegraded: commerceResults?.degraded === true,
9986
10406
  armMetricsMissing: rawArms.some((arm) => arm.primaryConversions == null || arm.primaryRate == null)
9987
10407
  };
9988
10408
  const missing = /* @__PURE__ */ new Set();
9989
10409
  if (degradedFlags.commerceUnavailable) missing.add("commerce orders");
10410
+ if (degradedFlags.commerceResultsDegraded) missing.add("commerce profit");
9990
10411
  const controlRate = rawArms.find((arm) => arm.isControl === true)?.primaryRate;
9991
10412
  const treatmentCount = rawArms.filter((arm) => arm.isControl !== true).length;
9992
10413
  const plannedDays = typeof data.planned_days === "number" && Number.isFinite(data.planned_days) && data.planned_days > 0 ? Math.max(1, Math.round(data.planned_days)) : null;
@@ -10033,7 +10454,8 @@ function summarizeResults(payload) {
10033
10454
  })) : [],
10034
10455
  metrics: Array.isArray(data.metrics) ? data.metrics.map(record) : [],
10035
10456
  resultsWeekdays: data.results_weekdays,
10036
- confidenceIndication
10457
+ confidenceIndication,
10458
+ commerceResults
10037
10459
  };
10038
10460
  }
10039
10461
 
@@ -10052,7 +10474,7 @@ function deriveStoreUrl(patterns) {
10052
10474
  }
10053
10475
 
10054
10476
  // src/lib/asset-upload.ts
10055
- var import_node_crypto4 = __toESM(require("node:crypto"));
10477
+ var import_node_crypto5 = __toESM(require("node:crypto"));
10056
10478
  var import_node_fs7 = __toESM(require("node:fs"));
10057
10479
  var import_node_path5 = __toESM(require("node:path"));
10058
10480
  var INLINE_DATA_URI_MAX_CHARS = 4096;
@@ -10077,7 +10499,7 @@ var EXTENSION_BY_MIME = {
10077
10499
  "image/avif": ".avif"
10078
10500
  };
10079
10501
  function contentHash(bytes) {
10080
- return import_node_crypto4.default.createHash("sha256").update(bytes).digest("hex");
10502
+ return import_node_crypto5.default.createHash("sha256").update(bytes).digest("hex");
10081
10503
  }
10082
10504
  function deliveryUrl(baseUrl, asset) {
10083
10505
  const returnedUrl = asset.deliveryUrl ?? asset.delivery_url ?? asset.url;
@@ -10243,6 +10665,1425 @@ var experiment_filesystem_schema_default = {
10243
10665
  $id: "https://api.drip-apex.com/schemas/experiment-filesystem-v1.json",
10244
10666
  $schema: "https://json-schema.org/draft/2020-12/schema",
10245
10667
  files: {
10668
+ "commerce/offer.json": {
10669
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10670
+ oneOf: [
10671
+ {
10672
+ additionalProperties: false,
10673
+ properties: {
10674
+ arms: {
10675
+ items: {
10676
+ anyOf: [
10677
+ {
10678
+ additionalProperties: false,
10679
+ properties: {
10680
+ sameAsControl: {
10681
+ const: true,
10682
+ type: "boolean"
10683
+ },
10684
+ variation: {
10685
+ minLength: 1,
10686
+ type: "string"
10687
+ }
10688
+ },
10689
+ required: ["variation", "sameAsControl"],
10690
+ type: "object"
10691
+ },
10692
+ {
10693
+ additionalProperties: false,
10694
+ properties: {
10695
+ control: {
10696
+ const: true,
10697
+ type: "boolean"
10698
+ },
10699
+ key: {
10700
+ maxLength: 24,
10701
+ minLength: 1,
10702
+ pattern: "^[a-z0-9-]+$",
10703
+ type: "string"
10704
+ },
10705
+ label: {
10706
+ maxLength: 60,
10707
+ minLength: 1,
10708
+ type: "string"
10709
+ },
10710
+ percentOff: {
10711
+ anyOf: [
10712
+ {
10713
+ maximum: 90,
10714
+ minimum: 1,
10715
+ type: "integer"
10716
+ },
10717
+ {
10718
+ type: "null"
10719
+ }
10720
+ ]
10721
+ },
10722
+ variation: {
10723
+ minLength: 1,
10724
+ type: "string"
10725
+ }
10726
+ },
10727
+ required: ["key", "label", "percentOff", "variation"],
10728
+ type: "object"
10729
+ }
10730
+ ]
10731
+ },
10732
+ minItems: 1,
10733
+ type: "array"
10734
+ },
10735
+ kind: {
10736
+ const: "offer",
10737
+ type: "string"
10738
+ },
10739
+ lever: {
10740
+ additionalProperties: false,
10741
+ properties: {
10742
+ catalogSource: {
10743
+ const: "live",
10744
+ type: "string"
10745
+ },
10746
+ currencyCode: {
10747
+ maxLength: 3,
10748
+ minLength: 3,
10749
+ pattern: "^[A-Z]{3}$",
10750
+ type: "string"
10751
+ },
10752
+ offerType: {
10753
+ const: "discount_depth",
10754
+ type: "string"
10755
+ },
10756
+ target: {
10757
+ additionalProperties: false,
10758
+ properties: {
10759
+ productGids: {
10760
+ default: [],
10761
+ items: {
10762
+ pattern: "^gid:\\/\\/shopify\\/Product\\/\\d+$",
10763
+ type: "string"
10764
+ },
10765
+ maxItems: 100,
10766
+ type: "array"
10767
+ },
10768
+ variantGids: {
10769
+ default: [],
10770
+ items: {
10771
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
10772
+ type: "string"
10773
+ },
10774
+ maxItems: 100,
10775
+ type: "array"
10776
+ }
10777
+ },
10778
+ type: "object"
10779
+ }
10780
+ },
10781
+ required: ["offerType", "catalogSource", "currencyCode", "target"],
10782
+ type: "object"
10783
+ },
10784
+ schemaVersion: {
10785
+ const: 1,
10786
+ type: "number"
10787
+ },
10788
+ serverOwned: {
10789
+ additionalProperties: false,
10790
+ properties: {
10791
+ carriers: {
10792
+ additionalProperties: {},
10793
+ propertyNames: {
10794
+ type: "string"
10795
+ },
10796
+ type: "object"
10797
+ },
10798
+ readonly: {
10799
+ const: true,
10800
+ type: "boolean"
10801
+ }
10802
+ },
10803
+ readOnly: true,
10804
+ required: ["readonly", "carriers"],
10805
+ type: "object"
10806
+ }
10807
+ },
10808
+ required: ["schemaVersion", "kind", "lever", "arms"],
10809
+ type: "object"
10810
+ },
10811
+ {
10812
+ additionalProperties: false,
10813
+ properties: {
10814
+ arms: {
10815
+ items: {
10816
+ anyOf: [
10817
+ {
10818
+ additionalProperties: false,
10819
+ properties: {
10820
+ sameAsControl: {
10821
+ const: true,
10822
+ type: "boolean"
10823
+ },
10824
+ variation: {
10825
+ minLength: 1,
10826
+ type: "string"
10827
+ }
10828
+ },
10829
+ required: ["variation", "sameAsControl"],
10830
+ type: "object"
10831
+ },
10832
+ {
10833
+ additionalProperties: false,
10834
+ properties: {
10835
+ control: {
10836
+ const: true,
10837
+ type: "boolean"
10838
+ },
10839
+ key: {
10840
+ maxLength: 24,
10841
+ minLength: 1,
10842
+ pattern: "^[a-z0-9-]+$",
10843
+ type: "string"
10844
+ },
10845
+ label: {
10846
+ maxLength: 60,
10847
+ minLength: 1,
10848
+ type: "string"
10849
+ },
10850
+ threshold: {
10851
+ anyOf: [
10852
+ {
10853
+ exclusiveMinimum: 0,
10854
+ type: "number"
10855
+ },
10856
+ {
10857
+ type: "null"
10858
+ }
10859
+ ]
10860
+ },
10861
+ tiers: {
10862
+ items: {
10863
+ additionalProperties: false,
10864
+ properties: {
10865
+ giftChoiceVariantGids: {
10866
+ items: {
10867
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
10868
+ type: "string"
10869
+ },
10870
+ maxItems: 4,
10871
+ minItems: 2,
10872
+ type: "array"
10873
+ },
10874
+ giftVariantGid: {
10875
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
10876
+ type: "string"
10877
+ },
10878
+ threshold: {
10879
+ exclusiveMinimum: 0,
10880
+ type: "number"
10881
+ }
10882
+ },
10883
+ required: ["threshold", "giftVariantGid"],
10884
+ type: "object"
10885
+ },
10886
+ maxItems: 3,
10887
+ minItems: 1,
10888
+ type: "array"
10889
+ },
10890
+ variation: {
10891
+ minLength: 1,
10892
+ type: "string"
10893
+ }
10894
+ },
10895
+ required: ["key", "label", "variation"],
10896
+ type: "object"
10897
+ }
10898
+ ]
10899
+ },
10900
+ minItems: 1,
10901
+ type: "array"
10902
+ },
10903
+ kind: {
10904
+ const: "offer",
10905
+ type: "string"
10906
+ },
10907
+ lever: {
10908
+ additionalProperties: false,
10909
+ properties: {
10910
+ catalogSource: {
10911
+ const: "live",
10912
+ type: "string"
10913
+ },
10914
+ currencyCode: {
10915
+ maxLength: 3,
10916
+ minLength: 3,
10917
+ pattern: "^[A-Z]{3}$",
10918
+ type: "string"
10919
+ },
10920
+ giftVariantGid: {
10921
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
10922
+ type: "string"
10923
+ },
10924
+ offerType: {
10925
+ const: "gwp_threshold",
10926
+ type: "string"
10927
+ }
10928
+ },
10929
+ required: ["offerType", "catalogSource", "currencyCode"],
10930
+ type: "object"
10931
+ },
10932
+ schemaVersion: {
10933
+ const: 1,
10934
+ type: "number"
10935
+ },
10936
+ serverOwned: {
10937
+ additionalProperties: false,
10938
+ properties: {
10939
+ carriers: {
10940
+ additionalProperties: {},
10941
+ propertyNames: {
10942
+ type: "string"
10943
+ },
10944
+ type: "object"
10945
+ },
10946
+ readonly: {
10947
+ const: true,
10948
+ type: "boolean"
10949
+ }
10950
+ },
10951
+ readOnly: true,
10952
+ required: ["readonly", "carriers"],
10953
+ type: "object"
10954
+ }
10955
+ },
10956
+ required: ["schemaVersion", "kind", "lever", "arms"],
10957
+ type: "object"
10958
+ },
10959
+ {
10960
+ additionalProperties: false,
10961
+ properties: {
10962
+ arms: {
10963
+ items: {
10964
+ anyOf: [
10965
+ {
10966
+ additionalProperties: false,
10967
+ properties: {
10968
+ sameAsControl: {
10969
+ const: true,
10970
+ type: "boolean"
10971
+ },
10972
+ variation: {
10973
+ minLength: 1,
10974
+ type: "string"
10975
+ }
10976
+ },
10977
+ required: ["variation", "sameAsControl"],
10978
+ type: "object"
10979
+ },
10980
+ {
10981
+ additionalProperties: false,
10982
+ properties: {
10983
+ control: {
10984
+ const: true,
10985
+ type: "boolean"
10986
+ },
10987
+ key: {
10988
+ maxLength: 24,
10989
+ minLength: 1,
10990
+ pattern: "^[a-z0-9-]+$",
10991
+ type: "string"
10992
+ },
10993
+ label: {
10994
+ maxLength: 60,
10995
+ minLength: 1,
10996
+ type: "string"
10997
+ },
10998
+ minimumQuantity: {
10999
+ anyOf: [
11000
+ {
11001
+ maximum: 9007199254740991,
11002
+ minimum: 2,
11003
+ type: "integer"
11004
+ },
11005
+ {
11006
+ type: "null"
11007
+ }
11008
+ ]
11009
+ },
11010
+ percentOff: {
11011
+ anyOf: [
11012
+ {
11013
+ maximum: 90,
11014
+ minimum: 1,
11015
+ type: "integer"
11016
+ },
11017
+ {
11018
+ type: "null"
11019
+ }
11020
+ ]
11021
+ },
11022
+ variation: {
11023
+ minLength: 1,
11024
+ type: "string"
11025
+ }
11026
+ },
11027
+ required: ["key", "label", "minimumQuantity", "percentOff", "variation"],
11028
+ type: "object"
11029
+ }
11030
+ ]
11031
+ },
11032
+ minItems: 1,
11033
+ type: "array"
11034
+ },
11035
+ kind: {
11036
+ const: "offer",
11037
+ type: "string"
11038
+ },
11039
+ lever: {
11040
+ additionalProperties: false,
11041
+ properties: {
11042
+ catalogSource: {
11043
+ const: "live",
11044
+ type: "string"
11045
+ },
11046
+ currencyCode: {
11047
+ maxLength: 3,
11048
+ minLength: 3,
11049
+ pattern: "^[A-Z]{3}$",
11050
+ type: "string"
11051
+ },
11052
+ offerType: {
11053
+ const: "quantity_breaks",
11054
+ type: "string"
11055
+ },
11056
+ target: {
11057
+ additionalProperties: false,
11058
+ properties: {
11059
+ productGids: {
11060
+ default: [],
11061
+ items: {
11062
+ pattern: "^gid:\\/\\/shopify\\/Product\\/\\d+$",
11063
+ type: "string"
11064
+ },
11065
+ maxItems: 100,
11066
+ type: "array"
11067
+ },
11068
+ variantGids: {
11069
+ default: [],
11070
+ items: {
11071
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
11072
+ type: "string"
11073
+ },
11074
+ maxItems: 100,
11075
+ type: "array"
11076
+ }
11077
+ },
11078
+ type: "object"
11079
+ }
11080
+ },
11081
+ required: ["offerType", "catalogSource", "currencyCode", "target"],
11082
+ type: "object"
11083
+ },
11084
+ schemaVersion: {
11085
+ const: 1,
11086
+ type: "number"
11087
+ },
11088
+ serverOwned: {
11089
+ additionalProperties: false,
11090
+ properties: {
11091
+ carriers: {
11092
+ additionalProperties: {},
11093
+ propertyNames: {
11094
+ type: "string"
11095
+ },
11096
+ type: "object"
11097
+ },
11098
+ readonly: {
11099
+ const: true,
11100
+ type: "boolean"
11101
+ }
11102
+ },
11103
+ readOnly: true,
11104
+ required: ["readonly", "carriers"],
11105
+ type: "object"
11106
+ }
11107
+ },
11108
+ required: ["schemaVersion", "kind", "lever", "arms"],
11109
+ type: "object"
11110
+ },
11111
+ {
11112
+ additionalProperties: false,
11113
+ properties: {
11114
+ carriers: {
11115
+ additionalProperties: {
11116
+ items: {},
11117
+ type: "array"
11118
+ },
11119
+ propertyNames: {
11120
+ type: "string"
11121
+ },
11122
+ type: "object"
11123
+ },
11124
+ kind: {
11125
+ const: "offer",
11126
+ type: "string"
11127
+ },
11128
+ readonly: {
11129
+ const: true,
11130
+ type: "boolean"
11131
+ },
11132
+ reason: {
11133
+ type: "string"
11134
+ },
11135
+ schemaVersion: {
11136
+ const: 1,
11137
+ type: "number"
11138
+ }
11139
+ },
11140
+ required: ["schemaVersion", "kind", "readonly", "reason", "carriers"],
11141
+ type: "object"
11142
+ }
11143
+ ]
11144
+ },
11145
+ "commerce/post-purchase.json": {
11146
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11147
+ oneOf: [
11148
+ {
11149
+ additionalProperties: false,
11150
+ properties: {
11151
+ arms: {
11152
+ items: {
11153
+ anyOf: [
11154
+ {
11155
+ additionalProperties: false,
11156
+ properties: {
11157
+ sameAsControl: {
11158
+ const: true,
11159
+ type: "boolean"
11160
+ },
11161
+ variation: {
11162
+ minLength: 1,
11163
+ type: "string"
11164
+ }
11165
+ },
11166
+ required: ["variation", "sameAsControl"],
11167
+ type: "object"
11168
+ },
11169
+ {
11170
+ additionalProperties: false,
11171
+ properties: {
11172
+ control: {
11173
+ const: true,
11174
+ type: "boolean"
11175
+ },
11176
+ key: {
11177
+ maxLength: 80,
11178
+ minLength: 1,
11179
+ type: "string"
11180
+ },
11181
+ kind: {
11182
+ const: "control",
11183
+ type: "string"
11184
+ },
11185
+ variation: {
11186
+ minLength: 1,
11187
+ type: "string"
11188
+ }
11189
+ },
11190
+ required: ["kind", "key", "variation"],
11191
+ type: "object"
11192
+ },
11193
+ {
11194
+ additionalProperties: false,
11195
+ properties: {
11196
+ acceptLabel: {
11197
+ maxLength: 60,
11198
+ minLength: 1,
11199
+ type: "string"
11200
+ },
11201
+ control: {
11202
+ const: true,
11203
+ type: "boolean"
11204
+ },
11205
+ declineLabel: {
11206
+ maxLength: 60,
11207
+ minLength: 1,
11208
+ type: "string"
11209
+ },
11210
+ description: {
11211
+ maxLength: 500,
11212
+ type: "string"
11213
+ },
11214
+ discount: {
11215
+ oneOf: [
11216
+ {
11217
+ additionalProperties: false,
11218
+ properties: {
11219
+ kind: {
11220
+ const: "percent",
11221
+ type: "string"
11222
+ },
11223
+ percentOff: {
11224
+ maximum: 90,
11225
+ minimum: 1,
11226
+ type: "integer"
11227
+ }
11228
+ },
11229
+ required: ["kind", "percentOff"],
11230
+ type: "object"
11231
+ },
11232
+ {
11233
+ additionalProperties: false,
11234
+ properties: {
11235
+ amount: {
11236
+ exclusiveMinimum: 0,
11237
+ type: "number"
11238
+ },
11239
+ kind: {
11240
+ const: "fixed_price",
11241
+ type: "string"
11242
+ }
11243
+ },
11244
+ required: ["kind", "amount"],
11245
+ type: "object"
11246
+ }
11247
+ ]
11248
+ },
11249
+ headline: {
11250
+ maxLength: 160,
11251
+ minLength: 1,
11252
+ type: "string"
11253
+ },
11254
+ key: {
11255
+ maxLength: 80,
11256
+ minLength: 1,
11257
+ type: "string"
11258
+ },
11259
+ kind: {
11260
+ const: "offer",
11261
+ type: "string"
11262
+ },
11263
+ productVariantGid: {
11264
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
11265
+ type: "string"
11266
+ },
11267
+ productVariantTitle: {
11268
+ maxLength: 255,
11269
+ minLength: 1,
11270
+ type: "string"
11271
+ },
11272
+ quantity: {
11273
+ const: 1,
11274
+ type: "number"
11275
+ },
11276
+ variation: {
11277
+ minLength: 1,
11278
+ type: "string"
11279
+ }
11280
+ },
11281
+ required: [
11282
+ "kind",
11283
+ "key",
11284
+ "productVariantGid",
11285
+ "quantity",
11286
+ "discount",
11287
+ "variation"
11288
+ ],
11289
+ type: "object"
11290
+ }
11291
+ ]
11292
+ },
11293
+ minItems: 1,
11294
+ type: "array"
11295
+ },
11296
+ kind: {
11297
+ const: "post-purchase",
11298
+ type: "string"
11299
+ },
11300
+ lever: {
11301
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11302
+ additionalProperties: false,
11303
+ properties: {
11304
+ currencyCode: {
11305
+ maxLength: 3,
11306
+ minLength: 3,
11307
+ pattern: "^[A-Z]{3}$",
11308
+ type: "string"
11309
+ }
11310
+ },
11311
+ required: ["currencyCode"],
11312
+ type: "object"
11313
+ },
11314
+ schemaVersion: {
11315
+ const: 1,
11316
+ type: "number"
11317
+ },
11318
+ serverOwned: {
11319
+ additionalProperties: false,
11320
+ properties: {
11321
+ carriers: {
11322
+ additionalProperties: {},
11323
+ propertyNames: {
11324
+ type: "string"
11325
+ },
11326
+ type: "object"
11327
+ },
11328
+ readonly: {
11329
+ const: true,
11330
+ type: "boolean"
11331
+ }
11332
+ },
11333
+ readOnly: true,
11334
+ required: ["readonly", "carriers"],
11335
+ type: "object"
11336
+ }
11337
+ },
11338
+ required: ["schemaVersion", "kind", "lever", "arms"],
11339
+ type: "object"
11340
+ },
11341
+ {
11342
+ additionalProperties: false,
11343
+ properties: {
11344
+ carriers: {
11345
+ additionalProperties: {
11346
+ items: {},
11347
+ type: "array"
11348
+ },
11349
+ propertyNames: {
11350
+ type: "string"
11351
+ },
11352
+ type: "object"
11353
+ },
11354
+ kind: {
11355
+ const: "post-purchase",
11356
+ type: "string"
11357
+ },
11358
+ readonly: {
11359
+ const: true,
11360
+ type: "boolean"
11361
+ },
11362
+ reason: {
11363
+ type: "string"
11364
+ },
11365
+ schemaVersion: {
11366
+ const: 1,
11367
+ type: "number"
11368
+ }
11369
+ },
11370
+ required: ["schemaVersion", "kind", "readonly", "reason", "carriers"],
11371
+ type: "object"
11372
+ }
11373
+ ]
11374
+ },
11375
+ "commerce/price.json": {
11376
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11377
+ oneOf: [
11378
+ {
11379
+ additionalProperties: false,
11380
+ properties: {
11381
+ arms: {
11382
+ items: {
11383
+ anyOf: [
11384
+ {
11385
+ additionalProperties: false,
11386
+ properties: {
11387
+ control: {
11388
+ const: true,
11389
+ type: "boolean"
11390
+ },
11391
+ variation: {
11392
+ minLength: 1,
11393
+ type: "string"
11394
+ }
11395
+ },
11396
+ required: ["variation", "control"],
11397
+ type: "object"
11398
+ },
11399
+ {
11400
+ additionalProperties: false,
11401
+ properties: {
11402
+ sameAsControl: {
11403
+ const: true,
11404
+ type: "boolean"
11405
+ },
11406
+ variation: {
11407
+ minLength: 1,
11408
+ type: "string"
11409
+ }
11410
+ },
11411
+ required: ["variation", "sameAsControl"],
11412
+ type: "object"
11413
+ },
11414
+ {
11415
+ additionalProperties: false,
11416
+ properties: {
11417
+ key: {
11418
+ maxLength: 24,
11419
+ minLength: 1,
11420
+ pattern: "^[a-z0-9-]+$",
11421
+ type: "string"
11422
+ },
11423
+ label: {
11424
+ maxLength: 60,
11425
+ minLength: 1,
11426
+ type: "string"
11427
+ },
11428
+ prices: {
11429
+ additionalProperties: {
11430
+ additionalProperties: false,
11431
+ properties: {
11432
+ compareAt: {
11433
+ oneOf: [
11434
+ {
11435
+ additionalProperties: false,
11436
+ properties: {
11437
+ amount: {
11438
+ exclusiveMinimum: 0,
11439
+ type: "number"
11440
+ },
11441
+ mode: {
11442
+ const: "set",
11443
+ type: "string"
11444
+ }
11445
+ },
11446
+ required: ["mode", "amount"],
11447
+ type: "object"
11448
+ },
11449
+ {
11450
+ additionalProperties: false,
11451
+ properties: {
11452
+ mode: {
11453
+ const: "clear",
11454
+ type: "string"
11455
+ }
11456
+ },
11457
+ required: ["mode"],
11458
+ type: "object"
11459
+ }
11460
+ ]
11461
+ },
11462
+ price: {
11463
+ anyOf: [
11464
+ {
11465
+ exclusiveMinimum: 0,
11466
+ type: "number"
11467
+ },
11468
+ {
11469
+ type: "null"
11470
+ }
11471
+ ]
11472
+ }
11473
+ },
11474
+ required: ["price"],
11475
+ type: "object"
11476
+ },
11477
+ type: "object"
11478
+ },
11479
+ type: {
11480
+ const: "fixed",
11481
+ default: "fixed",
11482
+ type: "string"
11483
+ },
11484
+ variation: {
11485
+ minLength: 1,
11486
+ type: "string"
11487
+ }
11488
+ },
11489
+ required: ["key", "label", "variation", "prices"],
11490
+ type: "object"
11491
+ },
11492
+ {
11493
+ additionalProperties: false,
11494
+ properties: {
11495
+ key: {
11496
+ maxLength: 24,
11497
+ minLength: 1,
11498
+ pattern: "^[a-z0-9-]+$",
11499
+ type: "string"
11500
+ },
11501
+ label: {
11502
+ maxLength: 60,
11503
+ minLength: 1,
11504
+ type: "string"
11505
+ },
11506
+ percentDelta: {
11507
+ maximum: -1,
11508
+ minimum: -90,
11509
+ type: "integer"
11510
+ },
11511
+ prices: {
11512
+ additionalProperties: {
11513
+ additionalProperties: false,
11514
+ properties: {
11515
+ compareAt: {
11516
+ oneOf: [
11517
+ {
11518
+ additionalProperties: false,
11519
+ properties: {
11520
+ amount: {
11521
+ exclusiveMinimum: 0,
11522
+ type: "number"
11523
+ },
11524
+ mode: {
11525
+ const: "set",
11526
+ type: "string"
11527
+ }
11528
+ },
11529
+ required: ["mode", "amount"],
11530
+ type: "object"
11531
+ },
11532
+ {
11533
+ additionalProperties: false,
11534
+ properties: {
11535
+ mode: {
11536
+ const: "clear",
11537
+ type: "string"
11538
+ }
11539
+ },
11540
+ required: ["mode"],
11541
+ type: "object"
11542
+ }
11543
+ ]
11544
+ },
11545
+ price: {
11546
+ anyOf: [
11547
+ {
11548
+ exclusiveMinimum: 0,
11549
+ type: "number"
11550
+ },
11551
+ {
11552
+ type: "null"
11553
+ }
11554
+ ]
11555
+ }
11556
+ },
11557
+ required: ["price"],
11558
+ type: "object"
11559
+ },
11560
+ type: "object"
11561
+ },
11562
+ type: {
11563
+ const: "percent",
11564
+ type: "string"
11565
+ },
11566
+ variation: {
11567
+ minLength: 1,
11568
+ type: "string"
11569
+ }
11570
+ },
11571
+ required: ["key", "label", "type", "percentDelta", "variation", "prices"],
11572
+ type: "object"
11573
+ },
11574
+ {
11575
+ additionalProperties: false,
11576
+ properties: {
11577
+ key: {
11578
+ maxLength: 24,
11579
+ minLength: 1,
11580
+ pattern: "^[a-z0-9-]+$",
11581
+ type: "string"
11582
+ },
11583
+ label: {
11584
+ maxLength: 60,
11585
+ minLength: 1,
11586
+ type: "string"
11587
+ },
11588
+ prices: {
11589
+ additionalProperties: {
11590
+ additionalProperties: false,
11591
+ properties: {
11592
+ compareAt: {
11593
+ oneOf: [
11594
+ {
11595
+ additionalProperties: false,
11596
+ properties: {
11597
+ amount: {
11598
+ exclusiveMinimum: 0,
11599
+ type: "number"
11600
+ },
11601
+ mode: {
11602
+ const: "set",
11603
+ type: "string"
11604
+ }
11605
+ },
11606
+ required: ["mode", "amount"],
11607
+ type: "object"
11608
+ },
11609
+ {
11610
+ additionalProperties: false,
11611
+ properties: {
11612
+ mode: {
11613
+ const: "clear",
11614
+ type: "string"
11615
+ }
11616
+ },
11617
+ required: ["mode"],
11618
+ type: "object"
11619
+ }
11620
+ ]
11621
+ },
11622
+ price: {
11623
+ anyOf: [
11624
+ {
11625
+ exclusiveMinimum: 0,
11626
+ type: "number"
11627
+ },
11628
+ {
11629
+ type: "null"
11630
+ }
11631
+ ]
11632
+ }
11633
+ },
11634
+ required: ["price"],
11635
+ type: "object"
11636
+ },
11637
+ type: "object"
11638
+ },
11639
+ type: {
11640
+ const: "compare-at",
11641
+ type: "string"
11642
+ },
11643
+ variation: {
11644
+ minLength: 1,
11645
+ type: "string"
11646
+ }
11647
+ },
11648
+ required: ["key", "label", "type", "variation", "prices"],
11649
+ type: "object"
11650
+ }
11651
+ ]
11652
+ },
11653
+ minItems: 1,
11654
+ type: "array"
11655
+ },
11656
+ kind: {
11657
+ const: "price",
11658
+ type: "string"
11659
+ },
11660
+ lever: {
11661
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11662
+ additionalProperties: false,
11663
+ properties: {
11664
+ catalogSource: {
11665
+ anyOf: [
11666
+ {
11667
+ const: "sample",
11668
+ type: "string"
11669
+ },
11670
+ {
11671
+ const: "live",
11672
+ type: "string"
11673
+ }
11674
+ ]
11675
+ },
11676
+ currencyCode: {
11677
+ maxLength: 3,
11678
+ minLength: 3,
11679
+ pattern: "^[A-Z]{3}$",
11680
+ type: "string"
11681
+ },
11682
+ productId: {
11683
+ maxLength: 120,
11684
+ minLength: 1,
11685
+ type: "string"
11686
+ },
11687
+ productTitle: {
11688
+ maxLength: 255,
11689
+ minLength: 1,
11690
+ type: "string"
11691
+ },
11692
+ variants: {
11693
+ items: {
11694
+ additionalProperties: false,
11695
+ properties: {
11696
+ controlPrice: {
11697
+ minimum: 0,
11698
+ type: "number"
11699
+ },
11700
+ sku: {
11701
+ maxLength: 120,
11702
+ type: "string"
11703
+ },
11704
+ title: {
11705
+ maxLength: 255,
11706
+ minLength: 1,
11707
+ type: "string"
11708
+ },
11709
+ variantId: {
11710
+ maxLength: 120,
11711
+ minLength: 1,
11712
+ type: "string"
11713
+ }
11714
+ },
11715
+ required: ["variantId", "title", "controlPrice"],
11716
+ type: "object"
11717
+ },
11718
+ minItems: 1,
11719
+ type: "array"
11720
+ }
11721
+ },
11722
+ required: [
11723
+ "catalogSource",
11724
+ "productId",
11725
+ "productTitle",
11726
+ "currencyCode",
11727
+ "variants"
11728
+ ],
11729
+ type: "object"
11730
+ },
11731
+ schemaVersion: {
11732
+ const: 1,
11733
+ type: "number"
11734
+ },
11735
+ serverOwned: {
11736
+ additionalProperties: false,
11737
+ properties: {
11738
+ carriers: {
11739
+ additionalProperties: {},
11740
+ propertyNames: {
11741
+ type: "string"
11742
+ },
11743
+ type: "object"
11744
+ },
11745
+ readonly: {
11746
+ const: true,
11747
+ type: "boolean"
11748
+ }
11749
+ },
11750
+ readOnly: true,
11751
+ required: ["readonly", "carriers"],
11752
+ type: "object"
11753
+ }
11754
+ },
11755
+ required: ["schemaVersion", "kind", "lever", "arms"],
11756
+ type: "object"
11757
+ },
11758
+ {
11759
+ additionalProperties: false,
11760
+ properties: {
11761
+ carriers: {
11762
+ additionalProperties: {
11763
+ items: {},
11764
+ type: "array"
11765
+ },
11766
+ propertyNames: {
11767
+ type: "string"
11768
+ },
11769
+ type: "object"
11770
+ },
11771
+ kind: {
11772
+ const: "price",
11773
+ type: "string"
11774
+ },
11775
+ readonly: {
11776
+ const: true,
11777
+ type: "boolean"
11778
+ },
11779
+ reason: {
11780
+ type: "string"
11781
+ },
11782
+ schemaVersion: {
11783
+ const: 1,
11784
+ type: "number"
11785
+ }
11786
+ },
11787
+ required: ["schemaVersion", "kind", "readonly", "reason", "carriers"],
11788
+ type: "object"
11789
+ }
11790
+ ]
11791
+ },
11792
+ "commerce/shipping.json": {
11793
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11794
+ oneOf: [
11795
+ {
11796
+ additionalProperties: false,
11797
+ properties: {
11798
+ arms: {
11799
+ items: {
11800
+ anyOf: [
11801
+ {
11802
+ additionalProperties: false,
11803
+ properties: {
11804
+ sameAsControl: {
11805
+ const: true,
11806
+ type: "boolean"
11807
+ },
11808
+ variation: {
11809
+ minLength: 1,
11810
+ type: "string"
11811
+ }
11812
+ },
11813
+ required: ["variation", "sameAsControl"],
11814
+ type: "object"
11815
+ },
11816
+ {
11817
+ additionalProperties: false,
11818
+ properties: {
11819
+ bannerCopy: {
11820
+ default: "",
11821
+ maxLength: 200,
11822
+ type: "string"
11823
+ },
11824
+ control: {
11825
+ const: true,
11826
+ type: "boolean"
11827
+ },
11828
+ flatRate: {
11829
+ anyOf: [
11830
+ {
11831
+ minimum: 0,
11832
+ type: "number"
11833
+ },
11834
+ {
11835
+ type: "null"
11836
+ }
11837
+ ],
11838
+ default: null
11839
+ },
11840
+ key: {
11841
+ maxLength: 24,
11842
+ minLength: 1,
11843
+ pattern: "^[a-z0-9-]+$",
11844
+ type: "string"
11845
+ },
11846
+ label: {
11847
+ maxLength: 60,
11848
+ minLength: 1,
11849
+ type: "string"
11850
+ },
11851
+ percentOff: {
11852
+ anyOf: [
11853
+ {
11854
+ maximum: 100,
11855
+ minimum: 1,
11856
+ type: "integer"
11857
+ },
11858
+ {
11859
+ type: "null"
11860
+ }
11861
+ ],
11862
+ default: null
11863
+ },
11864
+ progressCopy: {
11865
+ maxLength: 200,
11866
+ type: "string"
11867
+ },
11868
+ reachedCopy: {
11869
+ maxLength: 200,
11870
+ type: "string"
11871
+ },
11872
+ target: {
11873
+ additionalProperties: false,
11874
+ default: {
11875
+ productGids: [],
11876
+ variantGids: []
11877
+ },
11878
+ properties: {
11879
+ productGids: {
11880
+ default: [],
11881
+ items: {
11882
+ pattern: "^gid:\\/\\/shopify\\/Product\\/\\d+$",
11883
+ type: "string"
11884
+ },
11885
+ maxItems: 100,
11886
+ type: "array"
11887
+ },
11888
+ variantGids: {
11889
+ default: [],
11890
+ items: {
11891
+ pattern: "^gid:\\/\\/shopify\\/ProductVariant\\/\\d+$",
11892
+ type: "string"
11893
+ },
11894
+ maxItems: 100,
11895
+ type: "array"
11896
+ }
11897
+ },
11898
+ type: "object"
11899
+ },
11900
+ threshold: {
11901
+ anyOf: [
11902
+ {
11903
+ minimum: 0,
11904
+ type: "number"
11905
+ },
11906
+ {
11907
+ type: "null"
11908
+ }
11909
+ ]
11910
+ },
11911
+ treatment: {
11912
+ enum: [
11913
+ "progress_bar",
11914
+ "banner",
11915
+ "free_shipping",
11916
+ "flat_rate",
11917
+ "percent_off",
11918
+ "none"
11919
+ ],
11920
+ type: "string"
11921
+ },
11922
+ variation: {
11923
+ minLength: 1,
11924
+ type: "string"
11925
+ }
11926
+ },
11927
+ required: [
11928
+ "key",
11929
+ "label",
11930
+ "threshold",
11931
+ "treatment",
11932
+ "progressCopy",
11933
+ "reachedCopy",
11934
+ "variation"
11935
+ ],
11936
+ type: "object"
11937
+ }
11938
+ ]
11939
+ },
11940
+ minItems: 1,
11941
+ type: "array"
11942
+ },
11943
+ kind: {
11944
+ const: "shipping",
11945
+ type: "string"
11946
+ },
11947
+ lever: {
11948
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11949
+ additionalProperties: false,
11950
+ properties: {
11951
+ catalogSource: {
11952
+ anyOf: [
11953
+ {
11954
+ const: "sample",
11955
+ type: "string"
11956
+ },
11957
+ {
11958
+ const: "live",
11959
+ type: "string"
11960
+ }
11961
+ ]
11962
+ },
11963
+ currencyCode: {
11964
+ maxLength: 3,
11965
+ minLength: 3,
11966
+ pattern: "^[A-Z]{3}$",
11967
+ type: "string"
11968
+ },
11969
+ enforcement: {
11970
+ default: "none",
11971
+ enum: ["none", "free_shipping"],
11972
+ type: "string"
11973
+ },
11974
+ enforcementPolicy: {
11975
+ additionalProperties: false,
11976
+ properties: {
11977
+ collisionPolicy: {
11978
+ const: "block_on_existing_shipping_discount",
11979
+ type: "string"
11980
+ },
11981
+ expressServiceHandles: {
11982
+ default: [],
11983
+ items: {
11984
+ maxLength: 255,
11985
+ minLength: 1,
11986
+ pattern: "^[a-z0-9][a-z0-9-]*$",
11987
+ type: "string"
11988
+ },
11989
+ maxItems: 50,
11990
+ type: "array"
11991
+ },
11992
+ maximumSubsidy: {
11993
+ exclusiveMinimum: 0,
11994
+ type: "number"
11995
+ },
11996
+ standardServiceHandles: {
11997
+ items: {
11998
+ maxLength: 255,
11999
+ minLength: 1,
12000
+ pattern: "^[a-z0-9][a-z0-9-]*$",
12001
+ type: "string"
12002
+ },
12003
+ maxItems: 50,
12004
+ minItems: 1,
12005
+ type: "array"
12006
+ }
12007
+ },
12008
+ required: ["standardServiceHandles", "maximumSubsidy", "collisionPolicy"],
12009
+ type: "object"
12010
+ },
12011
+ liveFreeShippingThreshold: {
12012
+ anyOf: [
12013
+ {
12014
+ minimum: 0,
12015
+ type: "number"
12016
+ },
12017
+ {
12018
+ type: "null"
12019
+ }
12020
+ ]
12021
+ }
12022
+ },
12023
+ required: ["catalogSource", "currencyCode", "liveFreeShippingThreshold"],
12024
+ type: "object"
12025
+ },
12026
+ schemaVersion: {
12027
+ const: 1,
12028
+ type: "number"
12029
+ },
12030
+ serverOwned: {
12031
+ additionalProperties: false,
12032
+ properties: {
12033
+ carriers: {
12034
+ additionalProperties: {},
12035
+ propertyNames: {
12036
+ type: "string"
12037
+ },
12038
+ type: "object"
12039
+ },
12040
+ readonly: {
12041
+ const: true,
12042
+ type: "boolean"
12043
+ }
12044
+ },
12045
+ readOnly: true,
12046
+ required: ["readonly", "carriers"],
12047
+ type: "object"
12048
+ }
12049
+ },
12050
+ required: ["schemaVersion", "kind", "lever", "arms"],
12051
+ type: "object"
12052
+ },
12053
+ {
12054
+ additionalProperties: false,
12055
+ properties: {
12056
+ carriers: {
12057
+ additionalProperties: {
12058
+ items: {},
12059
+ type: "array"
12060
+ },
12061
+ propertyNames: {
12062
+ type: "string"
12063
+ },
12064
+ type: "object"
12065
+ },
12066
+ kind: {
12067
+ const: "shipping",
12068
+ type: "string"
12069
+ },
12070
+ readonly: {
12071
+ const: true,
12072
+ type: "boolean"
12073
+ },
12074
+ reason: {
12075
+ type: "string"
12076
+ },
12077
+ schemaVersion: {
12078
+ const: 1,
12079
+ type: "number"
12080
+ }
12081
+ },
12082
+ required: ["schemaVersion", "kind", "readonly", "reason", "carriers"],
12083
+ type: "object"
12084
+ }
12085
+ ]
12086
+ },
10246
12087
  "experiment.json": {
10247
12088
  $schema: "https://json-schema.org/draft/2020-12/schema",
10248
12089
  additionalProperties: false,
@@ -10824,13 +12665,7 @@ var experiment_filesystem_schema_default = {
10824
12665
  {
10825
12666
  properties: {
10826
12667
  action: {
10827
- enum: [
10828
- "priceTest",
10829
- "shippingTest",
10830
- "offerTest",
10831
- "postPurchaseOffer",
10832
- "checkoutBlock"
10833
- ]
12668
+ enum: ["checkoutBlock"]
10834
12669
  }
10835
12670
  },
10836
12671
  required: ["action"]
@@ -11063,6 +12898,16 @@ function printExperimentResults(results2, currency) {
11063
12898
  const outcomeProvenance = summary.outcome.provenance === "manager_decided" ? " (manager decided)" : "";
11064
12899
  console.log();
11065
12900
  console.log(source_default.bold(String(experiment2.name ?? experiment2.id ?? "Experiment results")));
12901
+ if (typeof results2.computed_at === "string") {
12902
+ const ageSeconds = Math.max(
12903
+ 0,
12904
+ Math.round((Date.now() - Date.parse(results2.computed_at)) / 1e3)
12905
+ );
12906
+ const age = ageSeconds < 60 ? `${ageSeconds}s ago` : ageSeconds < 3600 ? `${Math.floor(ageSeconds / 60)}m ago` : `${Math.floor(ageSeconds / 3600)}h ago`;
12907
+ console.log(
12908
+ ` Computed: ${results2.computed_at} (${age}, from ${String(results2.results_source ?? "unknown")})`
12909
+ );
12910
+ }
11066
12911
  console.log(` Status: ${statusBadge(String(experiment2.status ?? "unknown"))}`);
11067
12912
  const audience = audienceLine(results2.segment_applied);
11068
12913
  if (audience) console.log(` Audience: ${audience}`);
@@ -11111,6 +12956,49 @@ function printExperimentResults(results2, currency) {
11111
12956
  [32, 8, 6, 8, 10]
11112
12957
  );
11113
12958
  }
12959
+ const commerceResults = summary.commerceResults;
12960
+ if (commerceResults) {
12961
+ console.log();
12962
+ console.log(source_default.bold("Profit per visitor (CM1)"));
12963
+ if (commerceResults.degraded || !commerceResults.plane) {
12964
+ console.log(` Unavailable: ${String(commerceResults.reason ?? "unknown")}`);
12965
+ } else {
12966
+ const plane = commerceResults.plane;
12967
+ const commerceArms = Array.isArray(plane.arms) ? plane.arms : [];
12968
+ printTable(
12969
+ ["ARM", "VISITORS", "ORDERS", "CM1/VISITOR", "DELTA (95% CI)", "STATE"],
12970
+ commerceArms.map((arm) => {
12971
+ const deltas = arm.deltaVsControl;
12972
+ const cm = deltas?.cm;
12973
+ const ci = cm?.ci95;
12974
+ const count2 = (value) => typeof value === "number" && Number.isFinite(value) ? String(value) : "-";
12975
+ const money = (value) => typeof value === "number" ? formatRevenue(value, String(plane.currency ?? currency ?? "")) : "-";
12976
+ return [
12977
+ String(arm.name ?? arm.variationId ?? "-"),
12978
+ // Fail closed: a missing count is unavailable, never a measured 0.
12979
+ count2(arm.visitors),
12980
+ count2(arm.orders),
12981
+ money(arm.cmPerVisitor),
12982
+ cm ? `${money(cm.abs)}${ci ? ` (${money(ci.lower)} to ${money(ci.upper)})` : ""}` : "-",
12983
+ String(cm?.state ?? "control")
12984
+ ];
12985
+ })
12986
+ );
12987
+ const oec = plane.oec;
12988
+ const coverage = plane.coverage;
12989
+ console.log(` Verdict: ${String(oec?.verdict ?? "unknown")}`);
12990
+ console.log(` ${String(oec?.plainLanguage ?? "")}`);
12991
+ console.log(
12992
+ ` Cost coverage: ${typeof coverage?.costCoverage === "number" ? `${(coverage.costCoverage * 100).toFixed(1)}%` : "-"}`
12993
+ );
12994
+ const health = plane.health;
12995
+ for (const [name, check] of Object.entries(health ?? {})) {
12996
+ if (name === "trustworthy" || typeof check !== "object" || check === null || check.ok !== false)
12997
+ continue;
12998
+ console.log(` Health: ${name} failed`);
12999
+ }
13000
+ }
13001
+ }
11114
13002
  const metrics = summary.metrics;
11115
13003
  if (metrics.length > 0) {
11116
13004
  console.log();
@@ -11133,16 +13021,17 @@ var filesystemPaths = Object.keys(experiment_filesystem_schema_default.files);
11133
13021
  var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
11134
13022
  topLevel: filesystemPaths.filter((entry) => !entry.includes("/")),
11135
13023
  experiment: filesystemPaths.filter((entry) => entry.startsWith("experiment/")).map((entry) => entry.slice("experiment/".length)),
13024
+ commerce: filesystemPaths.filter((entry) => entry.startsWith("commerce/")).map((entry) => entry.slice("commerce/".length)),
11136
13025
  variation: filesystemPaths.filter((entry) => entry.startsWith("variations/*/")).map((entry) => entry.slice("variations/*/".length))
11137
13026
  };
11138
13027
  var SERVER_OWNED_EXPERIMENT_FILES = /* @__PURE__ */ new Set(["qa.json", "launch.json"]);
11139
13028
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11140
13029
  var pathAlternation = (paths) => paths.map(escapeRegex).join("|");
11141
13030
  var FILESYSTEM_FILE_RE = new RegExp(
11142
- `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
13031
+ `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|commerce/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.commerce)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
11143
13032
  );
11144
13033
  function sha256Text(value) {
11145
- return import_node_crypto5.default.createHash("sha256").update(value).digest("hex");
13034
+ return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
11146
13035
  }
11147
13036
  function folderPath(folder) {
11148
13037
  return import_node_path6.default.resolve(process.cwd(), folder);
@@ -11239,6 +13128,13 @@ function listExperimentFilesystemFiles(folder, session) {
11239
13128
  const content = import_node_fs8.default.readFileSync(absPath, "utf8");
11240
13129
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
11241
13130
  }
13131
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.commerce) {
13132
+ const relativePath = `commerce/${fileName}`;
13133
+ const absPath = import_node_path6.default.join(folder, relativePath);
13134
+ if (!import_node_fs8.default.existsSync(absPath)) continue;
13135
+ const content = import_node_fs8.default.readFileSync(absPath, "utf8");
13136
+ files.push({ path: relativePath, content, sha256: sha256Text(content) });
13137
+ }
11242
13138
  const variationsRoot = import_node_path6.default.join(folder, "variations");
11243
13139
  if (import_node_fs8.default.existsSync(variationsRoot)) {
11244
13140
  for (const variationDir of import_node_fs8.default.readdirSync(variationsRoot, { withFileTypes: true })) {
@@ -11261,7 +13157,7 @@ async function rewriteExperimentFilesystemAssets(input) {
11261
13157
  baseUrl: input.baseUrl,
11262
13158
  enabled: input.enabled,
11263
13159
  upload: async (file) => {
11264
- const hash = import_node_crypto5.default.createHash("sha256").update(file.bytes).digest("hex");
13160
+ const hash = import_node_crypto6.default.createHash("sha256").update(file.bytes).digest("hex");
11265
13161
  const cached = input.assets[hash];
11266
13162
  if (cached) return cached;
11267
13163
  if (input.upload === null) return null;
@@ -11591,7 +13487,7 @@ var list = new Command("list").alias("ls").description(
11591
13487
  "Filter by effective storefront URL targeting (applied server-side)"
11592
13488
  ).option("-n, --limit <n>", "Max results", "20").option("--include-archived", "Include archived experiments").option("--full", "Return full experiment details instead of the default summary").option(
11593
13489
  "--fields <list>",
11594
- "Comma-separated fields to return and table columns in order (JSON always keeps id)"
13490
+ "Comma-separated fields to return and table columns in order (JSON always keeps id; fields outside the summary switch to full details automatically)"
11595
13491
  ).action(async function action3(opts) {
11596
13492
  const creds = requireCredentials();
11597
13493
  const spinner = await createSpinner("Fetching experiments...");
@@ -11747,7 +13643,7 @@ ${JSON.stringify(data.order, null, 2)}`);
11747
13643
  console.log(`
11748
13644
  Definitions: ${data.definitions_url}`);
11749
13645
  }
11750
- var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").option("--segment <dim:value>", "Include one audience segment").option("--exclude-segment <dim:value>", "Exclude one audience segment (include wins)").option("--explain", "Explain raw-event exclusions and residuals").option("--goal <id>", "Goal ID or event key for --explain").option("--order <key>", "Order label trail for --explain").option("--samples", "Show sample identifiers with --explain").action(async function action7(idOrSlug, opts) {
13646
+ var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").option("--segment <dim:value>", "Include one audience segment").option("--exclude-segment <dim:value>", "Exclude one audience segment (include wins)").option("--explain", "Explain raw-event exclusions and residuals").option("--goal <id>", "Goal ID or event key for --explain").option("--order <key>", "Order label trail for --explain").option("--samples", "Show sample identifiers with --explain").option("--wait <seconds>", "Wait for a cold server-side results build", "120").action(async function action7(idOrSlug, opts) {
11751
13647
  if (opts.explain && (opts.segment !== void 0 || opts.excludeSegment !== void 0))
11752
13648
  fail(
11753
13649
  this,
@@ -11764,16 +13660,38 @@ var results = new Command("results").description("Read experiment results and ti
11764
13660
  spinner?.fail(`Experiment "${idOrSlug}" not found`);
11765
13661
  fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
11766
13662
  }
11767
- const res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, {
13663
+ const waitSeconds = Number(opts.wait ?? "120");
13664
+ if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
13665
+ fail(this, "--wait must be a non-negative number of seconds", EXIT_CODES.validation);
13666
+ }
13667
+ const startedWaitingAt = Date.now();
13668
+ let res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, {
11768
13669
  currency: opts.currency,
11769
13670
  segment: opts.segment,
11770
13671
  exclude_segment: opts.excludeSegment
11771
13672
  });
13673
+ while (!opts.explain && isResultsComputing(res)) {
13674
+ const elapsedSeconds = (Date.now() - startedWaitingAt) / 1e3;
13675
+ const remainingSeconds = waitSeconds - elapsedSeconds;
13676
+ if (remainingSeconds <= 0) break;
13677
+ const requestedDelay = Number(
13678
+ res.retryAfter ?? res.errorDetails?.retry_after_seconds ?? 15
13679
+ );
13680
+ const delaySeconds = Math.min(30, Math.max(0, requestedDelay), remainingSeconds);
13681
+ if (!shouldUseJson(this))
13682
+ console.error(`Results are computing on the server; retrying in ${delaySeconds} s`);
13683
+ await new Promise((resolve2) => setTimeout(resolve2, delaySeconds * 1e3));
13684
+ res = await getExperimentResults(creds, exp.id, {
13685
+ currency: opts.currency,
13686
+ segment: opts.segment,
13687
+ exclude_segment: opts.excludeSegment
13688
+ });
13689
+ }
11772
13690
  if (!res.ok || !res.data) {
11773
13691
  spinner?.fail("Failed to fetch experiment results");
11774
13692
  fail(
11775
13693
  this,
11776
- res.error ?? "Failed to fetch experiment results",
13694
+ `${res.error ?? "Failed to fetch experiment results"}${isResultsComputing(res) ? "; increase --wait to keep retrying" : ""}`,
11777
13695
  classifyStatus(res.status),
11778
13696
  res.errorDetails
11779
13697
  );
@@ -11790,6 +13708,11 @@ var results = new Command("results").description("Read experiment results and ti
11790
13708
  fail(this, error instanceof Error ? error.message : String(error));
11791
13709
  }
11792
13710
  });
13711
+ function isResultsComputing(res) {
13712
+ if (res.status !== 503) return false;
13713
+ const body5 = res.data;
13714
+ return body5?.error?.code === "results_computing" || res.errorDetails?.code === "results_computing";
13715
+ }
11793
13716
  var DAY_NAMES = {
11794
13717
  mon: "monday",
11795
13718
  monday: "monday",
@@ -12864,7 +14787,45 @@ var applyFilesystem = new Command("apply").option(
12864
14787
  acceptQaInvalidation = true;
12865
14788
  }
12866
14789
  const spinner = process.stderr.isTTY ? await createSpinner("Applying experiment filesystem...") : null;
12867
- let res = await applyExperimentFilesystem(creds, session.experimentId, {
14790
+ let verifiedAfterTimeout = false;
14791
+ let verifiedTimeoutMs;
14792
+ let publicationUnconfirmed = false;
14793
+ const applyWithVerification = async (body5) => {
14794
+ try {
14795
+ return await applyExperimentFilesystem(creds, session.experimentId, body5);
14796
+ } catch (error) {
14797
+ if (!(error instanceof CliError) || error.details?.reason !== "timeout" && error.details?.reason !== "reset")
14798
+ throw error;
14799
+ const reason2 = error.details.reason;
14800
+ const timeoutMs = typeof error.details.timeoutMs === "number" ? error.details.timeoutMs : APPLY_TIMEOUT_MS;
14801
+ const verification = await planExperimentFilesystem(creds, session.experimentId, {
14802
+ files
14803
+ }).catch(() => null);
14804
+ if (verification?.ok && verification.data?.ok === true && verification.data.changes.length === 0) {
14805
+ verifiedAfterTimeout = true;
14806
+ verifiedTimeoutMs = timeoutMs;
14807
+ publicationUnconfirmed = verification.data.qaApproval?.status !== "draft";
14808
+ return {
14809
+ ok: true,
14810
+ status: 200,
14811
+ data: { applied: true, plan: verification.data }
14812
+ };
14813
+ }
14814
+ const rerun = `apex experiments apply ${shellQuote(folderArg)}`;
14815
+ throw new CliError(
14816
+ `Apply did not confirm within ${timeoutMs / 1e3}s and the server does not yet hold these changes; re-run ${rerun}`,
14817
+ EXIT_CODES.general,
14818
+ {
14819
+ code: "apply_unconfirmed",
14820
+ reason: reason2,
14821
+ timeoutMs,
14822
+ pendingChanges: verification?.ok && verification.data ? verification.data.changes.length : null,
14823
+ next_steps: [{ command: rerun, why: "Re-run the unconfirmed apply." }]
14824
+ }
14825
+ );
14826
+ }
14827
+ };
14828
+ let res = await applyWithVerification({
12868
14829
  files,
12869
14830
  accept_qa_invalidation: acceptQaInvalidation,
12870
14831
  ...opts.confirmTargetingReplace ? { confirmTargetingReplace: true } : {}
@@ -12885,7 +14846,7 @@ var applyFilesystem = new Command("apply").option(
12885
14846
  output(this, { aborted: true }, () => console.log("Aborted."));
12886
14847
  return;
12887
14848
  }
12888
- res = await applyExperimentFilesystem(creds, session.experimentId, {
14849
+ res = await applyWithVerification({
12889
14850
  files,
12890
14851
  accept_qa_invalidation: acceptQaInvalidation,
12891
14852
  confirmTargetingReplace: true
@@ -12907,7 +14868,8 @@ var applyFilesystem = new Command("apply").option(
12907
14868
  plan: res.data?.plan
12908
14869
  });
12909
14870
  }
12910
- spinner?.succeed("Applied experiment filesystem");
14871
+ if (publicationUnconfirmed) spinner?.stop();
14872
+ else spinner?.succeed("Applied experiment filesystem");
12911
14873
  const synced = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
12912
14874
  if (!synced) {
12913
14875
  await refreshServerOwnedFilesIfNeeded(
@@ -12931,6 +14893,11 @@ var applyFilesystem = new Command("apply").option(
12931
14893
  const syncMessage = synced ? "Local files were synced." : opts.sync ? "Local file sync failed; retry --sync or apex experiments pull." : "Local authored files were left unchanged; use --sync or apex experiments pull to refresh." + (res.data?.receipt?.files ? "" : " No receipt file digests; authored-file manifest entries left unchanged.");
12932
14894
  const previewResult = await getQaPreviewUrls(creds, session.experimentId);
12933
14895
  const { previewUrls, dashboardUrl } = previewResult;
14896
+ const publicationWarning = "Storefront publication is unconfirmed; visitors may still receive the previous treatment.";
14897
+ const publicationNextStep = {
14898
+ command: `apex experiments status ${shellQuote(folderArg)} --json`,
14899
+ why: "Check experiment status before handover; persisted files do not confirm storefront publication."
14900
+ };
12934
14901
  output(
12935
14902
  this,
12936
14903
  {
@@ -12939,14 +14906,30 @@ var applyFilesystem = new Command("apply").option(
12939
14906
  applied: true,
12940
14907
  plan: planRes.data,
12941
14908
  requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
12942
- result: res.data,
14909
+ result: verifiedAfterTimeout ? null : res.data,
12943
14910
  synced,
12944
14911
  dashboardUrl,
12945
- receipt: res.data?.receipt ?? null,
12946
- previewUrls
14912
+ receipt: verifiedAfterTimeout ? null : res.data?.receipt ?? null,
14913
+ previewUrls,
14914
+ ...verifiedAfterTimeout ? { verifiedAfterTimeout: true, timeoutMs: verifiedTimeoutMs } : {},
14915
+ ...publicationUnconfirmed ? {
14916
+ persisted: true,
14917
+ publication: "unconfirmed",
14918
+ warnings: [publicationWarning],
14919
+ next_steps: [publicationNextStep]
14920
+ } : {}
12947
14921
  },
12948
14922
  () => {
12949
14923
  printFilesystemPlan(planRes.data);
14924
+ if (verifiedAfterTimeout) {
14925
+ console.log(
14926
+ `${publicationUnconfirmed ? "Apply saved" : "Apply confirmed"}: the server already holds these changes (the response timed out after ${verifiedTimeoutMs / 1e3}s).`
14927
+ );
14928
+ }
14929
+ if (publicationUnconfirmed) {
14930
+ console.log(`Warning: ${publicationWarning}`);
14931
+ console.log(`Next: ${publicationNextStep.command} (${publicationNextStep.why})`);
14932
+ }
12950
14933
  const receipt = res.data?.receipt;
12951
14934
  if (receipt) {
12952
14935
  console.log("\nReceipt");
@@ -13107,6 +15090,9 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
13107
15090
  "[folder-or-id]",
13108
15091
  "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
13109
15092
  "."
15093
+ ).option(
15094
+ "--acknowledge-zero-traffic",
15095
+ "Acknowledge variations that receive no traffic for this split (Apex staff only)"
13110
15096
  ).action(async function action19(target) {
13111
15097
  const creds = requireCredentials();
13112
15098
  const spinner = await createSpinner("Approving QA...");
@@ -13114,7 +15100,40 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
13114
15100
  const resolved = await resolveExperimentForFilesystemOrId(target);
13115
15101
  const before = await resolveExperiment(resolved.experimentId);
13116
15102
  if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
13117
- const res = await approveExperimentQa(creds, resolved.experimentId);
15103
+ let res = await approveExperimentQa(creds, resolved.experimentId);
15104
+ const details = res.errorDetails;
15105
+ if (!res.ok && res.status === 409 && details?.code === "degenerate_weights_ack_required") {
15106
+ if (!this.opts().acknowledgeZeroTraffic) {
15107
+ throw new CliError(
15108
+ `${res.error ?? "QA approval requires acknowledgement"} Rerun with --acknowledge-zero-traffic.`,
15109
+ classifyStatus(res.status),
15110
+ details
15111
+ );
15112
+ }
15113
+ if (details.internal_only === true) {
15114
+ throw new CliError(
15115
+ res.error ?? "Only Apex Admin or Internal users can acknowledge zero traffic.",
15116
+ classifyStatus(res.status),
15117
+ details
15118
+ );
15119
+ }
15120
+ if (typeof details.split_hash === "string" && details.split_hash.length > 0) {
15121
+ const weights = Array.isArray(details.variation_weights) ? details.variation_weights : [];
15122
+ for (const value of weights) {
15123
+ if (value && typeof value === "object") {
15124
+ const variation = value;
15125
+ process.stderr.write(
15126
+ `${variation.name ?? variation.id ?? "Variation"}: ${(variation.weight ?? 0) * 100}%${variation.weight === 0 ? " (no traffic)" : ""}
15127
+ `
15128
+ );
15129
+ }
15130
+ }
15131
+ res = await approveExperimentQa(creds, resolved.experimentId, {
15132
+ acknowledge_degenerate_weights: true,
15133
+ acknowledge_degenerate_weights_hash: details.split_hash
15134
+ });
15135
+ }
15136
+ }
13118
15137
  if (!res.ok)
13119
15138
  throw new CliError(
13120
15139
  res.error ?? "Failed to approve QA",
@@ -13291,6 +15310,46 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
13291
15310
  }
13292
15311
  });
13293
15312
  var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(
15313
+ new Command("probe").description("Run or read saved commerce QA probes").argument("<folder-or-id>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--kind <kind...>", "Probe kinds: price, shipping, post_purchase").option("--saved", "Read saved evidence without calling Shopify").action(async function(target, opts) {
15314
+ const kinds = opts.kind;
15315
+ if (kinds && (kinds.length > 3 || new Set(kinds).size !== kinds.length || kinds.some((kind5) => !["price", "shipping", "post_purchase"].includes(kind5))))
15316
+ fail(this, "--kind must be price, shipping, or post_purchase", EXIT_CODES.validation);
15317
+ const creds = requireCredentials();
15318
+ const { experimentId } = await resolveExperimentForFilesystemOrId(target);
15319
+ const response = opts.saved ? await getExperimentCommerceProbes(creds, experimentId) : await runExperimentCommerceProbes(creds, experimentId, kinds);
15320
+ if (!response.ok || !response.data)
15321
+ throw new CliError(
15322
+ response.error ?? "Failed to load commerce probes",
15323
+ classifyStatus(response.status),
15324
+ response.errorDetails
15325
+ );
15326
+ const data = opts.saved && kinds ? {
15327
+ ...response.data,
15328
+ kinds: Object.fromEntries(
15329
+ Object.entries(response.data.kinds).filter(([kind5]) => kinds.includes(kind5))
15330
+ )
15331
+ } : response.data;
15332
+ output(
15333
+ this,
15334
+ data,
15335
+ () => printTable(
15336
+ ["KIND", "STATE", "SUMMARY"],
15337
+ Object.entries(data.kinds).map(([kind5, value]) => [
15338
+ kind5,
15339
+ value.evidence.state,
15340
+ value.result?.summary ?? value.evidence.reasons[0] ?? "Saved evidence is current."
15341
+ ])
15342
+ )
15343
+ );
15344
+ const states = Object.values(data.kinds).map((value) => value?.evidence.state);
15345
+ if (states.some((state) => state !== "pass")) {
15346
+ console.error(
15347
+ "Next: fix stale, pending, or failing evidence and run apex experiments qa probe again."
15348
+ );
15349
+ process.exitCode = states.includes("fail") ? EXIT_CODES.checkFailed : EXIT_CODES.validation;
15350
+ }
15351
+ })
15352
+ ).addCommand(
13294
15353
  new Command("events").description("List QA-tagged events for an experiment session").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--session <id>", "QA session id (defaults to the latest QA run)").option("--minutes <n>", "Lookback window in minutes", "60").option("--type <type>", "Event type: all, goal, or custom", "all").action(async function(target, opts) {
13295
15354
  const minutes = Number(opts.minutes);
13296
15355
  if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440)
@@ -13419,7 +15478,7 @@ var screenshots = new Command("screenshots").description(
13419
15478
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
13420
15479
  )
13421
15480
  );
13422
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
15481
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
13423
15482
  continue;
13424
15483
  }
13425
15484
  if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
@@ -13429,7 +15488,7 @@ var screenshots = new Command("screenshots").description(
13429
15488
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
13430
15489
  )
13431
15490
  );
13432
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
15491
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
13433
15492
  continue;
13434
15493
  }
13435
15494
  break;
@@ -13583,7 +15642,7 @@ var screenshots = new Command("screenshots").description(
13583
15642
  );
13584
15643
  consecutiveTransientFailures += 1;
13585
15644
  if (consecutiveTransientFailures > 3) throw error;
13586
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
15645
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13587
15646
  continue;
13588
15647
  }
13589
15648
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
@@ -13608,11 +15667,11 @@ var screenshots = new Command("screenshots").description(
13608
15667
  polled.errorDetails
13609
15668
  );
13610
15669
  }
13611
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
15670
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13612
15671
  continue;
13613
15672
  }
13614
15673
  consecutiveTransientFailures = 0;
13615
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
15674
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13616
15675
  }
13617
15676
  if (opts.attachQa !== false) {
13618
15677
  const attached = await attachExperimentQaEvidence(creds, experiment2.id, {
@@ -13990,6 +16049,8 @@ init_source();
13990
16049
  async function signedPreviewFromReadiness(creds, experimentId, variationId, targetUrl) {
13991
16050
  try {
13992
16051
  const client = createHttpClient(creds, {
16052
+ onResponse: recordApiResponse2,
16053
+ observeNetworkErrors: true,
13993
16054
  onNetworkError: (info) => {
13994
16055
  throw new Error(info.cause);
13995
16056
  }
@@ -14233,8 +16294,7 @@ function loadHarnessConfig(folder) {
14233
16294
  experimentName: experiment2.name ?? id,
14234
16295
  variationSlug: slugify(variation, "variant-a"),
14235
16296
  variationName: `Variant ${variation}`,
14236
- targetUrl,
14237
- antiFlickerSelectors: []
16297
+ targetUrl
14238
16298
  }
14239
16299
  };
14240
16300
  }
@@ -14318,14 +16378,15 @@ async function createSession(folder) {
14318
16378
  if (!apex.targetUrl) {
14319
16379
  throw new Error("Missing targetUrl. Run `apex harness init` and set targetUrl.");
14320
16380
  }
16381
+ const resetJs = cleanupResetJs(experiment2);
14321
16382
  const res = await createHarnessSession(creds, {
14322
16383
  experimentSlug: apex.experimentSlug,
14323
16384
  experimentName: apex.experimentName,
14324
16385
  variationSlug: apex.variationSlug,
14325
16386
  variationName: apex.variationName,
14326
16387
  targetUrl: apex.targetUrl,
14327
- resetJs: cleanupResetJs(experiment2),
14328
- antiFlickerSelectors: apex.antiFlickerSelectors ?? []
16388
+ ...typeof resetJs === "string" ? { resetJs } : {},
16389
+ ...Array.isArray(apex.antiFlickerSelectors) ? { antiFlickerSelectors: apex.antiFlickerSelectors } : {}
14329
16390
  });
14330
16391
  if (!res.ok)
14331
16392
  throw new CliError(
@@ -14453,7 +16514,7 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
14453
16514
 
14454
16515
  // src/commands/operator.ts
14455
16516
  init_source();
14456
- var import_node_crypto6 = __toESM(require("node:crypto"));
16517
+ var import_node_crypto7 = __toESM(require("node:crypto"));
14457
16518
  var import_node_fs11 = __toESM(require("node:fs"));
14458
16519
  var import_node_path9 = __toESM(require("node:path"));
14459
16520
  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)$/;
@@ -14498,8 +16559,13 @@ function formatWorkspaceBlockingIssues(value) {
14498
16559
  return lines.join("\n");
14499
16560
  }).join("\n");
14500
16561
  }
16562
+ function formatHarnessRefusal(message, details) {
16563
+ const issues = formatWorkspaceBlockingIssues(details);
16564
+ return issues ? `${message}
16565
+ ${issues}` : message;
16566
+ }
14501
16567
  function sha256Text2(value) {
14502
- return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
16568
+ return import_node_crypto7.default.createHash("sha256").update(value).digest("hex");
14503
16569
  }
14504
16570
  function byteLength(value) {
14505
16571
  return Buffer.byteLength(value, "utf8");
@@ -14692,12 +16758,11 @@ async function createExactHarnessSession(folder, session) {
14692
16758
  experimentName: expRes.data.name,
14693
16759
  variationSlug: config.variationSlug ?? variation.slug ?? slugify2(variation.name, "variation"),
14694
16760
  variationName: variation.name,
14695
- targetUrl,
14696
- antiFlickerSelectors: []
16761
+ targetUrl
14697
16762
  });
14698
16763
  if (!res.ok || !res.data)
14699
16764
  throw new CliError(
14700
- res.error ?? "Failed to create harness session",
16765
+ formatHarnessRefusal(res.error ?? "Failed to create harness session", res.errorDetails),
14701
16766
  classifyStatus(res.status),
14702
16767
  res.errorDetails
14703
16768
  );
@@ -14789,7 +16854,15 @@ async function runPublish(folderArg, opts = {}) {
14789
16854
  const harnessSession = await createExactHarnessSession(folder, session);
14790
16855
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
14791
16856
  const published = await publishHarnessSession(creds, harnessSession.sessionId);
14792
- if (!published.ok) throw new Error(published.error ?? "Failed to publish external artifacts");
16857
+ if (!published.ok)
16858
+ throw new CliError(
16859
+ formatHarnessRefusal(
16860
+ published.error ?? "Failed to publish external artifacts",
16861
+ published.errorDetails
16862
+ ),
16863
+ classifyStatus(published.status),
16864
+ published.errorDetails
16865
+ );
14793
16866
  external = {
14794
16867
  sessionId: harnessSession.sessionId,
14795
16868
  version: version.version,
@@ -15436,10 +17509,81 @@ trackingCommand.command("latency").description("Measure forced-preview versus na
15436
17509
 
15437
17510
  // src/commands/events.ts
15438
17511
  var import_node_fs14 = __toESM(require("node:fs"));
17512
+ var maxWindowMs = 31 * 864e5;
17513
+ function correctedWindowStep(opts, nowMs) {
17514
+ const fromMs = Date.parse(opts.from);
17515
+ const endMs = Math.min(fromMs + maxWindowMs, nowMs);
17516
+ if (endMs <= fromMs) {
17517
+ return {
17518
+ command: "Choose a --from start before now.",
17519
+ why: "No valid export window exists from this start; --to must be after --from and at or before now."
17520
+ };
17521
+ }
17522
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
17523
+ const args = [
17524
+ "apex events export",
17525
+ `--from ${quote(opts.from)}`,
17526
+ `--to ${quote(new Date(endMs).toISOString())}`,
17527
+ `--out ${quote(opts.out ?? "events.ndjson")}`
17528
+ ];
17529
+ if (opts.experiment !== void 0) args.push(`--experiment ${quote(opts.experiment)}`);
17530
+ if (opts.eventType !== void 0) args.push(`--event-type ${quote(opts.eventType)}`);
17531
+ args.push(`--qa ${quote(opts.qa)}`, `--limit ${quote(opts.limit)}`);
17532
+ if (opts.jsonSummary) args.push("--json-summary");
17533
+ return {
17534
+ command: args.join(" "),
17535
+ why: endMs === nowMs ? "Exports events through the current time with the same filters." : "Exports the first 31 days with the same filters; repeat with the next window."
17536
+ };
17537
+ }
15439
17538
  var eventsCommand = new Command("events").description("Export raw tracking events");
15440
- eventsCommand.command("export").description("Export every raw event in an absolute time window").requiredOption("--from <iso>", "Inclusive ISO 8601 window start").requiredOption("--to <iso>", "Exclusive ISO 8601 window end").option("--experiment <id>", "Filter by experiment ID").option("--event-type <types>", "Comma-separated event types").option("--qa <scope>", "QA scope: exclude, include, or only", "include").option("--limit <count>", "Page size from 1 to 1000", "500").option("--out <path>", "Write event rows to a file instead of stdout").option("--json-summary", "Print a JSON summary on stdout; requires --out for the NDJSON rows").action(async (opts, command) => {
17539
+ eventsCommand.command("export").description(
17540
+ "Export every raw event in a window of at most 31 days that does not end in the future"
17541
+ ).requiredOption(
17542
+ "--from <iso>",
17543
+ "Inclusive ISO 8601 window start; the window spans at most 31 days"
17544
+ ).requiredOption("--to <iso>", "Exclusive ISO 8601 window end; not in the future").option("--experiment <id>", "Filter by experiment ID").option("--event-type <types>", "Comma-separated event types").option("--qa <scope>", "QA scope: exclude, include, or only", "include").option("--limit <count>", "Page size from 1 to 1000", "500").option("--out <path>", "Write event rows to a file instead of stdout").option("--json-summary", "Print a JSON summary on stdout; requires --out for the NDJSON rows").action(async (opts, command) => {
15441
17545
  if (!Number.isFinite(Date.parse(opts.from)) || !Number.isFinite(Date.parse(opts.to))) {
15442
- fail(command, "--from and --to must be ISO 8601 timestamps", EXIT_CODES.validation);
17546
+ fail(command, "--from and --to must be ISO 8601 timestamps", EXIT_CODES.validation, {
17547
+ code: "invalid_window",
17548
+ reason: "invalid_timestamp"
17549
+ });
17550
+ }
17551
+ const fromMs = Date.parse(opts.from);
17552
+ const toMs = Date.parse(opts.to);
17553
+ if (toMs <= fromMs)
17554
+ fail(command, "--to must be after --from.", EXIT_CODES.validation, {
17555
+ code: "invalid_window",
17556
+ reason: "to_not_after_from"
17557
+ });
17558
+ const now = /* @__PURE__ */ new Date();
17559
+ if (toMs - fromMs > maxWindowMs) {
17560
+ const requestedDays = ((toMs - fromMs) / 864e5).toFixed(2);
17561
+ fail(
17562
+ command,
17563
+ `The export window is ${requestedDays} days; the maximum is 31. Export in windows of at most 31 days.`,
17564
+ EXIT_CODES.validation,
17565
+ {
17566
+ code: "invalid_window",
17567
+ reason: "window_too_long",
17568
+ max_days: 31,
17569
+ requested_days: requestedDays,
17570
+ next_steps: [correctedWindowStep(opts, now.getTime())]
17571
+ }
17572
+ );
17573
+ }
17574
+ if (toMs > now.getTime()) {
17575
+ const nowIso = now.toISOString();
17576
+ fail(
17577
+ command,
17578
+ `--to is in the future; use a time at or before now (${nowIso}).`,
17579
+ EXIT_CODES.validation,
17580
+ {
17581
+ code: "invalid_window",
17582
+ reason: "to_in_future",
17583
+ now: nowIso,
17584
+ next_steps: [correctedWindowStep(opts, now.getTime())]
17585
+ }
17586
+ );
15443
17587
  }
15444
17588
  if (!["exclude", "include", "only"].includes(opts.qa)) {
15445
17589
  fail(command, "--qa must be exclude, include, or only", EXIT_CODES.validation);
@@ -15479,129 +17623,150 @@ eventsCommand.command("export").description("Export every raw event in an absolu
15479
17623
  output2.once("error", reject);
15480
17624
  });
15481
17625
  } catch (error) {
17626
+ const exists = error?.code === "EEXIST";
15482
17627
  fail(
15483
17628
  command,
15484
- `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
15485
- EXIT_CODES.general,
15486
- { code: "local_file_error" }
17629
+ exists ? `Cannot write ${opts.out}: the file already exists. Choose a new --out path or delete the file first.` : `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
17630
+ exists ? EXIT_CODES.validation : EXIT_CODES.general,
17631
+ { code: exists ? "output_exists" : "local_file_error", path: opts.out }
15487
17632
  );
15488
17633
  }
15489
17634
  }
15490
- const writeLine = async (line) => {
15491
- const stream = output2 ?? process.stdout;
15492
- const accepted = output2 ? output2.write(line) : writeRawCommandOutput(line);
15493
- if (accepted) return;
15494
- await new Promise((resolve2, reject) => {
15495
- const onDrain = () => {
15496
- stream.off("error", onError);
15497
- resolve2();
15498
- };
15499
- const onError = (error) => {
15500
- stream.off("drain", onDrain);
15501
- reject(error);
15502
- };
15503
- stream.once("drain", onDrain);
15504
- stream.once("error", onError);
15505
- });
15506
- };
15507
- let cursor;
15508
- let written = 0;
15509
- let pages = 0;
15510
- let totals;
15511
- do {
15512
- const response = await exportEventsPage(requireCredentials(), {
15513
- from: opts.from,
15514
- to: opts.to,
15515
- experiment: opts.experiment,
15516
- eventTypes: opts.eventType?.split(",").map((value) => value.trim()).filter(Boolean),
15517
- qa: opts.qa,
15518
- limit,
15519
- cursor
15520
- });
15521
- if (!response.ok || !response.data) {
15522
- output2?.destroy();
15523
- fail(
15524
- command,
15525
- response.error ?? "Event export page failed",
15526
- classifyStatus(response.status),
15527
- response.errorDetails
15528
- );
15529
- }
15530
- if (outputError)
15531
- fail(
15532
- command,
15533
- `Cannot write ${opts.out}: ${outputError.message}`,
15534
- EXIT_CODES.general,
15535
- {
15536
- code: "local_file_error"
17635
+ try {
17636
+ const writeLine = async (line) => {
17637
+ const stream = output2 ?? process.stdout;
17638
+ const accepted = output2 ? output2.write(line) : writeRawCommandOutput(line);
17639
+ if (accepted) return;
17640
+ await new Promise((resolve2, reject) => {
17641
+ const onDrain = () => {
17642
+ stream.off("error", onError);
17643
+ resolve2();
17644
+ };
17645
+ const onError = (error) => {
17646
+ stream.off("drain", onDrain);
17647
+ reject(error);
17648
+ };
17649
+ stream.once("drain", onDrain);
17650
+ stream.once("error", onError);
17651
+ });
17652
+ };
17653
+ let cursor;
17654
+ let written = 0;
17655
+ let pages = 0;
17656
+ let totals;
17657
+ do {
17658
+ const response = await exportEventsPage(requireCredentials(), {
17659
+ from: opts.from,
17660
+ to: opts.to,
17661
+ experiment: opts.experiment,
17662
+ eventTypes: opts.eventType?.split(",").map((value) => value.trim()).filter(Boolean),
17663
+ qa: opts.qa,
17664
+ limit,
17665
+ cursor
17666
+ });
17667
+ if (!response.ok || !response.data) {
17668
+ fail(
17669
+ command,
17670
+ response.error ?? "Event export page failed",
17671
+ classifyStatus(response.status),
17672
+ response.errorDetails
17673
+ );
17674
+ }
17675
+ if (outputError)
17676
+ fail(
17677
+ command,
17678
+ `Cannot write ${opts.out}: ${outputError.message}`,
17679
+ EXIT_CODES.general,
17680
+ {
17681
+ code: "local_file_error"
17682
+ }
17683
+ );
17684
+ const page = response.data.data;
17685
+ totals ??= page.totals;
17686
+ for (const event of page.events) {
17687
+ try {
17688
+ await writeLine(`${JSON.stringify(event)}
17689
+ `);
17690
+ } catch (error) {
17691
+ fail(
17692
+ command,
17693
+ `Cannot write ${opts.out ?? "stdout"}: ${error instanceof Error ? error.message : String(error)}`,
17694
+ EXIT_CODES.general,
17695
+ { code: "local_file_error" }
17696
+ );
15537
17697
  }
15538
- );
15539
- const page = response.data.data;
15540
- totals ??= page.totals;
15541
- for (const event of page.events) {
17698
+ written += 1;
17699
+ }
17700
+ pages += 1;
17701
+ cursor = page.next_cursor ?? void 0;
17702
+ } while (cursor);
17703
+ if (output2) {
15542
17704
  try {
15543
- await writeLine(`${JSON.stringify(event)}
15544
- `);
17705
+ await new Promise((resolve2, reject) => {
17706
+ if (outputError) return reject(outputError);
17707
+ output2.once("error", reject);
17708
+ output2.end(resolve2);
17709
+ });
15545
17710
  } catch (error) {
15546
17711
  fail(
15547
17712
  command,
15548
- `Cannot write ${opts.out ?? "stdout"}: ${error instanceof Error ? error.message : String(error)}`,
17713
+ `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
15549
17714
  EXIT_CODES.general,
15550
17715
  { code: "local_file_error" }
15551
17716
  );
15552
17717
  }
15553
- written += 1;
15554
- }
15555
- pages += 1;
15556
- cursor = page.next_cursor ?? void 0;
15557
- } while (cursor);
15558
- if (output2) {
15559
- try {
15560
- await new Promise((resolve2, reject) => {
15561
- if (outputError) return reject(outputError);
15562
- output2.once("error", reject);
15563
- output2.end(resolve2);
15564
- });
15565
- } catch (error) {
15566
- fail(
15567
- command,
15568
- `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
15569
- EXIT_CODES.general,
15570
- { code: "local_file_error" }
15571
- );
17718
+ if (outputError)
17719
+ fail(
17720
+ command,
17721
+ `Cannot write ${opts.out}: ${outputError.message}`,
17722
+ EXIT_CODES.general,
17723
+ {
17724
+ code: "local_file_error"
17725
+ }
17726
+ );
15572
17727
  }
15573
- if (outputError)
17728
+ const matches = totals ? written === totals.events : false;
17729
+ const summary = {
17730
+ events_written: written,
17731
+ pages,
17732
+ totals: totals ?? null,
17733
+ count_matches: matches
17734
+ };
17735
+ if (!matches)
15574
17736
  fail(
15575
17737
  command,
15576
- `Cannot write ${opts.out}: ${outputError.message}`,
17738
+ "Exported event count does not match the server total. Totals are counted when the first page is read, so events that arrive late for a window ending in the last few minutes, or rows sharing one timestamp and event ID, can cause a difference. Re-run for a window that ended at least 15 minutes ago.",
15577
17739
  EXIT_CODES.general,
15578
17740
  {
15579
- code: "local_file_error"
17741
+ reason: "export_count_mismatch",
17742
+ ...summary
15580
17743
  }
15581
17744
  );
15582
- }
15583
- const matches = totals ? written === totals.events : false;
15584
- const summary = {
15585
- events_written: written,
15586
- pages,
15587
- totals: totals ?? null,
15588
- count_matches: matches
15589
- };
15590
- if (!matches)
15591
- fail(
15592
- command,
15593
- "Exported event count does not match the server total. Totals are counted when the first page is read, so events that arrive late for a window ending in the last few minutes, or rows sharing one timestamp and event ID, can cause a difference. Re-run for a window that ended at least 15 minutes ago.",
15594
- EXIT_CODES.general,
15595
- {
15596
- reason: "export_count_mismatch",
15597
- ...summary
15598
- }
15599
- );
15600
- if (summaryOnStdout) writeEnvelope(summary, "apex events export");
15601
- process.stderr.write(
15602
- `Exported ${written} events across ${pages} page(s); total ${totals?.events ?? "unknown"}; count matches: ${matches}.
17745
+ if (summaryOnStdout) writeEnvelope(summary, "apex events export");
17746
+ process.stderr.write(
17747
+ `Exported ${written} events across ${pages} page(s); total ${totals?.events ?? "unknown"}; count matches: ${matches}.
15603
17748
  `
15604
- );
17749
+ );
17750
+ } catch (error) {
17751
+ if (output2 && opts.out) {
17752
+ output2.destroy();
17753
+ import_node_fs14.default.rmSync(opts.out, { force: true });
17754
+ const suffix = `The partial file ${opts.out} was removed.`;
17755
+ if (error instanceof CliError)
17756
+ throw new CliError(
17757
+ `${error.message} ${suffix}`,
17758
+ error.exitCode,
17759
+ { ...error.details, partial_file_removed: true },
17760
+ error.api
17761
+ );
17762
+ throw new CliError(
17763
+ `${error instanceof Error ? error.message : String(error)} ${suffix}`,
17764
+ EXIT_CODES.general,
17765
+ { code: "local_file_error", partial_file_removed: true }
17766
+ );
17767
+ }
17768
+ throw error;
17769
+ }
15605
17770
  });
15606
17771
 
15607
17772
  // src/commands/goals.ts
@@ -15652,6 +17817,10 @@ function sdkPageviewGoalMatch(urlPattern, href) {
15652
17817
  var INVENTORY_PAGE_SIZE = 100;
15653
17818
  var GOALS_PAGE_SIZE = 100;
15654
17819
  var MAX_GOAL_PAGES = 100;
17820
+ var MAX_PAGE_LOOKUP_PAGES = 1e3;
17821
+ var UUID_V1_TO_V8 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
17822
+ var PAGE_OPTION_DESCRIPTION = "Page scope: page ID or page key (required for click goals, optional for pageview; a page key needs pages:read; a page ID does not; list pages with apex pages list)";
17823
+ var PAGE_NEXT_STEPS = [{ command: "apex pages list", why: "Find the page ID or key." }];
15655
17824
  var NON_QUANTIFIABLE = /* @__PURE__ */ new Set(["(", "|", "^", "$"]);
15656
17825
  function urlPatternDependence(pattern) {
15657
17826
  let fragment = false;
@@ -15794,6 +17963,58 @@ function parseCapture(command, value) {
15794
17963
  function cleanBody(input) {
15795
17964
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
15796
17965
  }
17966
+ async function resolveGoalPageId(command, creds, value) {
17967
+ if (UUID_V1_TO_V8.test(value)) return value;
17968
+ let cursor;
17969
+ for (let page = 0; page < MAX_PAGE_LOOKUP_PAGES; page += 1) {
17970
+ const response = await requestTargetingResource(
17971
+ creds,
17972
+ "GET",
17973
+ "pages",
17974
+ void 0,
17975
+ void 0,
17976
+ 100,
17977
+ cursor
17978
+ );
17979
+ if (!response.ok || !response.data) {
17980
+ if (response.status === 403) {
17981
+ fail(
17982
+ command,
17983
+ "Resolving a page key needs the pages:read permission. Pass the page ID instead (find it with apex pages list using a key that has pages:read, or in the dashboard).",
17984
+ EXIT_CODES.auth,
17985
+ {
17986
+ code: "page_lookup_forbidden",
17987
+ next_steps: [
17988
+ {
17989
+ command: "apex pages list",
17990
+ why: "Use a key with pages:read to find the page ID, or get it from the dashboard; pass --page <page ID> instead."
17991
+ }
17992
+ ]
17993
+ }
17994
+ );
17995
+ }
17996
+ fail(
17997
+ command,
17998
+ response.error ?? "Failed to list pages",
17999
+ classifyStatus(response.status),
18000
+ response.errorDetails
18001
+ );
18002
+ }
18003
+ if (!("data" in response.data) || !Array.isArray(response.data.data)) break;
18004
+ const pageData = response.data;
18005
+ const match2 = pageData.data.find((item) => item.key === value);
18006
+ if (match2) return match2.id;
18007
+ const next = pageData.pagination?.next_cursor ?? null;
18008
+ if (!pageData.pagination?.has_more || !next || next === cursor) break;
18009
+ cursor = next;
18010
+ }
18011
+ fail(
18012
+ command,
18013
+ `No active page with key ${value} in this shop; list pages with apex pages list`,
18014
+ EXIT_CODES.validation,
18015
+ { code: "page_not_found", next_steps: PAGE_NEXT_STEPS }
18016
+ );
18017
+ }
15797
18018
  function printGoal(goal) {
15798
18019
  console.log();
15799
18020
  console.log(source_default.bold(goal.name));
@@ -15806,6 +18027,7 @@ function printGoal(goal) {
15806
18027
  if (goal.text_match) console.log(` Text match: ${goal.text_match}`);
15807
18028
  if (goal.url_pattern) console.log(` URL: ${goal.url_pattern}`);
15808
18029
  if (goal.event_name) console.log(` Event: ${goal.event_name}`);
18030
+ if (goal.page_id) console.log(` Page: ${goal.page_id}`);
15809
18031
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
15810
18032
  console.log();
15811
18033
  }
@@ -15833,8 +18055,22 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
15833
18055
  );
15834
18056
  });
15835
18057
  });
15836
- var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action35(opts) {
18058
+ var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--page <id-or-key>", PAGE_OPTION_DESCRIPTION).option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action35(opts) {
15837
18059
  const creds = requireCredentials();
18060
+ if (opts.type === "click" && !opts.page) {
18061
+ fail(
18062
+ this,
18063
+ "Click goals need a page: pass --page <page id or key> (list pages with apex pages list)",
18064
+ EXIT_CODES.validation,
18065
+ { code: "page_required", next_steps: PAGE_NEXT_STEPS }
18066
+ );
18067
+ }
18068
+ if ((opts.type === "custom" || opts.type === "revenue") && opts.page) {
18069
+ fail(this, `--page is not supported for ${opts.type} goals`, EXIT_CODES.validation, {
18070
+ code: "page_not_supported"
18071
+ });
18072
+ }
18073
+ const pageId = typeof opts.page === "string" ? await resolveGoalPageId(this, creds, opts.page) : void 0;
15838
18074
  const body5 = cleanBody({
15839
18075
  name: opts.name,
15840
18076
  type: opts.type,
@@ -15845,6 +18081,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
15845
18081
  text_match: opts.textMatch,
15846
18082
  url_pattern: opts.urlPattern,
15847
18083
  event_name: opts.eventName,
18084
+ page_id: pageId,
15848
18085
  count_once: opts.countOnce === true,
15849
18086
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
15850
18087
  });
@@ -15862,8 +18099,9 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
15862
18099
  var update2 = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").option(
15863
18100
  "--value-property <property>",
15864
18101
  "Numeric event property for value goals; pass null to clear"
15865
- ).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action36(id, opts) {
18102
+ ).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--page <id-or-key>", `${PAGE_OPTION_DESCRIPTION}; pass null to clear`).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 action36(id, opts) {
15866
18103
  const creds = requireCredentials();
18104
+ const pageId = opts.page === "null" ? null : typeof opts.page === "string" ? await resolveGoalPageId(this, creds, opts.page) : void 0;
15867
18105
  const body5 = cleanBody({
15868
18106
  name: opts.name,
15869
18107
  type: opts.type,
@@ -15874,6 +18112,7 @@ var update2 = new Command("update").description("Update a goal").argument("<id>"
15874
18112
  text_match: opts.textMatch,
15875
18113
  url_pattern: opts.urlPattern,
15876
18114
  event_name: opts.eventName,
18115
+ page_id: pageId,
15877
18116
  count_once: opts.countOnce,
15878
18117
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
15879
18118
  });
@@ -17518,13 +19757,13 @@ var NAMED_HTML_ENTITIES = {
17518
19757
  nbsp: "\xA0"
17519
19758
  };
17520
19759
  function decodeHtmlEntities(value) {
17521
- return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z]+);/g, (match2, entity) => {
17522
- if (entity.startsWith("#")) {
17523
- const hex = entity[1] === "x" || entity[1] === "X";
17524
- const code = Number.parseInt(hex ? entity.slice(2) : entity.slice(1), hex ? 16 : 10);
19760
+ return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z]+);/g, (match2, entity2) => {
19761
+ if (entity2.startsWith("#")) {
19762
+ const hex = entity2[1] === "x" || entity2[1] === "X";
19763
+ const code = Number.parseInt(hex ? entity2.slice(2) : entity2.slice(1), hex ? 16 : 10);
17525
19764
  return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : match2;
17526
19765
  }
17527
- return NAMED_HTML_ENTITIES[entity.toLowerCase()] ?? match2;
19766
+ return NAMED_HTML_ENTITIES[entity2.toLowerCase()] ?? match2;
17528
19767
  });
17529
19768
  }
17530
19769
  function printSettings(settings2) {
@@ -18294,9 +20533,6 @@ function resolveWorkerBase(options) {
18294
20533
  const raw = options.worker ?? process.env.DRIP_WORKER_URL ?? PRODUCTION_WORKER_BASE_URL;
18295
20534
  return raw.trim().replace(/\/+$/, "");
18296
20535
  }
18297
- function resolveShopId(options) {
18298
- return (options.shopId ?? process.env.DRIP_SHOP_ID ?? "").trim() || null;
18299
- }
18300
20536
  function servedConfigUrl(workerBase, shopId) {
18301
20537
  return `${workerBase}/config?shopId=${encodeURIComponent(shopId)}&env=${PRODUCTION_ENVIRONMENT_KEY}`;
18302
20538
  }
@@ -18469,7 +20705,8 @@ async function readGoals(creds) {
18469
20705
  return { goals };
18470
20706
  }
18471
20707
  async function runDoctor(options = {}) {
18472
- const creds = requireCredentials();
20708
+ const resolved = resolveCheckShop(options.shopId, process.env.DRIP_SHOP_ID);
20709
+ const { creds } = resolved;
18473
20710
  if (options.check === "purchase_capture") {
18474
20711
  const domainsRes2 = await getInstallDomains(creds);
18475
20712
  if (!domainsRes2.ok || !domainsRes2.data)
@@ -18479,7 +20716,9 @@ async function runDoctor(options = {}) {
18479
20716
  classifyStatus(domainsRes2.status),
18480
20717
  domainsRes2.errorDetails
18481
20718
  );
18482
- const shopId2 = resolveShopId(options) ?? domainsRes2.data.shop_id;
20719
+ if (resolved.shopId)
20720
+ assertResponseShop(domainsRes2.data.shop_id, resolved.shopId, creds.credentialKind);
20721
+ const shopId2 = resolved.shopId ?? domainsRes2.data.shop_id;
18483
20722
  const servedResult2 = shopId2 ? await fetchServedConfig(resolveWorkerBase(options), shopId2) : {};
18484
20723
  const readTracking = async (minutes) => {
18485
20724
  try {
@@ -18530,6 +20769,7 @@ async function runDoctor(options = {}) {
18530
20769
  apiFailureOf(res)
18531
20770
  );
18532
20771
  }
20772
+ assertResponseShop(domainsRes.data.shop_id, resolved.shopId, creds.credentialKind);
18533
20773
  for (const [label, res] of [
18534
20774
  ["url inventory", inventory],
18535
20775
  ["goals", goals]
@@ -18573,7 +20813,7 @@ async function runDoctor(options = {}) {
18573
20813
  withCaveat(devPollutionCheck(eventHosts, envsRes.data, unparsedRows), inventoryCaveat)
18574
20814
  );
18575
20815
  const primary = hostOnly(domainsRes.data.domain) ?? eventHosts[0];
18576
- const shopId = resolveShopId(options) ?? domainsRes.data.shop_id;
20816
+ const shopId = resolved.shopId ?? domainsRes.data.shop_id;
18577
20817
  const workerBase = resolveWorkerBase(options);
18578
20818
  const [page, servedResult] = await Promise.all([
18579
20819
  primary ? fetchPrimaryHtml(primary) : Promise.resolve({}),
@@ -18794,7 +21034,7 @@ function consentRateCheck(items) {
18794
21034
  }
18795
21035
  var doctorCommand = new Command("doctor").description("Check shop configuration against observed analytics").option(
18796
21036
  "--shop-id <uuid>",
18797
- "Override the shop ID discovered from the API (DRIP_SHOP_ID also overrides it)"
21037
+ "Apex shop ID to check; API requests use it too (DRIP_SHOP_ID is the fallback when no shop is selected)"
18798
21038
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
18799
21039
  "--worker <url>",
18800
21040
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
@@ -18803,7 +21043,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
18803
21043
  if (options.check && data.checks.length === 0)
18804
21044
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
18805
21045
  output(this, data, () => printDoctorReport(data.checks));
18806
- if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.general;
21046
+ if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.checkFailed;
18807
21047
  });
18808
21048
  function printDoctorReport(checks) {
18809
21049
  printTable(
@@ -18987,7 +21227,7 @@ Examples:
18987
21227
  );
18988
21228
 
18989
21229
  // src/commands/install.ts
18990
- var import_promises4 = require("node:dns/promises");
21230
+ var import_promises3 = require("node:dns/promises");
18991
21231
  var import_node_http2 = require("node:http");
18992
21232
  var import_node_https = require("node:https");
18993
21233
  var import_node_net = require("node:net");
@@ -19045,7 +21285,7 @@ var testResolver;
19045
21285
  var testFetch;
19046
21286
  async function resolveHost(hostname) {
19047
21287
  const mocked = testResolver?.(hostname) ?? null;
19048
- const addresses = mocked ?? await (0, import_promises4.lookup)(hostname, { all: true });
21288
+ const addresses = mocked ?? await (0, import_promises3.lookup)(hostname, { all: true });
19049
21289
  return addresses.map((entry) => entry.address);
19050
21290
  }
19051
21291
  async function validateUrl(raw, allowedHosts) {
@@ -19525,16 +21765,16 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
19525
21765
  };
19526
21766
  }
19527
21767
  var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description(
19528
- "Verify snippet, served config, and recent tracking using --shop-id, the selected shop, or DRIP_SHOP_ID"
21768
+ "Verify snippet, served config, and recent tracking for --shop-id (which also selects the shop for API requests), the selected shop, or DRIP_SHOP_ID"
19529
21769
  ).option("--url <url>", "Check one explicit bound storefront URL").option(
19530
21770
  "--shop-id <id>",
19531
- "Apex shop ID to verify (falls back to --shop/apex use, then DRIP_SHOP_ID)"
21771
+ "Apex shop ID to verify; API requests use it too (falls back to --shop/apex use, then DRIP_SHOP_ID)"
19532
21772
  ).option(
19533
21773
  "--worker-url <url>",
19534
21774
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
19535
21775
  ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action65(opts) {
19536
- const creds = requireCredentials();
19537
- const shopId = opts.shopId?.trim() || resolveContext(creds).shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
21776
+ const resolved = resolveCheckShop(opts.shopId, process.env.DRIP_SHOP_ID);
21777
+ const { creds, shopId, overriddenShopId } = resolved;
19538
21778
  if (!shopId)
19539
21779
  fail(
19540
21780
  this,
@@ -19542,6 +21782,10 @@ var installCommand = new Command("install").description("Check Apex storefront i
19542
21782
  EXIT_CODES.validation,
19543
21783
  { field: "shop-id" }
19544
21784
  );
21785
+ if (overriddenShopId)
21786
+ console.error(
21787
+ `Checking shop ${shopId} from --shop-id instead of the selected shop ${overriddenShopId}.`
21788
+ );
19545
21789
  const domains = await getInstallDomains(creds);
19546
21790
  if (!domains.ok || !domains.data)
19547
21791
  fail(
@@ -19550,6 +21794,7 @@ var installCommand = new Command("install").description("Check Apex storefront i
19550
21794
  classifyStatus(domains.status),
19551
21795
  domains.errorDetails
19552
21796
  );
21797
+ assertResponseShop(domains.data.shop_id, shopId, creds.credentialKind);
19553
21798
  const mockStoreOrigins = cliTestSetting("STORE_ORIGINS");
19554
21799
  const storeOrigins = cliTestSetting("API") === "1" && mockStoreOrigins ? mockStoreOrigins.split(",") : domains.data.storeOrigins;
19555
21800
  const origins = opts.url ? [opts.url] : storeOrigins;
@@ -19581,7 +21826,16 @@ var installCommand = new Command("install").description("Check Apex storefront i
19581
21826
  });
19582
21827
  continue;
19583
21828
  }
19584
- if (error instanceof UrlPolicyError) fail(this, error.message, EXIT_CODES.validation);
21829
+ if (error instanceof UrlPolicyError) {
21830
+ const boundHosts = [...allowedHosts].sort();
21831
+ const unbound = error.message.includes("host is not bound to this shop");
21832
+ fail(
21833
+ this,
21834
+ unbound ? `${error.message} (shop ${shopId} is bound to: ${boundHosts.join(", ")})` : error.message,
21835
+ EXIT_CODES.validation,
21836
+ unbound ? { code: "url_not_bound", field: "url", shop_id: shopId, bound_hosts: boundHosts } : { field: "url" }
21837
+ );
21838
+ }
19585
21839
  if (error instanceof BodyDecodeError) {
19586
21840
  pages.push({
19587
21841
  url,
@@ -19618,10 +21872,12 @@ var installCommand = new Command("install").description("Check Apex storefront i
19618
21872
  );
19619
21873
  let tracking;
19620
21874
  let trackingExitCode = null;
21875
+ let trackingApiFailure;
19621
21876
  try {
19622
21877
  const result = await verifyTracking(creds);
19623
21878
  if (!result.ok || !result.data) {
19624
21879
  trackingExitCode = classifyStatus(result.status);
21880
+ trackingApiFailure = apiFailureOf(result);
19625
21881
  tracking = {
19626
21882
  status: "FAIL",
19627
21883
  message: result.error ?? "Failed to verify tracking",
@@ -19645,6 +21901,11 @@ var installCommand = new Command("install").description("Check Apex storefront i
19645
21901
  if (servedConfig.status !== "PASS") warnings.push(servedConfig.message);
19646
21902
  if (tracking.status === "WARN") warnings.push(tracking.message);
19647
21903
  const failed = pages.some((page) => page.status === "FAIL") || servedConfig.status === "FAIL" || tracking.status === "FAIL";
21904
+ const failedChecks = [
21905
+ ...pages.filter((page) => page.status === "FAIL").map((page) => ({ check: "page", url: page.url, message: page.message })),
21906
+ ...servedConfig.status === "FAIL" ? [{ check: "served_config", url: servedConfig.url, message: servedConfig.message }] : [],
21907
+ ...tracking.status === "FAIL" ? [{ check: "tracking", message: tracking.message }] : []
21908
+ ];
19648
21909
  const data = { ok: !failed, pages, warnings, servedConfig, tracking };
19649
21910
  const renderReport = () => {
19650
21911
  printTable(
@@ -19676,12 +21937,24 @@ var installCommand = new Command("install").description("Check Apex storefront i
19676
21937
  };
19677
21938
  if (failed) {
19678
21939
  if (!shouldUseJson(this)) renderReport();
19679
- fail(
19680
- this,
19681
- "Apex install check found a definite failure",
19682
- trackingExitCode ?? EXIT_CODES.general,
19683
- data
19684
- );
21940
+ if (trackingExitCode !== null)
21941
+ fail(
21942
+ this,
21943
+ `Apex install check could not verify tracking: ${tracking.message}`,
21944
+ trackingExitCode,
21945
+ { ...data, shop_id: shopId, failed_checks: failedChecks },
21946
+ trackingApiFailure
21947
+ );
21948
+ const summaries = failedChecks.map((entry) => {
21949
+ const label = entry.check === "page" && "url" in entry ? `page ${entry.url}` : entry.check === "served_config" ? "served config" : "tracking";
21950
+ return `${label}: ${entry.message.replace(/\.$/, "")}`;
21951
+ });
21952
+ fail(this, `Apex install check failed: ${summaries.join("; ")}`, EXIT_CODES.checkFailed, {
21953
+ ...data,
21954
+ code: "install_check_failed",
21955
+ shop_id: shopId,
21956
+ failed_checks: failedChecks
21957
+ });
19685
21958
  }
19686
21959
  output(this, data, renderReport);
19687
21960
  }).parent;
@@ -20332,6 +22605,258 @@ var personalizationsCommand = new Command("personalizations").description("Manag
20332
22605
  init_source();
20333
22606
  var import_node_fs23 = __toESM(require("node:fs"));
20334
22607
  var import_node_path20 = __toESM(require("node:path"));
22608
+
22609
+ // src/generated/exclusion-group-body.schema.json
22610
+ var exclusion_group_body_schema_default = {
22611
+ create: {
22612
+ type: "object",
22613
+ additionalProperties: false,
22614
+ properties: {
22615
+ name: {
22616
+ type: "string",
22617
+ minLength: 1,
22618
+ maxLength: 120,
22619
+ description: "Group name, 1 to 120 characters."
22620
+ },
22621
+ description: {
22622
+ type: ["string", "null"]
22623
+ },
22624
+ members: {
22625
+ type: "array",
22626
+ minItems: 2,
22627
+ description: "At least 2 distinct experiments. Use one shape per member: experiment_id with traffic_share as a fraction (0 to 1, shares total 1), or the dashboard shape experimentId with share as a percentage (0 to 100, shares total 100). Shares use 0.1 percent steps.",
22628
+ items: {
22629
+ oneOf: [
22630
+ {
22631
+ type: "object",
22632
+ additionalProperties: false,
22633
+ properties: {
22634
+ experiment_id: {
22635
+ type: "string",
22636
+ format: "uuid",
22637
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22638
+ },
22639
+ traffic_share: {
22640
+ type: "number",
22641
+ minimum: 0,
22642
+ maximum: 1
22643
+ }
22644
+ },
22645
+ required: ["experiment_id", "traffic_share"]
22646
+ },
22647
+ {
22648
+ type: "object",
22649
+ additionalProperties: false,
22650
+ properties: {
22651
+ experimentId: {
22652
+ type: "string",
22653
+ format: "uuid",
22654
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22655
+ },
22656
+ share: {
22657
+ type: "number",
22658
+ minimum: 0,
22659
+ maximum: 100
22660
+ }
22661
+ },
22662
+ required: ["experimentId", "share"]
22663
+ }
22664
+ ]
22665
+ }
22666
+ }
22667
+ },
22668
+ required: ["name", "members"]
22669
+ },
22670
+ update: {
22671
+ type: "object",
22672
+ additionalProperties: false,
22673
+ properties: {
22674
+ name: {
22675
+ type: "string",
22676
+ minLength: 1,
22677
+ maxLength: 120,
22678
+ description: "Group name, 1 to 120 characters."
22679
+ },
22680
+ description: {
22681
+ type: ["string", "null"]
22682
+ },
22683
+ members: {
22684
+ type: "array",
22685
+ minItems: 2,
22686
+ description: "At least 2 distinct experiments. Use one shape per member: experiment_id with traffic_share as a fraction (0 to 1, shares total 1), or the dashboard shape experimentId with share as a percentage (0 to 100, shares total 100). Shares use 0.1 percent steps.",
22687
+ items: {
22688
+ oneOf: [
22689
+ {
22690
+ type: "object",
22691
+ additionalProperties: false,
22692
+ properties: {
22693
+ experiment_id: {
22694
+ type: "string",
22695
+ format: "uuid",
22696
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22697
+ },
22698
+ traffic_share: {
22699
+ type: "number",
22700
+ minimum: 0,
22701
+ maximum: 1
22702
+ }
22703
+ },
22704
+ required: ["experiment_id", "traffic_share"]
22705
+ },
22706
+ {
22707
+ type: "object",
22708
+ additionalProperties: false,
22709
+ properties: {
22710
+ experimentId: {
22711
+ type: "string",
22712
+ format: "uuid",
22713
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22714
+ },
22715
+ share: {
22716
+ type: "number",
22717
+ minimum: 0,
22718
+ maximum: 100
22719
+ }
22720
+ },
22721
+ required: ["experimentId", "share"]
22722
+ }
22723
+ ]
22724
+ }
22725
+ }
22726
+ }
22727
+ },
22728
+ "replace-members": {
22729
+ type: "object",
22730
+ additionalProperties: false,
22731
+ properties: {
22732
+ members: {
22733
+ type: "array",
22734
+ minItems: 2,
22735
+ description: "At least 2 distinct experiments. Use one shape per member: experiment_id with traffic_share as a fraction (0 to 1, shares total 1), or the dashboard shape experimentId with share as a percentage (0 to 100, shares total 100). Shares use 0.1 percent steps.",
22736
+ items: {
22737
+ oneOf: [
22738
+ {
22739
+ type: "object",
22740
+ additionalProperties: false,
22741
+ properties: {
22742
+ experiment_id: {
22743
+ type: "string",
22744
+ format: "uuid",
22745
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22746
+ },
22747
+ traffic_share: {
22748
+ type: "number",
22749
+ minimum: 0,
22750
+ maximum: 1
22751
+ }
22752
+ },
22753
+ required: ["experiment_id", "traffic_share"]
22754
+ },
22755
+ {
22756
+ type: "object",
22757
+ additionalProperties: false,
22758
+ properties: {
22759
+ experimentId: {
22760
+ type: "string",
22761
+ format: "uuid",
22762
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
22763
+ },
22764
+ share: {
22765
+ type: "number",
22766
+ minimum: 0,
22767
+ maximum: 100
22768
+ }
22769
+ },
22770
+ required: ["experimentId", "share"]
22771
+ }
22772
+ ]
22773
+ }
22774
+ }
22775
+ },
22776
+ required: ["members"]
22777
+ }
22778
+ };
22779
+
22780
+ // src/commands/schema.ts
22781
+ var commandBodySchemas = /* @__PURE__ */ new WeakMap();
22782
+ function setCommandBodySchema(command, schema) {
22783
+ commandBodySchemas.set(command, schema);
22784
+ return command;
22785
+ }
22786
+ function optionSchema(option) {
22787
+ return {
22788
+ flags: option.flags,
22789
+ ...option.argChoices ? { choices: option.argChoices } : {},
22790
+ description: option.description,
22791
+ required: option.required,
22792
+ optional: option.optional,
22793
+ mandatory: Boolean(option.mandatory),
22794
+ variadic: option.variadic,
22795
+ ...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
22796
+ };
22797
+ }
22798
+ function commandSchema(command) {
22799
+ return {
22800
+ name: command.name(),
22801
+ aliases: command.aliases(),
22802
+ description: command.description(),
22803
+ usage: command.usage(),
22804
+ arguments: command.registeredArguments.map((arg) => ({
22805
+ name: arg.name(),
22806
+ required: arg.required,
22807
+ variadic: arg.variadic,
22808
+ description: arg.description,
22809
+ ...arg.defaultValue === void 0 ? {} : { defaultValue: arg.defaultValue }
22810
+ })),
22811
+ options: command.options.map(optionSchema),
22812
+ ...commandBodySchemas.has(command) ? { body: commandBodySchemas.get(command) } : {},
22813
+ commands: command.commands.map(commandSchema)
22814
+ };
22815
+ }
22816
+ function createSchemaCommand(program3) {
22817
+ return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
22818
+ const schema = {
22819
+ schemaVersion: 2,
22820
+ contract: {
22821
+ optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
22822
+ mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
22823
+ requiredArguments: "An argument's required field means the positional is mandatory.",
22824
+ body: "A command's body field is the JSON Schema of the JSON document it sends."
22825
+ },
22826
+ ...commandSchema(program3),
22827
+ files: experiment_filesystem_schema_default
22828
+ };
22829
+ output(this, schema, () => {
22830
+ process.stdout.write(`${JSON.stringify(schema, null, 2)}
22831
+ `);
22832
+ });
22833
+ });
22834
+ }
22835
+
22836
+ // src/commands/targeting-resources.ts
22837
+ var exclusionHelp = `Body shape: {"name":"...","members":[...]} (update fields are optional)
22838
+ Members: at least 2 distinct experiments; use experiment_id + traffic_share (0 to 1, total 1) or experimentId + share (0 to 100, total 100). Shares use 0.1 percent steps.
22839
+ Find experiment ids with: apex experiments list --fields id,name
22840
+ Example: apex exclusion-groups create '{"name":"PDP tests","members":[{"experiment_id":"<id-1>","traffic_share":0.5},{"experiment_id":"<id-2>","traffic_share":0.5}]}'
22841
+ Full JSON Schema: apex schema --json (this command's body key)`;
22842
+ function withBodyHelp(command, action78) {
22843
+ setCommandBodySchema(command, exclusion_group_body_schema_default[action78]);
22844
+ command.addHelpText("after", `
22845
+ ${exclusionHelp}
22846
+ `);
22847
+ }
22848
+ function exclusionFailureDetails(kind5, action78, status4, details) {
22849
+ if (kind5 !== "exclusion-groups" || status4 !== 400 && status4 !== 422) return details;
22850
+ return {
22851
+ ...details,
22852
+ next_steps: [
22853
+ {
22854
+ command: `apex exclusion-groups ${action78} --help`,
22855
+ why: "Shows the body shape, member rules and an example; error.details.issues names each failing field."
22856
+ }
22857
+ ]
22858
+ };
22859
+ }
20335
22860
  function body3(command, value) {
20336
22861
  if (!value) return {};
20337
22862
  const source = value.startsWith("@") ? value.slice(1) : value;
@@ -20409,7 +22934,10 @@ function targetingResourceCommand(kind5) {
20409
22934
  `${action78 === "create" ? "Create" : "Update"} a ${singular3}`
20410
22935
  );
20411
22936
  if (action78 === "update") command.argument("<id>");
20412
- command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
22937
+ command.argument(
22938
+ "[json]",
22939
+ kind5 === "exclusion-groups" ? "Request body as inline JSON, @file, or a file path (shape below)" : "Inline JSON, @file, or file path"
22940
+ ).option("--file <path>", "JSON body file").action(async function(...args) {
20413
22941
  const opts = this.opts();
20414
22942
  const id = action78 === "update" ? String(args[0]) : void 0;
20415
22943
  const value = opts.file ? `@${opts.file}` : String(args[action78 === "update" ? 1 : 0] ?? "") || void 0;
@@ -20428,10 +22956,11 @@ function targetingResourceCommand(kind5) {
20428
22956
  this,
20429
22957
  response.error ?? `Failed to ${action78}`,
20430
22958
  classifyStatus(response.status),
20431
- response.errorDetails
22959
+ exclusionFailureDetails(kind5, action78, response.status, response.errorDetails)
20432
22960
  );
20433
22961
  output(this, response.data, () => console.log(source_default.green(`${action78}d ${singular3}`)));
20434
22962
  });
22963
+ if (kind5 === "exclusion-groups") withBodyHelp(command, action78);
20435
22964
  root.addCommand(command);
20436
22965
  }
20437
22966
  const deleteCommand = new Command("delete").description(`Delete a ${singular3}`).argument("<id>");
@@ -20470,9 +22999,9 @@ function targetingResourceCommand(kind5) {
20470
22999
  output(this, response.data ?? null, () => console.log(source_default.green(`Deleted ${singular3}`)));
20471
23000
  })
20472
23001
  );
20473
- if (kind5 === "exclusion-groups")
23002
+ if (kind5 === "exclusion-groups") {
20474
23003
  root.addCommand(
20475
- new Command("replace-members").description("Replace exclusion group members").argument("<id>").argument("<json>", "Inline JSON, @file, or file path").action(async function(id, value) {
23004
+ new Command("replace-members").description("Replace exclusion group members").argument("<id>").argument("<json>", "Request body as inline JSON, @file, or a file path (shape below)").action(async function(id, value) {
20476
23005
  const response = await requestTargetingResource(
20477
23006
  requireCredentials(),
20478
23007
  "PUT",
@@ -20485,11 +23014,18 @@ function targetingResourceCommand(kind5) {
20485
23014
  this,
20486
23015
  response.error ?? "Failed to replace members",
20487
23016
  classifyStatus(response.status),
20488
- response.errorDetails
23017
+ exclusionFailureDetails(
23018
+ kind5,
23019
+ "replace-members",
23020
+ response.status,
23021
+ response.errorDetails
23022
+ )
20489
23023
  );
20490
23024
  output(this, response.data, () => console.log(source_default.green("Replaced members")));
20491
23025
  })
20492
23026
  );
23027
+ withBodyHelp(root.commands.at(-1), "replace-members");
23028
+ }
20493
23029
  return root;
20494
23030
  }
20495
23031
  var pagesCommand = targetingResourceCommand("pages");
@@ -20578,7 +23114,7 @@ var test2 = new Command("test").description("Evaluate experiment URL targeting f
20578
23114
  var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
20579
23115
 
20580
23116
  // src/commands/sos.ts
20581
- var import_node_crypto7 = require("node:crypto");
23117
+ var import_node_crypto8 = require("node:crypto");
20582
23118
  init_source();
20583
23119
  var import_node_fs24 = __toESM(require("node:fs"));
20584
23120
  var import_node_path21 = __toESM(require("node:path"));
@@ -20689,7 +23225,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
20689
23225
  const bytes = file.bytes;
20690
23226
  await uploadSosAttachment(grant.signed_url, file.content_type, bytes);
20691
23227
  attachmentPaths.push(grant.path);
20692
- attachmentDigests.push((0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex"));
23228
+ attachmentDigests.push((0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex"));
20693
23229
  }
20694
23230
  }
20695
23231
  const description = readText(opts.description) ?? descriptionArg ?? `Apex CLI ${opts.type} SOS request${opts.loom ? ` with Loom evidence: ${opts.loom}` : "."}`;
@@ -21002,55 +23538,6 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
21002
23538
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
21003
23539
  ).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);
21004
23540
 
21005
- // src/commands/schema.ts
21006
- function optionSchema(option) {
21007
- return {
21008
- flags: option.flags,
21009
- ...option.argChoices ? { choices: option.argChoices } : {},
21010
- description: option.description,
21011
- required: option.required,
21012
- optional: option.optional,
21013
- mandatory: Boolean(option.mandatory),
21014
- variadic: option.variadic,
21015
- ...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
21016
- };
21017
- }
21018
- function commandSchema(command) {
21019
- return {
21020
- name: command.name(),
21021
- aliases: command.aliases(),
21022
- description: command.description(),
21023
- usage: command.usage(),
21024
- arguments: command.registeredArguments.map((arg) => ({
21025
- name: arg.name(),
21026
- required: arg.required,
21027
- variadic: arg.variadic,
21028
- description: arg.description,
21029
- ...arg.defaultValue === void 0 ? {} : { defaultValue: arg.defaultValue }
21030
- })),
21031
- options: command.options.map(optionSchema),
21032
- commands: command.commands.map(commandSchema)
21033
- };
21034
- }
21035
- function createSchemaCommand(program3) {
21036
- return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
21037
- const schema = {
21038
- schemaVersion: 2,
21039
- contract: {
21040
- optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
21041
- mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
21042
- requiredArguments: "An argument's required field means the positional is mandatory."
21043
- },
21044
- ...commandSchema(program3),
21045
- files: experiment_filesystem_schema_default
21046
- };
21047
- output(this, schema, () => {
21048
- process.stdout.write(`${JSON.stringify(schema, null, 2)}
21049
- `);
21050
- });
21051
- });
21052
- }
21053
-
21054
23541
  // src/lib/gtm-miner.ts
21055
23542
  function extractContainerReferences(html) {
21056
23543
  const results2 = /* @__PURE__ */ new Map();
@@ -22085,7 +24572,7 @@ async function confirm(c, force, label, retry) {
22085
24572
  }
22086
24573
  return true;
22087
24574
  }
22088
- var orgCommand = new Command("org").description("Manage the current organization");
24575
+ var orgCommand = new Command("org").alias("orgs").description("Manage the current organization");
22089
24576
  orgCommand.command("get").action(async function() {
22090
24577
  await run2(this, getOrg(requireUserCredentials()), "get organization");
22091
24578
  });
@@ -22188,23 +24675,38 @@ orgCommand.command("remove-member").argument("<memberId>").option("-f, --force")
22188
24675
  return;
22189
24676
  await run2(this, removeOrgMember(requireUserCredentials(), id), "remove member");
22190
24677
  });
22191
- var teamCommand = new Command("team").description("Manage a shop team");
22192
- teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
24678
+ var teamCommand = new Command("team").alias("teams").description("Manage a shop team");
24679
+ var teamShopDescription = "Shop ID; defaults to the selected shop (--shop before the subcommand, DRIP_SHOP, or apex use)";
24680
+ function teamShop(command, explicit) {
24681
+ if (explicit) return explicit;
24682
+ const selected2 = requireUserCredentials().resolvedContext?.shopId;
24683
+ if (selected2) return selected2;
24684
+ fail(
24685
+ command,
24686
+ "apex team needs a shop. Pass --shop <id>, or select one with apex use <org>/<shop>.",
24687
+ 3,
24688
+ { code: "shop_context_required", retry: "apex use <org>/<shop>", flag: "--shop" }
24689
+ );
24690
+ }
24691
+ teamCommand.command("members").option("--shop <id>", teamShopDescription).action(async function(o) {
24692
+ o.shop = teamShop(this, o.shop);
22193
24693
  await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
22194
24694
  });
22195
- teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
24695
+ teamCommand.command("invite").argument("<email>").option("--shop <id>", teamShopDescription).addOption(
22196
24696
  new Option(
22197
24697
  "--role <role>",
22198
24698
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
22199
24699
  ).choices(["editor", "client_viewer"]).makeOptionMandatory()
22200
24700
  ).action(async function(email, o) {
24701
+ o.shop = teamShop(this, o.shop);
22201
24702
  await run2(
22202
24703
  this,
22203
24704
  inviteTeam(requireUserCredentials(), email, o.shop, o.role),
22204
24705
  "invite team member"
22205
24706
  );
22206
24707
  });
22207
- teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id>").action(async function(id, o) {
24708
+ teamCommand.command("revoke-invite").argument("<id>").option("--shop <id>", teamShopDescription).action(async function(id, o) {
24709
+ o.shop = teamShop(this, o.shop);
22208
24710
  await run2(
22209
24711
  this,
22210
24712
  revokeTeamInvite(requireUserCredentials(), id, o.shop),
@@ -22216,10 +24718,12 @@ teamCommand.command("set-role").argument("<memberId>").addArgument(
22216
24718
  "<role>",
22217
24719
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
22218
24720
  ).choices(["editor", "client_viewer"])
22219
- ).requiredOption("--shop <id>").action(async function(id, role, o) {
24721
+ ).option("--shop <id>", teamShopDescription).action(async function(id, role, o) {
24722
+ o.shop = teamShop(this, o.shop);
22220
24723
  await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
22221
24724
  });
22222
- teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
24725
+ teamCommand.command("remove").argument("<memberId>").option("--shop <id>", teamShopDescription).option("-f, --force").action(async function(id, o) {
24726
+ o.shop = teamShop(this, o.shop);
22223
24727
  if (!await confirm(
22224
24728
  this,
22225
24729
  o.force,
@@ -22230,6 +24734,38 @@ teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>
22230
24734
  await run2(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
22231
24735
  });
22232
24736
 
24737
+ // src/commands/upgrade.ts
24738
+ var import_node_child_process7 = require("node:child_process");
24739
+ var upgradeCommand = new Command("upgrade").description("Install the latest Apex CLI version").option("--dry-run", "Print the install command without running it").action(function(options) {
24740
+ if (options.dryRun) {
24741
+ output(
24742
+ this,
24743
+ { command: UPGRADE_COMMAND, ran: false, exit_code: null, previous_version: CLI_VERSION },
24744
+ () => console.log(UPGRADE_COMMAND)
24745
+ );
24746
+ return;
24747
+ }
24748
+ const npmBinary = process.env.APEX_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
24749
+ const json = shouldUseJson(this);
24750
+ const windows = process.platform === "win32";
24751
+ const command = windows ? `"${npmBinary}"` : npmBinary;
24752
+ const result = (0, import_node_child_process7.spawnSync)(command, ["install", "-g", "@drip-apex/apex-cli@latest"], {
24753
+ stdio: json ? ["inherit", 2, 2] : "inherit",
24754
+ shell: windows
24755
+ });
24756
+ if (result.error || result.status !== 0)
24757
+ fail(this, result.error?.message || "Apex CLI upgrade failed", EXIT_CODES.general, {
24758
+ code: "upgrade_failed",
24759
+ next_steps: [{ command: UPGRADE_COMMAND, why: "Run the install command manually." }]
24760
+ });
24761
+ clearUpdateCache();
24762
+ output(
24763
+ this,
24764
+ { command: UPGRADE_COMMAND, ran: true, exit_code: 0, previous_version: CLI_VERSION },
24765
+ () => console.log("Apex CLI upgraded.")
24766
+ );
24767
+ });
24768
+
22233
24769
  // src/commands/account.ts
22234
24770
  function userCredentials2() {
22235
24771
  return requireUserCredentials();
@@ -22835,7 +25371,7 @@ var import_node_fs29 = __toESM(require("node:fs"));
22835
25371
  var import_node_path26 = __toESM(require("node:path"));
22836
25372
 
22837
25373
  // src/commands/landing-pages.ts
22838
- var import_node_crypto8 = __toESM(require("node:crypto"));
25374
+ var import_node_crypto9 = __toESM(require("node:crypto"));
22839
25375
  var import_node_fs28 = __toESM(require("node:fs"));
22840
25376
  var import_node_path25 = __toESM(require("node:path"));
22841
25377
  function apiFailure(command, response, fallback2) {
@@ -22849,7 +25385,7 @@ function apiFailure(command, response, fallback2) {
22849
25385
  }
22850
25386
  }
22851
25387
  function sha256(body5) {
22852
- return import_node_crypto8.default.createHash("sha256").update(body5).digest("hex");
25388
+ return import_node_crypto9.default.createHash("sha256").update(body5).digest("hex");
22853
25389
  }
22854
25390
  function markerPath(folder) {
22855
25391
  return import_node_path25.default.join(folder, LANDING_PAGE_CHECKOUT_MARKER);
@@ -23598,7 +26134,10 @@ function addJsonOption(command) {
23598
26134
  if (!command.options.some((option) => option.long === "--shop"))
23599
26135
  command.option("--shop <id>", "Use a shop for this invocation");
23600
26136
  }
23601
- command.exitOverride((err) => interceptCliError(err, command));
26137
+ command.exitOverride((err) => {
26138
+ recordCommanderExit(err, command);
26139
+ interceptCliError(err, command);
26140
+ });
23602
26141
  for (const child of command.commands) addJsonOption(child);
23603
26142
  }
23604
26143
  var program2 = new Command().name("apex").description(source_default.bold("Apex CLI") + " \u2014 A/B testing experiments from the command line").version(CLI_VERSION).showSuggestionAfterError(true).showHelpAfterError(true);
@@ -23654,7 +26193,9 @@ program2.addCommand(skillsCommand);
23654
26193
  program2.addCommand(adminCommand, { hidden: true });
23655
26194
  program2.addCommand(orgCommand);
23656
26195
  program2.addCommand(teamCommand);
26196
+ program2.addCommand(upgradeCommand);
23657
26197
  addJsonOption(program2);
26198
+ registerTelemetryProgram(program2);
23658
26199
  var optionTerminatorIndex = process.argv.indexOf("--");
23659
26200
  var optionArgv = optionTerminatorIndex === -1 ? process.argv : process.argv.slice(0, optionTerminatorIndex);
23660
26201
  if (optionArgv.includes("--json")) program2.setOptionValue("json", true);