@drip-apex/apex-cli 0.15.3 → 0.15.4

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.15.4",
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) {
@@ -8569,8 +8913,9 @@ function attachExperimentQaEvidence(creds, id, body5) {
8569
8913
  function uploadExperimentQaEvidence(creds, id, form) {
8570
8914
  return requestForm(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, form);
8571
8915
  }
8572
- function approveExperimentQa(creds, id) {
8573
- return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
8916
+ function approveExperimentQa(creds, id, acknowledgement) {
8917
+ if (!acknowledgement) return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
8918
+ return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`, acknowledgement);
8574
8919
  }
8575
8920
  function reopenExperimentQa(creds, id) {
8576
8921
  return request(creds, "DELETE", `/api/v1/experiments/${id}/qa`);
@@ -8770,7 +9115,7 @@ function testTargeting(creds, url, opts) {
8770
9115
  return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
8771
9116
  }
8772
9117
  function createSos(creds, body5, options = {}) {
8773
- const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto2.createHash)("sha256").update(
9118
+ const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto3.createHash)("sha256").update(
8774
9119
  [
8775
9120
  body5.type,
8776
9121
  body5.experiment_id ?? "",
@@ -9267,6 +9612,16 @@ function resolveSelection(tree, input) {
9267
9612
  const shop = shopValue ? match(shopValue, org.shops, "shop") : void 0;
9268
9613
  return { orgId: org.id, ...shop ? { shopId: shop.id } : {} };
9269
9614
  }
9615
+ function resolveFlagSelection(tree, values) {
9616
+ if (values.org) {
9617
+ const org = match(values.org, tree.orgs, "organization");
9618
+ const shop2 = values.shop ? match(values.shop, org.shops, "shop") : void 0;
9619
+ return { orgId: org.id, ...shop2 ? { shopId: shop2.id } : {} };
9620
+ }
9621
+ const shops2 = tree.orgs.flatMap((org) => org.shops.map((shop2) => ({ ...shop2, orgId: org.id })));
9622
+ const shop = match(values.shop, shops2, "shop");
9623
+ return { orgId: shop.orgId, shopId: shop.id };
9624
+ }
9270
9625
  function ask(question) {
9271
9626
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
9272
9627
  return new Promise(
@@ -9279,8 +9634,23 @@ function ask(question) {
9279
9634
  async function pickContext(tree) {
9280
9635
  if (!process.stdin.isTTY || !process.stdout.isTTY)
9281
9636
  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
9637
+ "Interactive context selection requires a TTY. Pass <org>[/<shop>] to apex use, or use --org/--shop.",
9638
+ EXIT_CODES.validation,
9639
+ {
9640
+ code: "context_selection_required",
9641
+ candidates: tree.orgs.slice(0, 20).map((org2) => ({
9642
+ org_id: org2.id,
9643
+ org_name: org2.name,
9644
+ shops: org2.shops.slice(0, 20).map((shop2) => ({ id: shop2.id, name: shop2.name }))
9645
+ })),
9646
+ truncated: tree.orgs.length > 20 || tree.orgs.some((org2) => org2.shops.length > 20),
9647
+ next_steps: [
9648
+ {
9649
+ command: "apex use <org-id>/<shop-id>",
9650
+ why: "Choose one of error.details.candidates."
9651
+ }
9652
+ ]
9653
+ }
9284
9654
  );
9285
9655
  let org = tree.orgs.length === 1 ? tree.orgs[0] : void 0;
9286
9656
  if (!org) {
@@ -9400,7 +9770,7 @@ function parseFormBody(body5) {
9400
9770
  return new URLSearchParams(body5);
9401
9771
  }
9402
9772
  async function waitForBrowserLogin(baseUrl, userMode = false) {
9403
- const state = import_node_crypto3.default.randomBytes(24).toString("base64url");
9773
+ const state = import_node_crypto4.default.randomBytes(24).toString("base64url");
9404
9774
  const clientName = import_node_os6.default.hostname() || "Local machine";
9405
9775
  return new Promise((resolve2, reject) => {
9406
9776
  let settled = false;
@@ -9710,6 +10080,14 @@ Context: ${current.orgId ?? "none"}${current.shopId ? ` / ${current.shopId}` : "
9710
10080
 
9711
10081
  // src/commands/use.ts
9712
10082
  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) {
10083
+ const flags = this.optsWithGlobals();
10084
+ if (input && (flags.org || flags.shop))
10085
+ fail(
10086
+ this,
10087
+ "Pass the context either as <org>[/<shop>] or with --org/--shop, not both.",
10088
+ EXIT_CODES.validation,
10089
+ { code: "conflicting_context_input" }
10090
+ );
9713
10091
  const creds = requireUserCredentials();
9714
10092
  const response = await getAccountContext(creds);
9715
10093
  if (!response.ok || !response.data)
@@ -9719,9 +10097,32 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9719
10097
  classifyApiError(response.status, response.errorDetails),
9720
10098
  response.errorDetails
9721
10099
  );
9722
- const selected2 = input ? resolveSelection(response.data.data, input) : await pickContext(response.data.data);
10100
+ const selected2 = input ? resolveSelection(response.data.data, input) : flags.org || flags.shop ? resolveFlagSelection(response.data.data, flags) : await pickContext(response.data.data);
9723
10101
  const stored = getStoredCredentials();
9724
- if (!stored) fail(this, "No stored credential to update. Run apex login first.");
10102
+ if (!stored) {
10103
+ const env2 = {
10104
+ DRIP_ORG: selected2.orgId,
10105
+ ...selected2.shopId ? { DRIP_SHOP: selected2.shopId } : {}
10106
+ };
10107
+ const exportCommand = `export DRIP_ORG=${selected2.orgId}${selected2.shopId ? ` DRIP_SHOP=${selected2.shopId}` : " && unset DRIP_SHOP"}`;
10108
+ fail(
10109
+ this,
10110
+ `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.`,
10111
+ EXIT_CODES.validation,
10112
+ {
10113
+ code: "context_not_persistable",
10114
+ org_id: selected2.orgId,
10115
+ shop_id: selected2.shopId ?? null,
10116
+ env: env2,
10117
+ next_steps: [
10118
+ {
10119
+ command: exportCommand,
10120
+ why: "Selects this context for every later apex command in this environment."
10121
+ }
10122
+ ]
10123
+ }
10124
+ );
10125
+ }
9725
10126
  saveCredentials({ ...stored, context: selected2 });
9726
10127
  setEnvelopeContext({
9727
10128
  credential: "user",
@@ -9737,7 +10138,7 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9737
10138
 
9738
10139
  // src/commands/experiments.ts
9739
10140
  init_source();
9740
- var import_node_crypto5 = __toESM(require("node:crypto"));
10141
+ var import_node_crypto6 = __toESM(require("node:crypto"));
9741
10142
  var import_node_fs8 = __toESM(require("node:fs"));
9742
10143
  var import_node_path6 = __toESM(require("node:path"));
9743
10144
 
@@ -10052,7 +10453,7 @@ function deriveStoreUrl(patterns) {
10052
10453
  }
10053
10454
 
10054
10455
  // src/lib/asset-upload.ts
10055
- var import_node_crypto4 = __toESM(require("node:crypto"));
10456
+ var import_node_crypto5 = __toESM(require("node:crypto"));
10056
10457
  var import_node_fs7 = __toESM(require("node:fs"));
10057
10458
  var import_node_path5 = __toESM(require("node:path"));
10058
10459
  var INLINE_DATA_URI_MAX_CHARS = 4096;
@@ -10077,7 +10478,7 @@ var EXTENSION_BY_MIME = {
10077
10478
  "image/avif": ".avif"
10078
10479
  };
10079
10480
  function contentHash(bytes) {
10080
- return import_node_crypto4.default.createHash("sha256").update(bytes).digest("hex");
10481
+ return import_node_crypto5.default.createHash("sha256").update(bytes).digest("hex");
10081
10482
  }
10082
10483
  function deliveryUrl(baseUrl, asset) {
10083
10484
  const returnedUrl = asset.deliveryUrl ?? asset.delivery_url ?? asset.url;
@@ -11063,6 +11464,16 @@ function printExperimentResults(results2, currency) {
11063
11464
  const outcomeProvenance = summary.outcome.provenance === "manager_decided" ? " (manager decided)" : "";
11064
11465
  console.log();
11065
11466
  console.log(source_default.bold(String(experiment2.name ?? experiment2.id ?? "Experiment results")));
11467
+ if (typeof results2.computed_at === "string") {
11468
+ const ageSeconds = Math.max(
11469
+ 0,
11470
+ Math.round((Date.now() - Date.parse(results2.computed_at)) / 1e3)
11471
+ );
11472
+ const age = ageSeconds < 60 ? `${ageSeconds}s ago` : ageSeconds < 3600 ? `${Math.floor(ageSeconds / 60)}m ago` : `${Math.floor(ageSeconds / 3600)}h ago`;
11473
+ console.log(
11474
+ ` Computed: ${results2.computed_at} (${age}, from ${String(results2.results_source ?? "unknown")})`
11475
+ );
11476
+ }
11066
11477
  console.log(` Status: ${statusBadge(String(experiment2.status ?? "unknown"))}`);
11067
11478
  const audience = audienceLine(results2.segment_applied);
11068
11479
  if (audience) console.log(` Audience: ${audience}`);
@@ -11142,7 +11553,7 @@ var FILESYSTEM_FILE_RE = new RegExp(
11142
11553
  `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
11143
11554
  );
11144
11555
  function sha256Text(value) {
11145
- return import_node_crypto5.default.createHash("sha256").update(value).digest("hex");
11556
+ return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
11146
11557
  }
11147
11558
  function folderPath(folder) {
11148
11559
  return import_node_path6.default.resolve(process.cwd(), folder);
@@ -11261,7 +11672,7 @@ async function rewriteExperimentFilesystemAssets(input) {
11261
11672
  baseUrl: input.baseUrl,
11262
11673
  enabled: input.enabled,
11263
11674
  upload: async (file) => {
11264
- const hash = import_node_crypto5.default.createHash("sha256").update(file.bytes).digest("hex");
11675
+ const hash = import_node_crypto6.default.createHash("sha256").update(file.bytes).digest("hex");
11265
11676
  const cached = input.assets[hash];
11266
11677
  if (cached) return cached;
11267
11678
  if (input.upload === null) return null;
@@ -11591,7 +12002,7 @@ var list = new Command("list").alias("ls").description(
11591
12002
  "Filter by effective storefront URL targeting (applied server-side)"
11592
12003
  ).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
12004
  "--fields <list>",
11594
- "Comma-separated fields to return and table columns in order (JSON always keeps id)"
12005
+ "Comma-separated fields to return and table columns in order (JSON always keeps id; fields outside the summary switch to full details automatically)"
11595
12006
  ).action(async function action3(opts) {
11596
12007
  const creds = requireCredentials();
11597
12008
  const spinner = await createSpinner("Fetching experiments...");
@@ -11747,7 +12158,7 @@ ${JSON.stringify(data.order, null, 2)}`);
11747
12158
  console.log(`
11748
12159
  Definitions: ${data.definitions_url}`);
11749
12160
  }
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) {
12161
+ 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
12162
  if (opts.explain && (opts.segment !== void 0 || opts.excludeSegment !== void 0))
11752
12163
  fail(
11753
12164
  this,
@@ -11764,16 +12175,38 @@ var results = new Command("results").description("Read experiment results and ti
11764
12175
  spinner?.fail(`Experiment "${idOrSlug}" not found`);
11765
12176
  fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
11766
12177
  }
11767
- const res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, {
12178
+ const waitSeconds = Number(opts.wait ?? "120");
12179
+ if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
12180
+ fail(this, "--wait must be a non-negative number of seconds", EXIT_CODES.validation);
12181
+ }
12182
+ const startedWaitingAt = Date.now();
12183
+ let res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, {
11768
12184
  currency: opts.currency,
11769
12185
  segment: opts.segment,
11770
12186
  exclude_segment: opts.excludeSegment
11771
12187
  });
12188
+ while (!opts.explain && isResultsComputing(res)) {
12189
+ const elapsedSeconds = (Date.now() - startedWaitingAt) / 1e3;
12190
+ const remainingSeconds = waitSeconds - elapsedSeconds;
12191
+ if (remainingSeconds <= 0) break;
12192
+ const requestedDelay = Number(
12193
+ res.retryAfter ?? res.errorDetails?.retry_after_seconds ?? 15
12194
+ );
12195
+ const delaySeconds = Math.min(30, Math.max(0, requestedDelay), remainingSeconds);
12196
+ if (!shouldUseJson(this))
12197
+ console.error(`Results are computing on the server; retrying in ${delaySeconds} s`);
12198
+ await new Promise((resolve2) => setTimeout(resolve2, delaySeconds * 1e3));
12199
+ res = await getExperimentResults(creds, exp.id, {
12200
+ currency: opts.currency,
12201
+ segment: opts.segment,
12202
+ exclude_segment: opts.excludeSegment
12203
+ });
12204
+ }
11772
12205
  if (!res.ok || !res.data) {
11773
12206
  spinner?.fail("Failed to fetch experiment results");
11774
12207
  fail(
11775
12208
  this,
11776
- res.error ?? "Failed to fetch experiment results",
12209
+ `${res.error ?? "Failed to fetch experiment results"}${isResultsComputing(res) ? "; increase --wait to keep retrying" : ""}`,
11777
12210
  classifyStatus(res.status),
11778
12211
  res.errorDetails
11779
12212
  );
@@ -11790,6 +12223,11 @@ var results = new Command("results").description("Read experiment results and ti
11790
12223
  fail(this, error instanceof Error ? error.message : String(error));
11791
12224
  }
11792
12225
  });
12226
+ function isResultsComputing(res) {
12227
+ if (res.status !== 503) return false;
12228
+ const body5 = res.data;
12229
+ return body5?.error?.code === "results_computing" || res.errorDetails?.code === "results_computing";
12230
+ }
11793
12231
  var DAY_NAMES = {
11794
12232
  mon: "monday",
11795
12233
  monday: "monday",
@@ -12864,7 +13302,45 @@ var applyFilesystem = new Command("apply").option(
12864
13302
  acceptQaInvalidation = true;
12865
13303
  }
12866
13304
  const spinner = process.stderr.isTTY ? await createSpinner("Applying experiment filesystem...") : null;
12867
- let res = await applyExperimentFilesystem(creds, session.experimentId, {
13305
+ let verifiedAfterTimeout = false;
13306
+ let verifiedTimeoutMs;
13307
+ let publicationUnconfirmed = false;
13308
+ const applyWithVerification = async (body5) => {
13309
+ try {
13310
+ return await applyExperimentFilesystem(creds, session.experimentId, body5);
13311
+ } catch (error) {
13312
+ if (!(error instanceof CliError) || error.details?.reason !== "timeout" && error.details?.reason !== "reset")
13313
+ throw error;
13314
+ const reason2 = error.details.reason;
13315
+ const timeoutMs = typeof error.details.timeoutMs === "number" ? error.details.timeoutMs : APPLY_TIMEOUT_MS;
13316
+ const verification = await planExperimentFilesystem(creds, session.experimentId, {
13317
+ files
13318
+ }).catch(() => null);
13319
+ if (verification?.ok && verification.data?.ok === true && verification.data.changes.length === 0) {
13320
+ verifiedAfterTimeout = true;
13321
+ verifiedTimeoutMs = timeoutMs;
13322
+ publicationUnconfirmed = verification.data.qaApproval?.status !== "draft";
13323
+ return {
13324
+ ok: true,
13325
+ status: 200,
13326
+ data: { applied: true, plan: verification.data }
13327
+ };
13328
+ }
13329
+ const rerun = `apex experiments apply ${shellQuote(folderArg)}`;
13330
+ throw new CliError(
13331
+ `Apply did not confirm within ${timeoutMs / 1e3}s and the server does not yet hold these changes; re-run ${rerun}`,
13332
+ EXIT_CODES.general,
13333
+ {
13334
+ code: "apply_unconfirmed",
13335
+ reason: reason2,
13336
+ timeoutMs,
13337
+ pendingChanges: verification?.ok && verification.data ? verification.data.changes.length : null,
13338
+ next_steps: [{ command: rerun, why: "Re-run the unconfirmed apply." }]
13339
+ }
13340
+ );
13341
+ }
13342
+ };
13343
+ let res = await applyWithVerification({
12868
13344
  files,
12869
13345
  accept_qa_invalidation: acceptQaInvalidation,
12870
13346
  ...opts.confirmTargetingReplace ? { confirmTargetingReplace: true } : {}
@@ -12885,7 +13361,7 @@ var applyFilesystem = new Command("apply").option(
12885
13361
  output(this, { aborted: true }, () => console.log("Aborted."));
12886
13362
  return;
12887
13363
  }
12888
- res = await applyExperimentFilesystem(creds, session.experimentId, {
13364
+ res = await applyWithVerification({
12889
13365
  files,
12890
13366
  accept_qa_invalidation: acceptQaInvalidation,
12891
13367
  confirmTargetingReplace: true
@@ -12907,7 +13383,8 @@ var applyFilesystem = new Command("apply").option(
12907
13383
  plan: res.data?.plan
12908
13384
  });
12909
13385
  }
12910
- spinner?.succeed("Applied experiment filesystem");
13386
+ if (publicationUnconfirmed) spinner?.stop();
13387
+ else spinner?.succeed("Applied experiment filesystem");
12911
13388
  const synced = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
12912
13389
  if (!synced) {
12913
13390
  await refreshServerOwnedFilesIfNeeded(
@@ -12931,6 +13408,11 @@ var applyFilesystem = new Command("apply").option(
12931
13408
  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
13409
  const previewResult = await getQaPreviewUrls(creds, session.experimentId);
12933
13410
  const { previewUrls, dashboardUrl } = previewResult;
13411
+ const publicationWarning = "Storefront publication is unconfirmed; visitors may still receive the previous treatment.";
13412
+ const publicationNextStep = {
13413
+ command: `apex experiments status ${shellQuote(folderArg)} --json`,
13414
+ why: "Check experiment status before handover; persisted files do not confirm storefront publication."
13415
+ };
12934
13416
  output(
12935
13417
  this,
12936
13418
  {
@@ -12939,14 +13421,30 @@ var applyFilesystem = new Command("apply").option(
12939
13421
  applied: true,
12940
13422
  plan: planRes.data,
12941
13423
  requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
12942
- result: res.data,
13424
+ result: verifiedAfterTimeout ? null : res.data,
12943
13425
  synced,
12944
13426
  dashboardUrl,
12945
- receipt: res.data?.receipt ?? null,
12946
- previewUrls
13427
+ receipt: verifiedAfterTimeout ? null : res.data?.receipt ?? null,
13428
+ previewUrls,
13429
+ ...verifiedAfterTimeout ? { verifiedAfterTimeout: true, timeoutMs: verifiedTimeoutMs } : {},
13430
+ ...publicationUnconfirmed ? {
13431
+ persisted: true,
13432
+ publication: "unconfirmed",
13433
+ warnings: [publicationWarning],
13434
+ next_steps: [publicationNextStep]
13435
+ } : {}
12947
13436
  },
12948
13437
  () => {
12949
13438
  printFilesystemPlan(planRes.data);
13439
+ if (verifiedAfterTimeout) {
13440
+ console.log(
13441
+ `${publicationUnconfirmed ? "Apply saved" : "Apply confirmed"}: the server already holds these changes (the response timed out after ${verifiedTimeoutMs / 1e3}s).`
13442
+ );
13443
+ }
13444
+ if (publicationUnconfirmed) {
13445
+ console.log(`Warning: ${publicationWarning}`);
13446
+ console.log(`Next: ${publicationNextStep.command} (${publicationNextStep.why})`);
13447
+ }
12950
13448
  const receipt = res.data?.receipt;
12951
13449
  if (receipt) {
12952
13450
  console.log("\nReceipt");
@@ -13107,6 +13605,9 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
13107
13605
  "[folder-or-id]",
13108
13606
  "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
13109
13607
  "."
13608
+ ).option(
13609
+ "--acknowledge-zero-traffic",
13610
+ "Acknowledge variations that receive no traffic for this split (Apex staff only)"
13110
13611
  ).action(async function action19(target) {
13111
13612
  const creds = requireCredentials();
13112
13613
  const spinner = await createSpinner("Approving QA...");
@@ -13114,7 +13615,40 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
13114
13615
  const resolved = await resolveExperimentForFilesystemOrId(target);
13115
13616
  const before = await resolveExperiment(resolved.experimentId);
13116
13617
  if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
13117
- const res = await approveExperimentQa(creds, resolved.experimentId);
13618
+ let res = await approveExperimentQa(creds, resolved.experimentId);
13619
+ const details = res.errorDetails;
13620
+ if (!res.ok && res.status === 409 && details?.code === "degenerate_weights_ack_required") {
13621
+ if (!this.opts().acknowledgeZeroTraffic) {
13622
+ throw new CliError(
13623
+ `${res.error ?? "QA approval requires acknowledgement"} Rerun with --acknowledge-zero-traffic.`,
13624
+ classifyStatus(res.status),
13625
+ details
13626
+ );
13627
+ }
13628
+ if (details.internal_only === true) {
13629
+ throw new CliError(
13630
+ res.error ?? "Only Apex Admin or Internal users can acknowledge zero traffic.",
13631
+ classifyStatus(res.status),
13632
+ details
13633
+ );
13634
+ }
13635
+ if (typeof details.split_hash === "string" && details.split_hash.length > 0) {
13636
+ const weights = Array.isArray(details.variation_weights) ? details.variation_weights : [];
13637
+ for (const value of weights) {
13638
+ if (value && typeof value === "object") {
13639
+ const variation = value;
13640
+ process.stderr.write(
13641
+ `${variation.name ?? variation.id ?? "Variation"}: ${(variation.weight ?? 0) * 100}%${variation.weight === 0 ? " (no traffic)" : ""}
13642
+ `
13643
+ );
13644
+ }
13645
+ }
13646
+ res = await approveExperimentQa(creds, resolved.experimentId, {
13647
+ acknowledge_degenerate_weights: true,
13648
+ acknowledge_degenerate_weights_hash: details.split_hash
13649
+ });
13650
+ }
13651
+ }
13118
13652
  if (!res.ok)
13119
13653
  throw new CliError(
13120
13654
  res.error ?? "Failed to approve QA",
@@ -13419,7 +13953,7 @@ var screenshots = new Command("screenshots").description(
13419
13953
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
13420
13954
  )
13421
13955
  );
13422
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
13956
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
13423
13957
  continue;
13424
13958
  }
13425
13959
  if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
@@ -13429,7 +13963,7 @@ var screenshots = new Command("screenshots").description(
13429
13963
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
13430
13964
  )
13431
13965
  );
13432
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
13966
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
13433
13967
  continue;
13434
13968
  }
13435
13969
  break;
@@ -13583,7 +14117,7 @@ var screenshots = new Command("screenshots").description(
13583
14117
  );
13584
14118
  consecutiveTransientFailures += 1;
13585
14119
  if (consecutiveTransientFailures > 3) throw error;
13586
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14120
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13587
14121
  continue;
13588
14122
  }
13589
14123
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
@@ -13608,11 +14142,11 @@ var screenshots = new Command("screenshots").description(
13608
14142
  polled.errorDetails
13609
14143
  );
13610
14144
  }
13611
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14145
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13612
14146
  continue;
13613
14147
  }
13614
14148
  consecutiveTransientFailures = 0;
13615
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14149
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
13616
14150
  }
13617
14151
  if (opts.attachQa !== false) {
13618
14152
  const attached = await attachExperimentQaEvidence(creds, experiment2.id, {
@@ -13990,6 +14524,8 @@ init_source();
13990
14524
  async function signedPreviewFromReadiness(creds, experimentId, variationId, targetUrl) {
13991
14525
  try {
13992
14526
  const client = createHttpClient(creds, {
14527
+ onResponse: recordApiResponse2,
14528
+ observeNetworkErrors: true,
13993
14529
  onNetworkError: (info) => {
13994
14530
  throw new Error(info.cause);
13995
14531
  }
@@ -14233,8 +14769,7 @@ function loadHarnessConfig(folder) {
14233
14769
  experimentName: experiment2.name ?? id,
14234
14770
  variationSlug: slugify(variation, "variant-a"),
14235
14771
  variationName: `Variant ${variation}`,
14236
- targetUrl,
14237
- antiFlickerSelectors: []
14772
+ targetUrl
14238
14773
  }
14239
14774
  };
14240
14775
  }
@@ -14318,14 +14853,15 @@ async function createSession(folder) {
14318
14853
  if (!apex.targetUrl) {
14319
14854
  throw new Error("Missing targetUrl. Run `apex harness init` and set targetUrl.");
14320
14855
  }
14856
+ const resetJs = cleanupResetJs(experiment2);
14321
14857
  const res = await createHarnessSession(creds, {
14322
14858
  experimentSlug: apex.experimentSlug,
14323
14859
  experimentName: apex.experimentName,
14324
14860
  variationSlug: apex.variationSlug,
14325
14861
  variationName: apex.variationName,
14326
14862
  targetUrl: apex.targetUrl,
14327
- resetJs: cleanupResetJs(experiment2),
14328
- antiFlickerSelectors: apex.antiFlickerSelectors ?? []
14863
+ ...typeof resetJs === "string" ? { resetJs } : {},
14864
+ ...Array.isArray(apex.antiFlickerSelectors) ? { antiFlickerSelectors: apex.antiFlickerSelectors } : {}
14329
14865
  });
14330
14866
  if (!res.ok)
14331
14867
  throw new CliError(
@@ -14453,7 +14989,7 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
14453
14989
 
14454
14990
  // src/commands/operator.ts
14455
14991
  init_source();
14456
- var import_node_crypto6 = __toESM(require("node:crypto"));
14992
+ var import_node_crypto7 = __toESM(require("node:crypto"));
14457
14993
  var import_node_fs11 = __toESM(require("node:fs"));
14458
14994
  var import_node_path9 = __toESM(require("node:path"));
14459
14995
  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 +15034,13 @@ function formatWorkspaceBlockingIssues(value) {
14498
15034
  return lines.join("\n");
14499
15035
  }).join("\n");
14500
15036
  }
15037
+ function formatHarnessRefusal(message, details) {
15038
+ const issues = formatWorkspaceBlockingIssues(details);
15039
+ return issues ? `${message}
15040
+ ${issues}` : message;
15041
+ }
14501
15042
  function sha256Text2(value) {
14502
- return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
15043
+ return import_node_crypto7.default.createHash("sha256").update(value).digest("hex");
14503
15044
  }
14504
15045
  function byteLength(value) {
14505
15046
  return Buffer.byteLength(value, "utf8");
@@ -14692,12 +15233,11 @@ async function createExactHarnessSession(folder, session) {
14692
15233
  experimentName: expRes.data.name,
14693
15234
  variationSlug: config.variationSlug ?? variation.slug ?? slugify2(variation.name, "variation"),
14694
15235
  variationName: variation.name,
14695
- targetUrl,
14696
- antiFlickerSelectors: []
15236
+ targetUrl
14697
15237
  });
14698
15238
  if (!res.ok || !res.data)
14699
15239
  throw new CliError(
14700
- res.error ?? "Failed to create harness session",
15240
+ formatHarnessRefusal(res.error ?? "Failed to create harness session", res.errorDetails),
14701
15241
  classifyStatus(res.status),
14702
15242
  res.errorDetails
14703
15243
  );
@@ -14789,7 +15329,15 @@ async function runPublish(folderArg, opts = {}) {
14789
15329
  const harnessSession = await createExactHarnessSession(folder, session);
14790
15330
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
14791
15331
  const published = await publishHarnessSession(creds, harnessSession.sessionId);
14792
- if (!published.ok) throw new Error(published.error ?? "Failed to publish external artifacts");
15332
+ if (!published.ok)
15333
+ throw new CliError(
15334
+ formatHarnessRefusal(
15335
+ published.error ?? "Failed to publish external artifacts",
15336
+ published.errorDetails
15337
+ ),
15338
+ classifyStatus(published.status),
15339
+ published.errorDetails
15340
+ );
14793
15341
  external = {
14794
15342
  sessionId: harnessSession.sessionId,
14795
15343
  version: version.version,
@@ -15436,10 +15984,81 @@ trackingCommand.command("latency").description("Measure forced-preview versus na
15436
15984
 
15437
15985
  // src/commands/events.ts
15438
15986
  var import_node_fs14 = __toESM(require("node:fs"));
15987
+ var maxWindowMs = 31 * 864e5;
15988
+ function correctedWindowStep(opts, nowMs) {
15989
+ const fromMs = Date.parse(opts.from);
15990
+ const endMs = Math.min(fromMs + maxWindowMs, nowMs);
15991
+ if (endMs <= fromMs) {
15992
+ return {
15993
+ command: "Choose a --from start before now.",
15994
+ why: "No valid export window exists from this start; --to must be after --from and at or before now."
15995
+ };
15996
+ }
15997
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
15998
+ const args = [
15999
+ "apex events export",
16000
+ `--from ${quote(opts.from)}`,
16001
+ `--to ${quote(new Date(endMs).toISOString())}`,
16002
+ `--out ${quote(opts.out ?? "events.ndjson")}`
16003
+ ];
16004
+ if (opts.experiment !== void 0) args.push(`--experiment ${quote(opts.experiment)}`);
16005
+ if (opts.eventType !== void 0) args.push(`--event-type ${quote(opts.eventType)}`);
16006
+ args.push(`--qa ${quote(opts.qa)}`, `--limit ${quote(opts.limit)}`);
16007
+ if (opts.jsonSummary) args.push("--json-summary");
16008
+ return {
16009
+ command: args.join(" "),
16010
+ 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."
16011
+ };
16012
+ }
15439
16013
  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) => {
16014
+ eventsCommand.command("export").description(
16015
+ "Export every raw event in a window of at most 31 days that does not end in the future"
16016
+ ).requiredOption(
16017
+ "--from <iso>",
16018
+ "Inclusive ISO 8601 window start; the window spans at most 31 days"
16019
+ ).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
16020
  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);
16021
+ fail(command, "--from and --to must be ISO 8601 timestamps", EXIT_CODES.validation, {
16022
+ code: "invalid_window",
16023
+ reason: "invalid_timestamp"
16024
+ });
16025
+ }
16026
+ const fromMs = Date.parse(opts.from);
16027
+ const toMs = Date.parse(opts.to);
16028
+ if (toMs <= fromMs)
16029
+ fail(command, "--to must be after --from.", EXIT_CODES.validation, {
16030
+ code: "invalid_window",
16031
+ reason: "to_not_after_from"
16032
+ });
16033
+ const now = /* @__PURE__ */ new Date();
16034
+ if (toMs - fromMs > maxWindowMs) {
16035
+ const requestedDays = ((toMs - fromMs) / 864e5).toFixed(2);
16036
+ fail(
16037
+ command,
16038
+ `The export window is ${requestedDays} days; the maximum is 31. Export in windows of at most 31 days.`,
16039
+ EXIT_CODES.validation,
16040
+ {
16041
+ code: "invalid_window",
16042
+ reason: "window_too_long",
16043
+ max_days: 31,
16044
+ requested_days: requestedDays,
16045
+ next_steps: [correctedWindowStep(opts, now.getTime())]
16046
+ }
16047
+ );
16048
+ }
16049
+ if (toMs > now.getTime()) {
16050
+ const nowIso = now.toISOString();
16051
+ fail(
16052
+ command,
16053
+ `--to is in the future; use a time at or before now (${nowIso}).`,
16054
+ EXIT_CODES.validation,
16055
+ {
16056
+ code: "invalid_window",
16057
+ reason: "to_in_future",
16058
+ now: nowIso,
16059
+ next_steps: [correctedWindowStep(opts, now.getTime())]
16060
+ }
16061
+ );
15443
16062
  }
15444
16063
  if (!["exclude", "include", "only"].includes(opts.qa)) {
15445
16064
  fail(command, "--qa must be exclude, include, or only", EXIT_CODES.validation);
@@ -15479,129 +16098,150 @@ eventsCommand.command("export").description("Export every raw event in an absolu
15479
16098
  output2.once("error", reject);
15480
16099
  });
15481
16100
  } catch (error) {
16101
+ const exists = error?.code === "EEXIST";
15482
16102
  fail(
15483
16103
  command,
15484
- `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
15485
- EXIT_CODES.general,
15486
- { code: "local_file_error" }
16104
+ 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)}`,
16105
+ exists ? EXIT_CODES.validation : EXIT_CODES.general,
16106
+ { code: exists ? "output_exists" : "local_file_error", path: opts.out }
15487
16107
  );
15488
16108
  }
15489
16109
  }
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"
15537
- }
15538
- );
15539
- const page = response.data.data;
15540
- totals ??= page.totals;
15541
- for (const event of page.events) {
15542
- try {
15543
- await writeLine(`${JSON.stringify(event)}
15544
- `);
15545
- } catch (error) {
16110
+ try {
16111
+ const writeLine = async (line) => {
16112
+ const stream = output2 ?? process.stdout;
16113
+ const accepted = output2 ? output2.write(line) : writeRawCommandOutput(line);
16114
+ if (accepted) return;
16115
+ await new Promise((resolve2, reject) => {
16116
+ const onDrain = () => {
16117
+ stream.off("error", onError);
16118
+ resolve2();
16119
+ };
16120
+ const onError = (error) => {
16121
+ stream.off("drain", onDrain);
16122
+ reject(error);
16123
+ };
16124
+ stream.once("drain", onDrain);
16125
+ stream.once("error", onError);
16126
+ });
16127
+ };
16128
+ let cursor;
16129
+ let written = 0;
16130
+ let pages = 0;
16131
+ let totals;
16132
+ do {
16133
+ const response = await exportEventsPage(requireCredentials(), {
16134
+ from: opts.from,
16135
+ to: opts.to,
16136
+ experiment: opts.experiment,
16137
+ eventTypes: opts.eventType?.split(",").map((value) => value.trim()).filter(Boolean),
16138
+ qa: opts.qa,
16139
+ limit,
16140
+ cursor
16141
+ });
16142
+ if (!response.ok || !response.data) {
15546
16143
  fail(
15547
16144
  command,
15548
- `Cannot write ${opts.out ?? "stdout"}: ${error instanceof Error ? error.message : String(error)}`,
15549
- EXIT_CODES.general,
15550
- { code: "local_file_error" }
16145
+ response.error ?? "Event export page failed",
16146
+ classifyStatus(response.status),
16147
+ response.errorDetails
15551
16148
  );
15552
16149
  }
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
- );
16150
+ if (outputError)
16151
+ fail(
16152
+ command,
16153
+ `Cannot write ${opts.out}: ${outputError.message}`,
16154
+ EXIT_CODES.general,
16155
+ {
16156
+ code: "local_file_error"
16157
+ }
16158
+ );
16159
+ const page = response.data.data;
16160
+ totals ??= page.totals;
16161
+ for (const event of page.events) {
16162
+ try {
16163
+ await writeLine(`${JSON.stringify(event)}
16164
+ `);
16165
+ } catch (error) {
16166
+ fail(
16167
+ command,
16168
+ `Cannot write ${opts.out ?? "stdout"}: ${error instanceof Error ? error.message : String(error)}`,
16169
+ EXIT_CODES.general,
16170
+ { code: "local_file_error" }
16171
+ );
16172
+ }
16173
+ written += 1;
16174
+ }
16175
+ pages += 1;
16176
+ cursor = page.next_cursor ?? void 0;
16177
+ } while (cursor);
16178
+ if (output2) {
16179
+ try {
16180
+ await new Promise((resolve2, reject) => {
16181
+ if (outputError) return reject(outputError);
16182
+ output2.once("error", reject);
16183
+ output2.end(resolve2);
16184
+ });
16185
+ } catch (error) {
16186
+ fail(
16187
+ command,
16188
+ `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
16189
+ EXIT_CODES.general,
16190
+ { code: "local_file_error" }
16191
+ );
16192
+ }
16193
+ if (outputError)
16194
+ fail(
16195
+ command,
16196
+ `Cannot write ${opts.out}: ${outputError.message}`,
16197
+ EXIT_CODES.general,
16198
+ {
16199
+ code: "local_file_error"
16200
+ }
16201
+ );
15572
16202
  }
15573
- if (outputError)
16203
+ const matches = totals ? written === totals.events : false;
16204
+ const summary = {
16205
+ events_written: written,
16206
+ pages,
16207
+ totals: totals ?? null,
16208
+ count_matches: matches
16209
+ };
16210
+ if (!matches)
15574
16211
  fail(
15575
16212
  command,
15576
- `Cannot write ${opts.out}: ${outputError.message}`,
16213
+ "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
16214
  EXIT_CODES.general,
15578
16215
  {
15579
- code: "local_file_error"
16216
+ reason: "export_count_mismatch",
16217
+ ...summary
15580
16218
  }
15581
16219
  );
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}.
16220
+ if (summaryOnStdout) writeEnvelope(summary, "apex events export");
16221
+ process.stderr.write(
16222
+ `Exported ${written} events across ${pages} page(s); total ${totals?.events ?? "unknown"}; count matches: ${matches}.
15603
16223
  `
15604
- );
16224
+ );
16225
+ } catch (error) {
16226
+ if (output2 && opts.out) {
16227
+ output2.destroy();
16228
+ import_node_fs14.default.rmSync(opts.out, { force: true });
16229
+ const suffix = `The partial file ${opts.out} was removed.`;
16230
+ if (error instanceof CliError)
16231
+ throw new CliError(
16232
+ `${error.message} ${suffix}`,
16233
+ error.exitCode,
16234
+ { ...error.details, partial_file_removed: true },
16235
+ error.api
16236
+ );
16237
+ throw new CliError(
16238
+ `${error instanceof Error ? error.message : String(error)} ${suffix}`,
16239
+ EXIT_CODES.general,
16240
+ { code: "local_file_error", partial_file_removed: true }
16241
+ );
16242
+ }
16243
+ throw error;
16244
+ }
15605
16245
  });
15606
16246
 
15607
16247
  // src/commands/goals.ts
@@ -15652,6 +16292,10 @@ function sdkPageviewGoalMatch(urlPattern, href) {
15652
16292
  var INVENTORY_PAGE_SIZE = 100;
15653
16293
  var GOALS_PAGE_SIZE = 100;
15654
16294
  var MAX_GOAL_PAGES = 100;
16295
+ var MAX_PAGE_LOOKUP_PAGES = 1e3;
16296
+ 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;
16297
+ 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)";
16298
+ var PAGE_NEXT_STEPS = [{ command: "apex pages list", why: "Find the page ID or key." }];
15655
16299
  var NON_QUANTIFIABLE = /* @__PURE__ */ new Set(["(", "|", "^", "$"]);
15656
16300
  function urlPatternDependence(pattern) {
15657
16301
  let fragment = false;
@@ -15794,6 +16438,58 @@ function parseCapture(command, value) {
15794
16438
  function cleanBody(input) {
15795
16439
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
15796
16440
  }
16441
+ async function resolveGoalPageId(command, creds, value) {
16442
+ if (UUID_V1_TO_V8.test(value)) return value;
16443
+ let cursor;
16444
+ for (let page = 0; page < MAX_PAGE_LOOKUP_PAGES; page += 1) {
16445
+ const response = await requestTargetingResource(
16446
+ creds,
16447
+ "GET",
16448
+ "pages",
16449
+ void 0,
16450
+ void 0,
16451
+ 100,
16452
+ cursor
16453
+ );
16454
+ if (!response.ok || !response.data) {
16455
+ if (response.status === 403) {
16456
+ fail(
16457
+ command,
16458
+ "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).",
16459
+ EXIT_CODES.auth,
16460
+ {
16461
+ code: "page_lookup_forbidden",
16462
+ next_steps: [
16463
+ {
16464
+ command: "apex pages list",
16465
+ why: "Use a key with pages:read to find the page ID, or get it from the dashboard; pass --page <page ID> instead."
16466
+ }
16467
+ ]
16468
+ }
16469
+ );
16470
+ }
16471
+ fail(
16472
+ command,
16473
+ response.error ?? "Failed to list pages",
16474
+ classifyStatus(response.status),
16475
+ response.errorDetails
16476
+ );
16477
+ }
16478
+ if (!("data" in response.data) || !Array.isArray(response.data.data)) break;
16479
+ const pageData = response.data;
16480
+ const match2 = pageData.data.find((item) => item.key === value);
16481
+ if (match2) return match2.id;
16482
+ const next = pageData.pagination?.next_cursor ?? null;
16483
+ if (!pageData.pagination?.has_more || !next || next === cursor) break;
16484
+ cursor = next;
16485
+ }
16486
+ fail(
16487
+ command,
16488
+ `No active page with key ${value} in this shop; list pages with apex pages list`,
16489
+ EXIT_CODES.validation,
16490
+ { code: "page_not_found", next_steps: PAGE_NEXT_STEPS }
16491
+ );
16492
+ }
15797
16493
  function printGoal(goal) {
15798
16494
  console.log();
15799
16495
  console.log(source_default.bold(goal.name));
@@ -15806,6 +16502,7 @@ function printGoal(goal) {
15806
16502
  if (goal.text_match) console.log(` Text match: ${goal.text_match}`);
15807
16503
  if (goal.url_pattern) console.log(` URL: ${goal.url_pattern}`);
15808
16504
  if (goal.event_name) console.log(` Event: ${goal.event_name}`);
16505
+ if (goal.page_id) console.log(` Page: ${goal.page_id}`);
15809
16506
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
15810
16507
  console.log();
15811
16508
  }
@@ -15833,8 +16530,22 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
15833
16530
  );
15834
16531
  });
15835
16532
  });
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) {
16533
+ 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
16534
  const creds = requireCredentials();
16535
+ if (opts.type === "click" && !opts.page) {
16536
+ fail(
16537
+ this,
16538
+ "Click goals need a page: pass --page <page id or key> (list pages with apex pages list)",
16539
+ EXIT_CODES.validation,
16540
+ { code: "page_required", next_steps: PAGE_NEXT_STEPS }
16541
+ );
16542
+ }
16543
+ if ((opts.type === "custom" || opts.type === "revenue") && opts.page) {
16544
+ fail(this, `--page is not supported for ${opts.type} goals`, EXIT_CODES.validation, {
16545
+ code: "page_not_supported"
16546
+ });
16547
+ }
16548
+ const pageId = typeof opts.page === "string" ? await resolveGoalPageId(this, creds, opts.page) : void 0;
15838
16549
  const body5 = cleanBody({
15839
16550
  name: opts.name,
15840
16551
  type: opts.type,
@@ -15845,6 +16556,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
15845
16556
  text_match: opts.textMatch,
15846
16557
  url_pattern: opts.urlPattern,
15847
16558
  event_name: opts.eventName,
16559
+ page_id: pageId,
15848
16560
  count_once: opts.countOnce === true,
15849
16561
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
15850
16562
  });
@@ -15862,8 +16574,9 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
15862
16574
  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
16575
  "--value-property <property>",
15864
16576
  "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) {
16577
+ ).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
16578
  const creds = requireCredentials();
16579
+ const pageId = opts.page === "null" ? null : typeof opts.page === "string" ? await resolveGoalPageId(this, creds, opts.page) : void 0;
15867
16580
  const body5 = cleanBody({
15868
16581
  name: opts.name,
15869
16582
  type: opts.type,
@@ -15874,6 +16587,7 @@ var update2 = new Command("update").description("Update a goal").argument("<id>"
15874
16587
  text_match: opts.textMatch,
15875
16588
  url_pattern: opts.urlPattern,
15876
16589
  event_name: opts.eventName,
16590
+ page_id: pageId,
15877
16591
  count_once: opts.countOnce,
15878
16592
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
15879
16593
  });
@@ -17518,13 +18232,13 @@ var NAMED_HTML_ENTITIES = {
17518
18232
  nbsp: "\xA0"
17519
18233
  };
17520
18234
  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);
18235
+ return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z]+);/g, (match2, entity2) => {
18236
+ if (entity2.startsWith("#")) {
18237
+ const hex = entity2[1] === "x" || entity2[1] === "X";
18238
+ const code = Number.parseInt(hex ? entity2.slice(2) : entity2.slice(1), hex ? 16 : 10);
17525
18239
  return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : match2;
17526
18240
  }
17527
- return NAMED_HTML_ENTITIES[entity.toLowerCase()] ?? match2;
18241
+ return NAMED_HTML_ENTITIES[entity2.toLowerCase()] ?? match2;
17528
18242
  });
17529
18243
  }
17530
18244
  function printSettings(settings2) {
@@ -18294,9 +19008,6 @@ function resolveWorkerBase(options) {
18294
19008
  const raw = options.worker ?? process.env.DRIP_WORKER_URL ?? PRODUCTION_WORKER_BASE_URL;
18295
19009
  return raw.trim().replace(/\/+$/, "");
18296
19010
  }
18297
- function resolveShopId(options) {
18298
- return (options.shopId ?? process.env.DRIP_SHOP_ID ?? "").trim() || null;
18299
- }
18300
19011
  function servedConfigUrl(workerBase, shopId) {
18301
19012
  return `${workerBase}/config?shopId=${encodeURIComponent(shopId)}&env=${PRODUCTION_ENVIRONMENT_KEY}`;
18302
19013
  }
@@ -18469,7 +19180,8 @@ async function readGoals(creds) {
18469
19180
  return { goals };
18470
19181
  }
18471
19182
  async function runDoctor(options = {}) {
18472
- const creds = requireCredentials();
19183
+ const resolved = resolveCheckShop(options.shopId, process.env.DRIP_SHOP_ID);
19184
+ const { creds } = resolved;
18473
19185
  if (options.check === "purchase_capture") {
18474
19186
  const domainsRes2 = await getInstallDomains(creds);
18475
19187
  if (!domainsRes2.ok || !domainsRes2.data)
@@ -18479,7 +19191,9 @@ async function runDoctor(options = {}) {
18479
19191
  classifyStatus(domainsRes2.status),
18480
19192
  domainsRes2.errorDetails
18481
19193
  );
18482
- const shopId2 = resolveShopId(options) ?? domainsRes2.data.shop_id;
19194
+ if (resolved.shopId)
19195
+ assertResponseShop(domainsRes2.data.shop_id, resolved.shopId, creds.credentialKind);
19196
+ const shopId2 = resolved.shopId ?? domainsRes2.data.shop_id;
18483
19197
  const servedResult2 = shopId2 ? await fetchServedConfig(resolveWorkerBase(options), shopId2) : {};
18484
19198
  const readTracking = async (minutes) => {
18485
19199
  try {
@@ -18530,6 +19244,7 @@ async function runDoctor(options = {}) {
18530
19244
  apiFailureOf(res)
18531
19245
  );
18532
19246
  }
19247
+ assertResponseShop(domainsRes.data.shop_id, resolved.shopId, creds.credentialKind);
18533
19248
  for (const [label, res] of [
18534
19249
  ["url inventory", inventory],
18535
19250
  ["goals", goals]
@@ -18573,7 +19288,7 @@ async function runDoctor(options = {}) {
18573
19288
  withCaveat(devPollutionCheck(eventHosts, envsRes.data, unparsedRows), inventoryCaveat)
18574
19289
  );
18575
19290
  const primary = hostOnly(domainsRes.data.domain) ?? eventHosts[0];
18576
- const shopId = resolveShopId(options) ?? domainsRes.data.shop_id;
19291
+ const shopId = resolved.shopId ?? domainsRes.data.shop_id;
18577
19292
  const workerBase = resolveWorkerBase(options);
18578
19293
  const [page, servedResult] = await Promise.all([
18579
19294
  primary ? fetchPrimaryHtml(primary) : Promise.resolve({}),
@@ -18794,7 +19509,7 @@ function consentRateCheck(items) {
18794
19509
  }
18795
19510
  var doctorCommand = new Command("doctor").description("Check shop configuration against observed analytics").option(
18796
19511
  "--shop-id <uuid>",
18797
- "Override the shop ID discovered from the API (DRIP_SHOP_ID also overrides it)"
19512
+ "Apex shop ID to check; API requests use it too (DRIP_SHOP_ID is the fallback when no shop is selected)"
18798
19513
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
18799
19514
  "--worker <url>",
18800
19515
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
@@ -18803,7 +19518,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
18803
19518
  if (options.check && data.checks.length === 0)
18804
19519
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
18805
19520
  output(this, data, () => printDoctorReport(data.checks));
18806
- if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.general;
19521
+ if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.checkFailed;
18807
19522
  });
18808
19523
  function printDoctorReport(checks) {
18809
19524
  printTable(
@@ -18987,7 +19702,7 @@ Examples:
18987
19702
  );
18988
19703
 
18989
19704
  // src/commands/install.ts
18990
- var import_promises4 = require("node:dns/promises");
19705
+ var import_promises3 = require("node:dns/promises");
18991
19706
  var import_node_http2 = require("node:http");
18992
19707
  var import_node_https = require("node:https");
18993
19708
  var import_node_net = require("node:net");
@@ -19045,7 +19760,7 @@ var testResolver;
19045
19760
  var testFetch;
19046
19761
  async function resolveHost(hostname) {
19047
19762
  const mocked = testResolver?.(hostname) ?? null;
19048
- const addresses = mocked ?? await (0, import_promises4.lookup)(hostname, { all: true });
19763
+ const addresses = mocked ?? await (0, import_promises3.lookup)(hostname, { all: true });
19049
19764
  return addresses.map((entry) => entry.address);
19050
19765
  }
19051
19766
  async function validateUrl(raw, allowedHosts) {
@@ -19525,16 +20240,16 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
19525
20240
  };
19526
20241
  }
19527
20242
  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"
20243
+ "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
20244
  ).option("--url <url>", "Check one explicit bound storefront URL").option(
19530
20245
  "--shop-id <id>",
19531
- "Apex shop ID to verify (falls back to --shop/apex use, then DRIP_SHOP_ID)"
20246
+ "Apex shop ID to verify; API requests use it too (falls back to --shop/apex use, then DRIP_SHOP_ID)"
19532
20247
  ).option(
19533
20248
  "--worker-url <url>",
19534
20249
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
19535
20250
  ).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;
20251
+ const resolved = resolveCheckShop(opts.shopId, process.env.DRIP_SHOP_ID);
20252
+ const { creds, shopId, overriddenShopId } = resolved;
19538
20253
  if (!shopId)
19539
20254
  fail(
19540
20255
  this,
@@ -19542,6 +20257,10 @@ var installCommand = new Command("install").description("Check Apex storefront i
19542
20257
  EXIT_CODES.validation,
19543
20258
  { field: "shop-id" }
19544
20259
  );
20260
+ if (overriddenShopId)
20261
+ console.error(
20262
+ `Checking shop ${shopId} from --shop-id instead of the selected shop ${overriddenShopId}.`
20263
+ );
19545
20264
  const domains = await getInstallDomains(creds);
19546
20265
  if (!domains.ok || !domains.data)
19547
20266
  fail(
@@ -19550,6 +20269,7 @@ var installCommand = new Command("install").description("Check Apex storefront i
19550
20269
  classifyStatus(domains.status),
19551
20270
  domains.errorDetails
19552
20271
  );
20272
+ assertResponseShop(domains.data.shop_id, shopId, creds.credentialKind);
19553
20273
  const mockStoreOrigins = cliTestSetting("STORE_ORIGINS");
19554
20274
  const storeOrigins = cliTestSetting("API") === "1" && mockStoreOrigins ? mockStoreOrigins.split(",") : domains.data.storeOrigins;
19555
20275
  const origins = opts.url ? [opts.url] : storeOrigins;
@@ -19581,7 +20301,16 @@ var installCommand = new Command("install").description("Check Apex storefront i
19581
20301
  });
19582
20302
  continue;
19583
20303
  }
19584
- if (error instanceof UrlPolicyError) fail(this, error.message, EXIT_CODES.validation);
20304
+ if (error instanceof UrlPolicyError) {
20305
+ const boundHosts = [...allowedHosts].sort();
20306
+ const unbound = error.message.includes("host is not bound to this shop");
20307
+ fail(
20308
+ this,
20309
+ unbound ? `${error.message} (shop ${shopId} is bound to: ${boundHosts.join(", ")})` : error.message,
20310
+ EXIT_CODES.validation,
20311
+ unbound ? { code: "url_not_bound", field: "url", shop_id: shopId, bound_hosts: boundHosts } : { field: "url" }
20312
+ );
20313
+ }
19585
20314
  if (error instanceof BodyDecodeError) {
19586
20315
  pages.push({
19587
20316
  url,
@@ -19618,10 +20347,12 @@ var installCommand = new Command("install").description("Check Apex storefront i
19618
20347
  );
19619
20348
  let tracking;
19620
20349
  let trackingExitCode = null;
20350
+ let trackingApiFailure;
19621
20351
  try {
19622
20352
  const result = await verifyTracking(creds);
19623
20353
  if (!result.ok || !result.data) {
19624
20354
  trackingExitCode = classifyStatus(result.status);
20355
+ trackingApiFailure = apiFailureOf(result);
19625
20356
  tracking = {
19626
20357
  status: "FAIL",
19627
20358
  message: result.error ?? "Failed to verify tracking",
@@ -19645,6 +20376,11 @@ var installCommand = new Command("install").description("Check Apex storefront i
19645
20376
  if (servedConfig.status !== "PASS") warnings.push(servedConfig.message);
19646
20377
  if (tracking.status === "WARN") warnings.push(tracking.message);
19647
20378
  const failed = pages.some((page) => page.status === "FAIL") || servedConfig.status === "FAIL" || tracking.status === "FAIL";
20379
+ const failedChecks = [
20380
+ ...pages.filter((page) => page.status === "FAIL").map((page) => ({ check: "page", url: page.url, message: page.message })),
20381
+ ...servedConfig.status === "FAIL" ? [{ check: "served_config", url: servedConfig.url, message: servedConfig.message }] : [],
20382
+ ...tracking.status === "FAIL" ? [{ check: "tracking", message: tracking.message }] : []
20383
+ ];
19648
20384
  const data = { ok: !failed, pages, warnings, servedConfig, tracking };
19649
20385
  const renderReport = () => {
19650
20386
  printTable(
@@ -19676,12 +20412,24 @@ var installCommand = new Command("install").description("Check Apex storefront i
19676
20412
  };
19677
20413
  if (failed) {
19678
20414
  if (!shouldUseJson(this)) renderReport();
19679
- fail(
19680
- this,
19681
- "Apex install check found a definite failure",
19682
- trackingExitCode ?? EXIT_CODES.general,
19683
- data
19684
- );
20415
+ if (trackingExitCode !== null)
20416
+ fail(
20417
+ this,
20418
+ `Apex install check could not verify tracking: ${tracking.message}`,
20419
+ trackingExitCode,
20420
+ { ...data, shop_id: shopId, failed_checks: failedChecks },
20421
+ trackingApiFailure
20422
+ );
20423
+ const summaries = failedChecks.map((entry) => {
20424
+ const label = entry.check === "page" && "url" in entry ? `page ${entry.url}` : entry.check === "served_config" ? "served config" : "tracking";
20425
+ return `${label}: ${entry.message.replace(/\.$/, "")}`;
20426
+ });
20427
+ fail(this, `Apex install check failed: ${summaries.join("; ")}`, EXIT_CODES.checkFailed, {
20428
+ ...data,
20429
+ code: "install_check_failed",
20430
+ shop_id: shopId,
20431
+ failed_checks: failedChecks
20432
+ });
19685
20433
  }
19686
20434
  output(this, data, renderReport);
19687
20435
  }).parent;
@@ -20332,6 +21080,258 @@ var personalizationsCommand = new Command("personalizations").description("Manag
20332
21080
  init_source();
20333
21081
  var import_node_fs23 = __toESM(require("node:fs"));
20334
21082
  var import_node_path20 = __toESM(require("node:path"));
21083
+
21084
+ // src/generated/exclusion-group-body.schema.json
21085
+ var exclusion_group_body_schema_default = {
21086
+ create: {
21087
+ type: "object",
21088
+ additionalProperties: false,
21089
+ properties: {
21090
+ name: {
21091
+ type: "string",
21092
+ minLength: 1,
21093
+ maxLength: 120,
21094
+ description: "Group name, 1 to 120 characters."
21095
+ },
21096
+ description: {
21097
+ type: ["string", "null"]
21098
+ },
21099
+ members: {
21100
+ type: "array",
21101
+ minItems: 2,
21102
+ 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.",
21103
+ items: {
21104
+ oneOf: [
21105
+ {
21106
+ type: "object",
21107
+ additionalProperties: false,
21108
+ properties: {
21109
+ experiment_id: {
21110
+ type: "string",
21111
+ format: "uuid",
21112
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21113
+ },
21114
+ traffic_share: {
21115
+ type: "number",
21116
+ minimum: 0,
21117
+ maximum: 1
21118
+ }
21119
+ },
21120
+ required: ["experiment_id", "traffic_share"]
21121
+ },
21122
+ {
21123
+ type: "object",
21124
+ additionalProperties: false,
21125
+ properties: {
21126
+ experimentId: {
21127
+ type: "string",
21128
+ format: "uuid",
21129
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21130
+ },
21131
+ share: {
21132
+ type: "number",
21133
+ minimum: 0,
21134
+ maximum: 100
21135
+ }
21136
+ },
21137
+ required: ["experimentId", "share"]
21138
+ }
21139
+ ]
21140
+ }
21141
+ }
21142
+ },
21143
+ required: ["name", "members"]
21144
+ },
21145
+ update: {
21146
+ type: "object",
21147
+ additionalProperties: false,
21148
+ properties: {
21149
+ name: {
21150
+ type: "string",
21151
+ minLength: 1,
21152
+ maxLength: 120,
21153
+ description: "Group name, 1 to 120 characters."
21154
+ },
21155
+ description: {
21156
+ type: ["string", "null"]
21157
+ },
21158
+ members: {
21159
+ type: "array",
21160
+ minItems: 2,
21161
+ 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.",
21162
+ items: {
21163
+ oneOf: [
21164
+ {
21165
+ type: "object",
21166
+ additionalProperties: false,
21167
+ properties: {
21168
+ experiment_id: {
21169
+ type: "string",
21170
+ format: "uuid",
21171
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21172
+ },
21173
+ traffic_share: {
21174
+ type: "number",
21175
+ minimum: 0,
21176
+ maximum: 1
21177
+ }
21178
+ },
21179
+ required: ["experiment_id", "traffic_share"]
21180
+ },
21181
+ {
21182
+ type: "object",
21183
+ additionalProperties: false,
21184
+ properties: {
21185
+ experimentId: {
21186
+ type: "string",
21187
+ format: "uuid",
21188
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21189
+ },
21190
+ share: {
21191
+ type: "number",
21192
+ minimum: 0,
21193
+ maximum: 100
21194
+ }
21195
+ },
21196
+ required: ["experimentId", "share"]
21197
+ }
21198
+ ]
21199
+ }
21200
+ }
21201
+ }
21202
+ },
21203
+ "replace-members": {
21204
+ type: "object",
21205
+ additionalProperties: false,
21206
+ properties: {
21207
+ members: {
21208
+ type: "array",
21209
+ minItems: 2,
21210
+ 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.",
21211
+ items: {
21212
+ oneOf: [
21213
+ {
21214
+ type: "object",
21215
+ additionalProperties: false,
21216
+ properties: {
21217
+ experiment_id: {
21218
+ type: "string",
21219
+ format: "uuid",
21220
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21221
+ },
21222
+ traffic_share: {
21223
+ type: "number",
21224
+ minimum: 0,
21225
+ maximum: 1
21226
+ }
21227
+ },
21228
+ required: ["experiment_id", "traffic_share"]
21229
+ },
21230
+ {
21231
+ type: "object",
21232
+ additionalProperties: false,
21233
+ properties: {
21234
+ experimentId: {
21235
+ type: "string",
21236
+ format: "uuid",
21237
+ description: "Internal experiment UUID. Public ids and test keys are not resolved here."
21238
+ },
21239
+ share: {
21240
+ type: "number",
21241
+ minimum: 0,
21242
+ maximum: 100
21243
+ }
21244
+ },
21245
+ required: ["experimentId", "share"]
21246
+ }
21247
+ ]
21248
+ }
21249
+ }
21250
+ },
21251
+ required: ["members"]
21252
+ }
21253
+ };
21254
+
21255
+ // src/commands/schema.ts
21256
+ var commandBodySchemas = /* @__PURE__ */ new WeakMap();
21257
+ function setCommandBodySchema(command, schema) {
21258
+ commandBodySchemas.set(command, schema);
21259
+ return command;
21260
+ }
21261
+ function optionSchema(option) {
21262
+ return {
21263
+ flags: option.flags,
21264
+ ...option.argChoices ? { choices: option.argChoices } : {},
21265
+ description: option.description,
21266
+ required: option.required,
21267
+ optional: option.optional,
21268
+ mandatory: Boolean(option.mandatory),
21269
+ variadic: option.variadic,
21270
+ ...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
21271
+ };
21272
+ }
21273
+ function commandSchema(command) {
21274
+ return {
21275
+ name: command.name(),
21276
+ aliases: command.aliases(),
21277
+ description: command.description(),
21278
+ usage: command.usage(),
21279
+ arguments: command.registeredArguments.map((arg) => ({
21280
+ name: arg.name(),
21281
+ required: arg.required,
21282
+ variadic: arg.variadic,
21283
+ description: arg.description,
21284
+ ...arg.defaultValue === void 0 ? {} : { defaultValue: arg.defaultValue }
21285
+ })),
21286
+ options: command.options.map(optionSchema),
21287
+ ...commandBodySchemas.has(command) ? { body: commandBodySchemas.get(command) } : {},
21288
+ commands: command.commands.map(commandSchema)
21289
+ };
21290
+ }
21291
+ function createSchemaCommand(program3) {
21292
+ return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
21293
+ const schema = {
21294
+ schemaVersion: 2,
21295
+ contract: {
21296
+ optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
21297
+ mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
21298
+ requiredArguments: "An argument's required field means the positional is mandatory.",
21299
+ body: "A command's body field is the JSON Schema of the JSON document it sends."
21300
+ },
21301
+ ...commandSchema(program3),
21302
+ files: experiment_filesystem_schema_default
21303
+ };
21304
+ output(this, schema, () => {
21305
+ process.stdout.write(`${JSON.stringify(schema, null, 2)}
21306
+ `);
21307
+ });
21308
+ });
21309
+ }
21310
+
21311
+ // src/commands/targeting-resources.ts
21312
+ var exclusionHelp = `Body shape: {"name":"...","members":[...]} (update fields are optional)
21313
+ 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.
21314
+ Find experiment ids with: apex experiments list --fields id,name
21315
+ 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}]}'
21316
+ Full JSON Schema: apex schema --json (this command's body key)`;
21317
+ function withBodyHelp(command, action78) {
21318
+ setCommandBodySchema(command, exclusion_group_body_schema_default[action78]);
21319
+ command.addHelpText("after", `
21320
+ ${exclusionHelp}
21321
+ `);
21322
+ }
21323
+ function exclusionFailureDetails(kind5, action78, status4, details) {
21324
+ if (kind5 !== "exclusion-groups" || status4 !== 400 && status4 !== 422) return details;
21325
+ return {
21326
+ ...details,
21327
+ next_steps: [
21328
+ {
21329
+ command: `apex exclusion-groups ${action78} --help`,
21330
+ why: "Shows the body shape, member rules and an example; error.details.issues names each failing field."
21331
+ }
21332
+ ]
21333
+ };
21334
+ }
20335
21335
  function body3(command, value) {
20336
21336
  if (!value) return {};
20337
21337
  const source = value.startsWith("@") ? value.slice(1) : value;
@@ -20409,7 +21409,10 @@ function targetingResourceCommand(kind5) {
20409
21409
  `${action78 === "create" ? "Create" : "Update"} a ${singular3}`
20410
21410
  );
20411
21411
  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) {
21412
+ command.argument(
21413
+ "[json]",
21414
+ kind5 === "exclusion-groups" ? "Request body as inline JSON, @file, or a file path (shape below)" : "Inline JSON, @file, or file path"
21415
+ ).option("--file <path>", "JSON body file").action(async function(...args) {
20413
21416
  const opts = this.opts();
20414
21417
  const id = action78 === "update" ? String(args[0]) : void 0;
20415
21418
  const value = opts.file ? `@${opts.file}` : String(args[action78 === "update" ? 1 : 0] ?? "") || void 0;
@@ -20428,10 +21431,11 @@ function targetingResourceCommand(kind5) {
20428
21431
  this,
20429
21432
  response.error ?? `Failed to ${action78}`,
20430
21433
  classifyStatus(response.status),
20431
- response.errorDetails
21434
+ exclusionFailureDetails(kind5, action78, response.status, response.errorDetails)
20432
21435
  );
20433
21436
  output(this, response.data, () => console.log(source_default.green(`${action78}d ${singular3}`)));
20434
21437
  });
21438
+ if (kind5 === "exclusion-groups") withBodyHelp(command, action78);
20435
21439
  root.addCommand(command);
20436
21440
  }
20437
21441
  const deleteCommand = new Command("delete").description(`Delete a ${singular3}`).argument("<id>");
@@ -20470,9 +21474,9 @@ function targetingResourceCommand(kind5) {
20470
21474
  output(this, response.data ?? null, () => console.log(source_default.green(`Deleted ${singular3}`)));
20471
21475
  })
20472
21476
  );
20473
- if (kind5 === "exclusion-groups")
21477
+ if (kind5 === "exclusion-groups") {
20474
21478
  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) {
21479
+ 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
21480
  const response = await requestTargetingResource(
20477
21481
  requireCredentials(),
20478
21482
  "PUT",
@@ -20485,11 +21489,18 @@ function targetingResourceCommand(kind5) {
20485
21489
  this,
20486
21490
  response.error ?? "Failed to replace members",
20487
21491
  classifyStatus(response.status),
20488
- response.errorDetails
21492
+ exclusionFailureDetails(
21493
+ kind5,
21494
+ "replace-members",
21495
+ response.status,
21496
+ response.errorDetails
21497
+ )
20489
21498
  );
20490
21499
  output(this, response.data, () => console.log(source_default.green("Replaced members")));
20491
21500
  })
20492
21501
  );
21502
+ withBodyHelp(root.commands.at(-1), "replace-members");
21503
+ }
20493
21504
  return root;
20494
21505
  }
20495
21506
  var pagesCommand = targetingResourceCommand("pages");
@@ -20578,7 +21589,7 @@ var test2 = new Command("test").description("Evaluate experiment URL targeting f
20578
21589
  var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
20579
21590
 
20580
21591
  // src/commands/sos.ts
20581
- var import_node_crypto7 = require("node:crypto");
21592
+ var import_node_crypto8 = require("node:crypto");
20582
21593
  init_source();
20583
21594
  var import_node_fs24 = __toESM(require("node:fs"));
20584
21595
  var import_node_path21 = __toESM(require("node:path"));
@@ -20689,7 +21700,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
20689
21700
  const bytes = file.bytes;
20690
21701
  await uploadSosAttachment(grant.signed_url, file.content_type, bytes);
20691
21702
  attachmentPaths.push(grant.path);
20692
- attachmentDigests.push((0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex"));
21703
+ attachmentDigests.push((0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex"));
20693
21704
  }
20694
21705
  }
20695
21706
  const description = readText(opts.description) ?? descriptionArg ?? `Apex CLI ${opts.type} SOS request${opts.loom ? ` with Loom evidence: ${opts.loom}` : "."}`;
@@ -21002,55 +22013,6 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
21002
22013
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
21003
22014
  ).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
22015
 
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
22016
  // src/lib/gtm-miner.ts
21055
22017
  function extractContainerReferences(html) {
21056
22018
  const results2 = /* @__PURE__ */ new Map();
@@ -22085,7 +23047,7 @@ async function confirm(c, force, label, retry) {
22085
23047
  }
22086
23048
  return true;
22087
23049
  }
22088
- var orgCommand = new Command("org").description("Manage the current organization");
23050
+ var orgCommand = new Command("org").alias("orgs").description("Manage the current organization");
22089
23051
  orgCommand.command("get").action(async function() {
22090
23052
  await run2(this, getOrg(requireUserCredentials()), "get organization");
22091
23053
  });
@@ -22188,23 +23150,38 @@ orgCommand.command("remove-member").argument("<memberId>").option("-f, --force")
22188
23150
  return;
22189
23151
  await run2(this, removeOrgMember(requireUserCredentials(), id), "remove member");
22190
23152
  });
22191
- var teamCommand = new Command("team").description("Manage a shop team");
22192
- teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
23153
+ var teamCommand = new Command("team").alias("teams").description("Manage a shop team");
23154
+ var teamShopDescription = "Shop ID; defaults to the selected shop (--shop before the subcommand, DRIP_SHOP, or apex use)";
23155
+ function teamShop(command, explicit) {
23156
+ if (explicit) return explicit;
23157
+ const selected2 = requireUserCredentials().resolvedContext?.shopId;
23158
+ if (selected2) return selected2;
23159
+ fail(
23160
+ command,
23161
+ "apex team needs a shop. Pass --shop <id>, or select one with apex use <org>/<shop>.",
23162
+ 3,
23163
+ { code: "shop_context_required", retry: "apex use <org>/<shop>", flag: "--shop" }
23164
+ );
23165
+ }
23166
+ teamCommand.command("members").option("--shop <id>", teamShopDescription).action(async function(o) {
23167
+ o.shop = teamShop(this, o.shop);
22193
23168
  await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
22194
23169
  });
22195
- teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
23170
+ teamCommand.command("invite").argument("<email>").option("--shop <id>", teamShopDescription).addOption(
22196
23171
  new Option(
22197
23172
  "--role <role>",
22198
23173
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
22199
23174
  ).choices(["editor", "client_viewer"]).makeOptionMandatory()
22200
23175
  ).action(async function(email, o) {
23176
+ o.shop = teamShop(this, o.shop);
22201
23177
  await run2(
22202
23178
  this,
22203
23179
  inviteTeam(requireUserCredentials(), email, o.shop, o.role),
22204
23180
  "invite team member"
22205
23181
  );
22206
23182
  });
22207
- teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id>").action(async function(id, o) {
23183
+ teamCommand.command("revoke-invite").argument("<id>").option("--shop <id>", teamShopDescription).action(async function(id, o) {
23184
+ o.shop = teamShop(this, o.shop);
22208
23185
  await run2(
22209
23186
  this,
22210
23187
  revokeTeamInvite(requireUserCredentials(), id, o.shop),
@@ -22216,10 +23193,12 @@ teamCommand.command("set-role").argument("<memberId>").addArgument(
22216
23193
  "<role>",
22217
23194
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
22218
23195
  ).choices(["editor", "client_viewer"])
22219
- ).requiredOption("--shop <id>").action(async function(id, role, o) {
23196
+ ).option("--shop <id>", teamShopDescription).action(async function(id, role, o) {
23197
+ o.shop = teamShop(this, o.shop);
22220
23198
  await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
22221
23199
  });
22222
- teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
23200
+ teamCommand.command("remove").argument("<memberId>").option("--shop <id>", teamShopDescription).option("-f, --force").action(async function(id, o) {
23201
+ o.shop = teamShop(this, o.shop);
22223
23202
  if (!await confirm(
22224
23203
  this,
22225
23204
  o.force,
@@ -22230,6 +23209,38 @@ teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>
22230
23209
  await run2(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
22231
23210
  });
22232
23211
 
23212
+ // src/commands/upgrade.ts
23213
+ var import_node_child_process7 = require("node:child_process");
23214
+ 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) {
23215
+ if (options.dryRun) {
23216
+ output(
23217
+ this,
23218
+ { command: UPGRADE_COMMAND, ran: false, exit_code: null, previous_version: CLI_VERSION },
23219
+ () => console.log(UPGRADE_COMMAND)
23220
+ );
23221
+ return;
23222
+ }
23223
+ const npmBinary = process.env.APEX_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
23224
+ const json = shouldUseJson(this);
23225
+ const windows = process.platform === "win32";
23226
+ const command = windows ? `"${npmBinary}"` : npmBinary;
23227
+ const result = (0, import_node_child_process7.spawnSync)(command, ["install", "-g", "@drip-apex/apex-cli@latest"], {
23228
+ stdio: json ? ["inherit", 2, 2] : "inherit",
23229
+ shell: windows
23230
+ });
23231
+ if (result.error || result.status !== 0)
23232
+ fail(this, result.error?.message || "Apex CLI upgrade failed", EXIT_CODES.general, {
23233
+ code: "upgrade_failed",
23234
+ next_steps: [{ command: UPGRADE_COMMAND, why: "Run the install command manually." }]
23235
+ });
23236
+ clearUpdateCache();
23237
+ output(
23238
+ this,
23239
+ { command: UPGRADE_COMMAND, ran: true, exit_code: 0, previous_version: CLI_VERSION },
23240
+ () => console.log("Apex CLI upgraded.")
23241
+ );
23242
+ });
23243
+
22233
23244
  // src/commands/account.ts
22234
23245
  function userCredentials2() {
22235
23246
  return requireUserCredentials();
@@ -22835,7 +23846,7 @@ var import_node_fs29 = __toESM(require("node:fs"));
22835
23846
  var import_node_path26 = __toESM(require("node:path"));
22836
23847
 
22837
23848
  // src/commands/landing-pages.ts
22838
- var import_node_crypto8 = __toESM(require("node:crypto"));
23849
+ var import_node_crypto9 = __toESM(require("node:crypto"));
22839
23850
  var import_node_fs28 = __toESM(require("node:fs"));
22840
23851
  var import_node_path25 = __toESM(require("node:path"));
22841
23852
  function apiFailure(command, response, fallback2) {
@@ -22849,7 +23860,7 @@ function apiFailure(command, response, fallback2) {
22849
23860
  }
22850
23861
  }
22851
23862
  function sha256(body5) {
22852
- return import_node_crypto8.default.createHash("sha256").update(body5).digest("hex");
23863
+ return import_node_crypto9.default.createHash("sha256").update(body5).digest("hex");
22853
23864
  }
22854
23865
  function markerPath(folder) {
22855
23866
  return import_node_path25.default.join(folder, LANDING_PAGE_CHECKOUT_MARKER);
@@ -23598,7 +24609,10 @@ function addJsonOption(command) {
23598
24609
  if (!command.options.some((option) => option.long === "--shop"))
23599
24610
  command.option("--shop <id>", "Use a shop for this invocation");
23600
24611
  }
23601
- command.exitOverride((err) => interceptCliError(err, command));
24612
+ command.exitOverride((err) => {
24613
+ recordCommanderExit(err, command);
24614
+ interceptCliError(err, command);
24615
+ });
23602
24616
  for (const child of command.commands) addJsonOption(child);
23603
24617
  }
23604
24618
  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 +24668,9 @@ program2.addCommand(skillsCommand);
23654
24668
  program2.addCommand(adminCommand, { hidden: true });
23655
24669
  program2.addCommand(orgCommand);
23656
24670
  program2.addCommand(teamCommand);
24671
+ program2.addCommand(upgradeCommand);
23657
24672
  addJsonOption(program2);
24673
+ registerTelemetryProgram(program2);
23658
24674
  var optionTerminatorIndex = process.argv.indexOf("--");
23659
24675
  var optionArgv = optionTerminatorIndex === -1 ? process.argv : process.argv.slice(0, optionTerminatorIndex);
23660
24676
  if (optionArgv.includes("--json")) program2.setOptionValue("json", true);