@drip-apex/apex-cli 0.15.2 → 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.2",
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);
@@ -7589,7 +7695,8 @@ function classifyApiError(status4, details) {
7589
7695
  const code = details?.code;
7590
7696
  if (code === "shop_context_required" || code === "org_context_required")
7591
7697
  return EXIT_CODES.validation;
7592
- if (code === "shop_access_denied" || code === "org_access_denied") return EXIT_CODES.auth;
7698
+ if (code === "shop_access_denied" || code === "org_access_denied" || code === "member_suspended")
7699
+ return EXIT_CODES.auth;
7593
7700
  return classifyStatus(status4);
7594
7701
  }
7595
7702
  function confirmationRequired(command, message, retry) {
@@ -7599,6 +7706,7 @@ var liveSpinners = /* @__PURE__ */ new Set();
7599
7706
  var exitFallback;
7600
7707
  function drainAndExit(code) {
7601
7708
  for (const spinner of liveSpinners) spinner.stop();
7709
+ writePendingUpdateNotice();
7602
7710
  process.exitCode = code;
7603
7711
  if (exitFallback) clearTimeout(exitFallback);
7604
7712
  exitFallback = setTimeout(() => process.exit(code), 500);
@@ -7637,13 +7745,35 @@ async function createSpinner(message) {
7637
7745
  }
7638
7746
  return spinner.start();
7639
7747
  }
7640
- var updateNotice;
7641
7748
  async function maybeCheckForUpdate() {
7642
- updateNotice = await checkForUpdate(CLI_VERSION);
7643
- if (updateNotice)
7644
- 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}
7645
7765
  `);
7646
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
+ }
7647
7777
  async function handleCliError(err, command) {
7648
7778
  if (err && typeof err === "object" && "exitCode" in err && err.exitCode === 0) {
7649
7779
  flushFallbackEnvelope();
@@ -7675,6 +7805,9 @@ async function handleCliError(err, command) {
7675
7805
  process.stderr.write(`Retry: ${String(cliError.details.retry)}
7676
7806
  `);
7677
7807
  }
7808
+ writeUpdateNotice(
7809
+ currentUpdateState(CLI_VERSION, minimumFromError(cliError, errorReason(cliError)))
7810
+ );
7678
7811
  }
7679
7812
  await finishInvocation({ exitCode: cliError.exitCode, error: err });
7680
7813
  drainAndExit(cliError.exitCode);
@@ -7686,9 +7819,20 @@ function errorReason(error) {
7686
7819
  const body5 = error.api?.body;
7687
7820
  const code = body5?.code ?? body5?.error?.code ?? (error.api ? details?.code : void 0);
7688
7821
  if (typeof code === "string" && code) return code;
7689
- if (["shop_context_required", "local_file_error", "validation_failed"].includes(
7690
- String(details?.code)
7691
- ))
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)))
7692
7836
  return String(details?.code);
7693
7837
  const status4 = error.api?.status;
7694
7838
  if (status4 === 401) return "auth_required";
@@ -7704,6 +7848,29 @@ function errorReason(error) {
7704
7848
  function errorNextSteps(error) {
7705
7849
  const reason2 = errorReason(error);
7706
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
+ );
7859
+ if (reason2 === "member_suspended")
7860
+ return [
7861
+ {
7862
+ command: "contact organization admin",
7863
+ why: "Ask an organization owner or admin to reinstate your membership."
7864
+ }
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
+ }
7707
7874
  if (reason2 === "auth_required" || error.api?.status === 401) {
7708
7875
  steps.push({ command: "apex login", why: "Authenticate with a valid API key." });
7709
7876
  if (!error.api)
@@ -7743,8 +7910,10 @@ function errorNextSteps(error) {
7743
7910
  const hints = error.details?.next_steps;
7744
7911
  if (Array.isArray(hints))
7745
7912
  for (const hint of hints) {
7746
- if (hint && typeof hint.command === "string" && typeof hint.why === "string")
7747
- 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
+ }
7748
7917
  }
7749
7918
  if (reason2 === "validation_failed" && error.details?.plan)
7750
7919
  steps.push({
@@ -7861,7 +8030,7 @@ function clearCredentials() {
7861
8030
  } catch {
7862
8031
  }
7863
8032
  }
7864
- function requireCredentials() {
8033
+ function requireCredentials(fallbackShopId) {
7865
8034
  const creds = getCredentials();
7866
8035
  if (!creds) {
7867
8036
  throw new CliError(
@@ -7870,6 +8039,7 @@ function requireCredentials() {
7870
8039
  );
7871
8040
  }
7872
8041
  const context = resolveContext(creds, void 0, defaultCredentialKind(creds));
8042
+ if (!context.shopId) context.shopId = fallbackShopId?.trim() || void 0;
7873
8043
  if (defaultCredentialKind(creds) === "user") {
7874
8044
  setEnvelopeContext({
7875
8045
  credential: "user",
@@ -7909,6 +8079,53 @@ function requireCredentials() {
7909
8079
  });
7910
8080
  return { ...creds, credentialKind: "shop", resolvedContext: context };
7911
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
+ }
7912
8129
  function requireUserCredentials() {
7913
8130
  const creds = getCredentials();
7914
8131
  const environmentUserKey = creds?.credentialKind === "user" ? creds.apiKey : void 0;
@@ -7969,7 +8186,65 @@ var invocation;
7969
8186
  var firstError;
7970
8187
  var apiCalls = 0;
7971
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
+ }
7972
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;
7973
8248
  const names = /* @__PURE__ */ new Set();
7974
8249
  for (let current = command; current; current = current.parent) {
7975
8250
  for (const option of current.options) {
@@ -8022,7 +8297,9 @@ var staticRouteSegments = /* @__PURE__ */ new Set([
8022
8297
  "duplicate",
8023
8298
  "entry-exit",
8024
8299
  "environments",
8300
+ "events",
8025
8301
  "evidence",
8302
+ "export",
8026
8303
  "exclusion-groups",
8027
8304
  "experiments",
8028
8305
  "external-pixels",
@@ -8116,6 +8393,24 @@ function normalizeRoute(path26) {
8116
8393
  }
8117
8394
  function recordApiResponse2(response) {
8118
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
+ }
8119
8414
  if (response.status >= 400 && !firstError)
8120
8415
  firstError = { ...response, path: normalizeRoute(response.path) };
8121
8416
  }
@@ -8125,20 +8420,62 @@ function classifyError(exitCode, error, status4) {
8125
8420
  if (detail5.code === "commander.unknownCommand") return "unknown_command";
8126
8421
  if (detail5.code?.startsWith("commander.")) return "validation";
8127
8422
  if (exitCode === 4) return "confirmation_required";
8423
+ if (exitCode === EXIT_CODES.checkFailed) return "check_failed";
8128
8424
  if (status4 === 401 || status4 === 403 || exitCode === 2) return "auth";
8129
- if (status4 === 404) return "not_found";
8425
+ if (status4 === 404 || status4 === 410) return "not_found";
8130
8426
  if (status4 === 429) return "rate_limited";
8131
8427
  if (status4 === 409 || exitCode === 5) return "conflict";
8132
8428
  if (status4 && status4 >= 500) return "server";
8133
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";
8134
8432
  if (Boolean(
8135
- detail5.details?.reason && ["network", "dns", "timeout", "tls", "refused", "reset", "unknown"].includes(
8136
- detail5.details.reason
8137
- )
8138
- ) || 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")
8139
8435
  return "network";
8140
8436
  return "internal";
8141
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
+ }
8142
8479
  async function finishInvocation({
8143
8480
  exitCode: rawExitCode,
8144
8481
  error
@@ -8160,10 +8497,8 @@ async function finishInvocation({
8160
8497
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
8161
8498
  const commanderExit = code.startsWith("commander.");
8162
8499
  const parse3 = commanderExit && exitCode !== 0;
8163
- const token = commanderExit ? process.argv.slice(2).find((token2) => !token2.startsWith("-")) : void 0;
8164
- 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;
8165
- if (attempted === "login" || attempted === "logout") return;
8166
- 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);
8167
8502
  if (!command || command === "login" || command === "logout") return;
8168
8503
  if (!getClientIdentity()) setClientIdentity({ client: "apex-cli", version: CLI_VERSION });
8169
8504
  const payload = {
@@ -8177,8 +8512,12 @@ async function finishInvocation({
8177
8512
  exit_code: exitCode,
8178
8513
  error_class: classifyError(exitCode, error, firstError?.status),
8179
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) } : {},
8180
8519
  first_error_status: firstError?.status,
8181
- first_error_code: firstError?.errorCode,
8520
+ first_error_code: invocationErrorCode(exitCode, error, commanderExit),
8182
8521
  first_error_request_id: firstError?.requestId,
8183
8522
  first_error_route: firstError?.path,
8184
8523
  duration_ms: Math.max(0, Math.round(performance.now() - started))
@@ -8199,13 +8538,13 @@ init_source();
8199
8538
 
8200
8539
  // src/commands/login.ts
8201
8540
  init_source();
8202
- var import_node_crypto3 = __toESM(require("node:crypto"));
8541
+ var import_node_crypto4 = __toESM(require("node:crypto"));
8203
8542
  var import_node_http = __toESM(require("node:http"));
8204
8543
  var import_node_os6 = __toESM(require("node:os"));
8205
8544
  var import_node_readline2 = __toESM(require("node:readline"));
8206
8545
 
8207
8546
  // src/api.ts
8208
- var import_node_crypto2 = require("node:crypto");
8547
+ var import_node_crypto3 = require("node:crypto");
8209
8548
 
8210
8549
  // ../api-client/src/landing-pages.ts
8211
8550
  var LANDING_PAGE_MANIFEST_FILE = "apex-landing-page.json";
@@ -8370,7 +8709,11 @@ function sharedClient(creds) {
8370
8709
  let client = sharedClients.get(creds);
8371
8710
  if (!client) {
8372
8711
  client = createHttpClient(creds, {
8373
- onResponse: recordApiResponse2,
8712
+ onResponse: (observation) => {
8713
+ recordApiResponse2(observation);
8714
+ recordServerCliPolicy?.(observation);
8715
+ },
8716
+ observeNetworkErrors: true,
8374
8717
  transport: testTransport,
8375
8718
  testTransportVersion: () => testTransport ? process.env[["APEX", "CLI", "MOCK", "API", "VERSION"].join("_")] : void 0,
8376
8719
  onNetworkError: (info) => {
@@ -8420,8 +8763,14 @@ function sharedClient(creds) {
8420
8763
  }
8421
8764
  return client;
8422
8765
  }
8423
- async function request(creds, method, path26, body5, requestTimeoutMs) {
8424
- 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
+ );
8425
8774
  recordApiResponse(response);
8426
8775
  return response;
8427
8776
  }
@@ -8438,6 +8787,7 @@ function listExperiments(creds, opts) {
8438
8787
  if (opts?.includeArchived) params.set("archived", "1");
8439
8788
  if (opts?.view) params.set("view", opts.view);
8440
8789
  if (opts?.fields !== void 0) params.set("fields", opts.fields);
8790
+ if (opts?.url !== void 0) params.set("url", opts.url);
8441
8791
  const qs = params.toString();
8442
8792
  return request(creds, "GET", `/api/v1/experiments${qs ? `?${qs}` : ""}`);
8443
8793
  }
@@ -8485,6 +8835,8 @@ function getExperimentFilesystem(creds, id) {
8485
8835
  function getExperimentResults(creds, id, opts) {
8486
8836
  const params = new URLSearchParams();
8487
8837
  if (opts?.currency) params.set("currency", opts.currency);
8838
+ for (const key of ["segment", "exclude_segment"])
8839
+ if (opts?.[key] !== void 0) params.set(key, opts[key]);
8488
8840
  const qs = params.toString();
8489
8841
  return request(
8490
8842
  creds,
@@ -8523,6 +8875,7 @@ function planExperimentFilesystem(creds, id, body5) {
8523
8875
  body5
8524
8876
  );
8525
8877
  }
8878
+ var APPLY_TIMEOUT_MS = 9e4;
8526
8879
  function applyExperimentFilesystem(creds, id, body5) {
8527
8880
  return request(
8528
8881
  creds,
@@ -8532,12 +8885,25 @@ function applyExperimentFilesystem(creds, id, body5) {
8532
8885
  files: body5.files,
8533
8886
  ...body5.accept_qa_invalidation !== void 0 ? { accept_qa_invalidation: body5.accept_qa_invalidation } : {},
8534
8887
  ...body5.confirmTargetingReplace === true ? { confirmTargetingReplace: true } : {}
8535
- }
8888
+ },
8889
+ void 0,
8890
+ { minTimeoutMs: APPLY_TIMEOUT_MS }
8536
8891
  );
8537
8892
  }
8538
8893
  function getExperimentQaRun(creds, id) {
8539
8894
  return request(creds, "GET", `/api/v1/experiments/${id}/qa/run`);
8540
8895
  }
8896
+ function getExperimentQaEvents(creds, id, options = {}) {
8897
+ const params = new URLSearchParams();
8898
+ if (options.session) params.set("qa_session", options.session);
8899
+ if (options.minutes !== void 0) params.set("minutes", String(options.minutes));
8900
+ if (options.type) params.set("type", options.type);
8901
+ return request(
8902
+ creds,
8903
+ "GET",
8904
+ `/api/v1/experiments/${id}/qa/events${params.size ? `?${params}` : ""}`
8905
+ );
8906
+ }
8541
8907
  function updateExperimentQaRun(creds, id, body5) {
8542
8908
  return request(creds, "PATCH", `/api/v1/experiments/${id}/qa/run`, body5);
8543
8909
  }
@@ -8547,8 +8913,9 @@ function attachExperimentQaEvidence(creds, id, body5) {
8547
8913
  function uploadExperimentQaEvidence(creds, id, form) {
8548
8914
  return requestForm(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, form);
8549
8915
  }
8550
- function approveExperimentQa(creds, id) {
8551
- 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);
8552
8919
  }
8553
8920
  function reopenExperimentQa(creds, id) {
8554
8921
  return request(creds, "DELETE", `/api/v1/experiments/${id}/qa`);
@@ -8748,7 +9115,7 @@ function testTargeting(creds, url, opts) {
8748
9115
  return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
8749
9116
  }
8750
9117
  function createSos(creds, body5, options = {}) {
8751
- 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(
8752
9119
  [
8753
9120
  body5.type,
8754
9121
  body5.experiment_id ?? "",
@@ -8891,6 +9258,12 @@ function createAdminCustomer(creds, body5) {
8891
9258
  function setAdminCustomerCategory(creds, orgId, body5) {
8892
9259
  return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
8893
9260
  }
9261
+ function setAdminCustomerManagedBy(creds, orgId, body5) {
9262
+ return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
9263
+ }
9264
+ function setAdminCustomerWorkspaceType(creds, orgId, body5) {
9265
+ return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
9266
+ }
8894
9267
  function getAdminSettings(creds) {
8895
9268
  return request(userCredentials(creds), "GET", "/api/v1/admin/settings");
8896
9269
  }
@@ -9045,6 +9418,17 @@ function revokeOrgInvite(creds, id) {
9045
9418
  function setOrgMember(creds, id, body5) {
9046
9419
  return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, body5);
9047
9420
  }
9421
+ function suspendOrgMember(creds, id, reason2) {
9422
+ return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, {
9423
+ suspended: true,
9424
+ ...reason2 ? { reason: reason2 } : {}
9425
+ });
9426
+ }
9427
+ function reinstateOrgMember(creds, id) {
9428
+ return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, {
9429
+ suspended: false
9430
+ });
9431
+ }
9048
9432
  function removeOrgMember(creds, id) {
9049
9433
  return request(userCreds(creds), "DELETE", `/api/v1/org/members/${encodeURIComponent(id)}`);
9050
9434
  }
@@ -9188,6 +9572,14 @@ function getOrderJourney(creds, opts) {
9188
9572
  45e3
9189
9573
  );
9190
9574
  }
9575
+ function moveAdminCustomerStores(creds, orgId, body5) {
9576
+ return request(
9577
+ userCredentials(creds),
9578
+ "POST",
9579
+ `/api/v1/admin/customers/${orgId}/move-stores`,
9580
+ body5
9581
+ );
9582
+ }
9191
9583
 
9192
9584
  // src/context.ts
9193
9585
  var import_node_readline = __toESM(require("node:readline"));
@@ -9220,6 +9612,16 @@ function resolveSelection(tree, input) {
9220
9612
  const shop = shopValue ? match(shopValue, org.shops, "shop") : void 0;
9221
9613
  return { orgId: org.id, ...shop ? { shopId: shop.id } : {} };
9222
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
+ }
9223
9625
  function ask(question) {
9224
9626
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
9225
9627
  return new Promise(
@@ -9232,8 +9634,23 @@ function ask(question) {
9232
9634
  async function pickContext(tree) {
9233
9635
  if (!process.stdin.isTTY || !process.stdout.isTTY)
9234
9636
  throw new CliError(
9235
- "Interactive context selection requires a TTY. Pass an org/shop to apex use, or use --org/--shop.",
9236
- 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
+ }
9237
9654
  );
9238
9655
  let org = tree.orgs.length === 1 ? tree.orgs[0] : void 0;
9239
9656
  if (!org) {
@@ -9353,7 +9770,7 @@ function parseFormBody(body5) {
9353
9770
  return new URLSearchParams(body5);
9354
9771
  }
9355
9772
  async function waitForBrowserLogin(baseUrl, userMode = false) {
9356
- const state = import_node_crypto3.default.randomBytes(24).toString("base64url");
9773
+ const state = import_node_crypto4.default.randomBytes(24).toString("base64url");
9357
9774
  const clientName = import_node_os6.default.hostname() || "Local machine";
9358
9775
  return new Promise((resolve2, reject) => {
9359
9776
  let settled = false;
@@ -9399,10 +9816,14 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
9399
9816
  const returnedState = params.get("state");
9400
9817
  const apiKey = params.get("apiKey");
9401
9818
  const error = params.get("error");
9819
+ const code = params.get("code");
9402
9820
  if (error) {
9403
9821
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
9404
9822
  res.end("<h1>Apex CLI login failed</h1><p>You can close this window.</p>");
9405
- finish(new Error(error));
9823
+ const details = code ? { code } : void 0;
9824
+ finish(
9825
+ details ? new CliError(error, classifyApiError(400, details), details) : new Error(error)
9826
+ );
9406
9827
  return;
9407
9828
  }
9408
9829
  if (returnedState !== state) {
@@ -9486,6 +9907,7 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
9486
9907
  try {
9487
9908
  apiKey = await waitForBrowserLogin(baseUrl, true);
9488
9909
  } catch (err) {
9910
+ if (err instanceof CliError) throw err;
9489
9911
  fail(this, err instanceof Error ? err.message : "Browser login failed.");
9490
9912
  }
9491
9913
  }
@@ -9658,6 +10080,14 @@ Context: ${current.orgId ?? "none"}${current.shopId ? ` / ${current.shopId}` : "
9658
10080
 
9659
10081
  // src/commands/use.ts
9660
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
+ );
9661
10091
  const creds = requireUserCredentials();
9662
10092
  const response = await getAccountContext(creds);
9663
10093
  if (!response.ok || !response.data)
@@ -9667,9 +10097,32 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9667
10097
  classifyApiError(response.status, response.errorDetails),
9668
10098
  response.errorDetails
9669
10099
  );
9670
- 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);
9671
10101
  const stored = getStoredCredentials();
9672
- 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
+ }
9673
10126
  saveCredentials({ ...stored, context: selected2 });
9674
10127
  setEnvelopeContext({
9675
10128
  credential: "user",
@@ -9685,7 +10138,7 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9685
10138
 
9686
10139
  // src/commands/experiments.ts
9687
10140
  init_source();
9688
- var import_node_crypto5 = __toESM(require("node:crypto"));
10141
+ var import_node_crypto6 = __toESM(require("node:crypto"));
9689
10142
  var import_node_fs8 = __toESM(require("node:fs"));
9690
10143
  var import_node_path6 = __toESM(require("node:path"));
9691
10144
 
@@ -9777,13 +10230,18 @@ function buildQaPreviewUrl(experiment2, options = {}) {
9777
10230
  }
9778
10231
 
9779
10232
  // src/lib/qa-preview-url.ts
9780
- function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
9781
- const url = buildQaPreviewUrl({
9782
- id: experimentId,
9783
- store_url: storeUrl,
9784
- variations: [{ id: variationId, is_control: false }]
9785
- });
10233
+ function buildQaPreviewUrl2(storeUrl, experimentId, variationId, qaSessionId) {
10234
+ const url = buildQaPreviewUrl(
10235
+ {
10236
+ id: experimentId,
10237
+ store_url: storeUrl,
10238
+ variations: [{ id: variationId, is_control: false }]
10239
+ },
10240
+ { qaSessionId }
10241
+ );
9786
10242
  if (!url) throw new TypeError("Invalid QA preview URL");
10243
+ if (qaSessionId && url.searchParams.get("drip_qa_session") !== qaSessionId)
10244
+ url.searchParams.set("drip_qa_session", qaSessionId);
9787
10245
  return url;
9788
10246
  }
9789
10247
 
@@ -9995,7 +10453,7 @@ function deriveStoreUrl(patterns) {
9995
10453
  }
9996
10454
 
9997
10455
  // src/lib/asset-upload.ts
9998
- var import_node_crypto4 = __toESM(require("node:crypto"));
10456
+ var import_node_crypto5 = __toESM(require("node:crypto"));
9999
10457
  var import_node_fs7 = __toESM(require("node:fs"));
10000
10458
  var import_node_path5 = __toESM(require("node:path"));
10001
10459
  var INLINE_DATA_URI_MAX_CHARS = 4096;
@@ -10020,7 +10478,7 @@ var EXTENSION_BY_MIME = {
10020
10478
  "image/avif": ".avif"
10021
10479
  };
10022
10480
  function contentHash(bytes) {
10023
- return import_node_crypto4.default.createHash("sha256").update(bytes).digest("hex");
10481
+ return import_node_crypto5.default.createHash("sha256").update(bytes).digest("hex");
10024
10482
  }
10025
10483
  function deliveryUrl(baseUrl, asset) {
10026
10484
  const returnedUrl = asset.deliveryUrl ?? asset.delivery_url ?? asset.url;
@@ -10181,91 +10639,859 @@ function createAssetReferenceRewriter(options) {
10181
10639
  return { rewrite, rewriteUnknown };
10182
10640
  }
10183
10641
 
10184
- // src/commands/experiments.ts
10185
- function formatDate(iso) {
10186
- return new Date(iso).toLocaleDateString("en-US", {
10187
- month: "short",
10188
- day: "numeric",
10189
- year: "numeric"
10190
- });
10191
- }
10192
- function printExperiment(exp, dashboardUrl = null) {
10193
- console.log();
10194
- console.log(source_default.bold(exp.name));
10195
- if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
10196
- console.log(` ID: ${source_default.dim(exp.id)}`);
10197
- if (exp.slug) console.log(` Slug: ${source_default.cyan(exp.slug)}`);
10198
- console.log(` Status: ${statusBadge(exp.status)}`);
10199
- console.log(` Classification: ${exp.counting_type}`);
10200
- console.log(` Traffic: ${(exp.traffic_allocation * 100).toFixed(0)}%`);
10201
- console.log(` QA approved: ${exp.qa_approved_at ?? "none"}`);
10202
- console.log(` Scheduled: ${exp.scheduled_start_at ?? "none"}`);
10203
- console.log(` Started: ${exp.started_at ?? "none"}`);
10204
- if (exp.targeting !== void 0) {
10205
- console.log(` Targeting: ${effectivePageTargetingLabel(exp.targeting)}`);
10206
- const device = deviceLabel(exp.targeting);
10207
- if (device) console.log(` Device: ${device}`);
10208
- }
10209
- console.log(` Created: ${formatDate(exp.created_at)}`);
10210
- if (exp.linked_idea) {
10211
- const label = exp.linked_idea.key ? `${exp.linked_idea.key}: ` : "";
10212
- const stage = exp.linked_idea.stage ? ` (${exp.linked_idea.stage})` : "";
10213
- console.log(` Idea: ${label}${exp.linked_idea.title}${stage}`);
10214
- }
10215
- if (exp.variations && exp.variations.length > 0) {
10216
- console.log(` Variations:`);
10217
- for (const v of exp.variations) {
10218
- const tag = v.is_control ? source_default.dim(" (control)") : "";
10219
- console.log(` - ${v.name}${tag} ${source_default.dim(`weight=${(v.weight * 100).toFixed(0)}%`)}`);
10220
- }
10221
- }
10222
- console.log();
10223
- }
10224
- function formatPct(value, digits = 1) {
10225
- return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}%` : "-";
10226
- }
10227
- function confidenceBasisLabel(value) {
10228
- if (value === "fixed_horizon") return "fixed-horizon";
10229
- if (value === "sequential") return "always-valid";
10230
- return "unknown";
10231
- }
10232
- function statisticsStateLabel(value) {
10233
- return value === "untrustworthy" ? "verdict withheld" : String(value ?? "unknown");
10234
- }
10235
- function formatRevenue(value, currency) {
10236
- if (value === null || value === void 0) return "\u2014";
10237
- if (!currency || typeof value !== "number" || !Number.isFinite(value)) return String(value);
10238
- try {
10239
- return new Intl.NumberFormat("en-US", {
10240
- style: "currency",
10241
- currency: currency.trim().toUpperCase()
10242
- }).format(value);
10243
- } catch {
10244
- return String(value);
10245
- }
10246
- }
10247
- function printExperimentResults(results2, currency) {
10248
- const summary = summarizeResults(results2);
10249
- const { experiment: experiment2, totals, decision, verdictWithheld, statisticsState, presentedOutcome } = summary;
10250
- const outcomeProvenance = summary.outcome.provenance === "manager_decided" ? " (manager decided)" : "";
10251
- console.log();
10252
- console.log(source_default.bold(String(experiment2.name ?? experiment2.id ?? "Experiment results")));
10253
- console.log(` Status: ${statusBadge(String(experiment2.status ?? "unknown"))}`);
10254
- console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
10255
- console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
10256
- console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
10257
- console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
10258
- const resultsWeekdays = summary.resultsWeekdays;
10259
- if (resultsWeekdays && Array.isArray(resultsWeekdays.days)) {
10260
- const labels = {
10261
- monday: "Mon",
10262
- tuesday: "Tue",
10263
- wednesday: "Wed",
10264
- thursday: "Thu",
10265
- friday: "Fri",
10266
- saturday: "Sat",
10267
- sunday: "Sun"
10268
- };
10642
+ // src/generated/experiment-filesystem.schema.json
10643
+ var experiment_filesystem_schema_default = {
10644
+ $id: "https://api.drip-apex.com/schemas/experiment-filesystem-v1.json",
10645
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10646
+ files: {
10647
+ "experiment.json": {
10648
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10649
+ additionalProperties: false,
10650
+ properties: {
10651
+ autoStopOverride: {
10652
+ anyOf: [
10653
+ {},
10654
+ {
10655
+ type: "null"
10656
+ }
10657
+ ]
10658
+ },
10659
+ countingType: {
10660
+ anyOf: [
10661
+ {
10662
+ enum: ["standard", "setup_test", "aa_test", "non_program"],
10663
+ type: "string"
10664
+ },
10665
+ {
10666
+ type: "null"
10667
+ }
10668
+ ]
10669
+ },
10670
+ description: {
10671
+ anyOf: [
10672
+ {
10673
+ type: "string"
10674
+ },
10675
+ {
10676
+ type: "null"
10677
+ }
10678
+ ]
10679
+ },
10680
+ environmentKey: {
10681
+ type: "string"
10682
+ },
10683
+ experimentType: {
10684
+ enum: ["mutation", "redirect"],
10685
+ type: "string"
10686
+ },
10687
+ id: {
10688
+ anyOf: [
10689
+ {
10690
+ type: "string"
10691
+ },
10692
+ {
10693
+ type: "null"
10694
+ }
10695
+ ]
10696
+ },
10697
+ name: {
10698
+ type: "string"
10699
+ },
10700
+ schemaVersion: {
10701
+ const: 1,
10702
+ type: "number"
10703
+ },
10704
+ sequentialTestingEnabled: {
10705
+ type: "boolean"
10706
+ },
10707
+ significanceLevel: {
10708
+ type: "number"
10709
+ },
10710
+ slug: {
10711
+ anyOf: [
10712
+ {
10713
+ type: "string"
10714
+ },
10715
+ {
10716
+ type: "null"
10717
+ }
10718
+ ]
10719
+ },
10720
+ statisticalPower: {
10721
+ type: "number"
10722
+ },
10723
+ storeUrl: {
10724
+ anyOf: [
10725
+ {
10726
+ type: "string"
10727
+ },
10728
+ {
10729
+ type: "null"
10730
+ }
10731
+ ]
10732
+ },
10733
+ testKey: {
10734
+ anyOf: [
10735
+ {
10736
+ type: "string"
10737
+ },
10738
+ {
10739
+ type: "null"
10740
+ }
10741
+ ]
10742
+ },
10743
+ trafficAllocation: {
10744
+ maximum: 1,
10745
+ minimum: 0,
10746
+ type: "number"
10747
+ }
10748
+ },
10749
+ required: ["schemaVersion"],
10750
+ type: "object"
10751
+ },
10752
+ "experiment/index.css": {
10753
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10754
+ contentMediaType: "text/css",
10755
+ type: "string"
10756
+ },
10757
+ "experiment/index.js": {
10758
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10759
+ contentMediaType: "text/javascript",
10760
+ type: "string"
10761
+ },
10762
+ "experiment/reset.js": {
10763
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10764
+ contentMediaType: "text/javascript",
10765
+ type: "string"
10766
+ },
10767
+ "launch.json": {
10768
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10769
+ additionalProperties: false,
10770
+ properties: {
10771
+ endedAt: {
10772
+ anyOf: [
10773
+ {
10774
+ type: "string"
10775
+ },
10776
+ {
10777
+ type: "null"
10778
+ }
10779
+ ]
10780
+ },
10781
+ launchApproval: {
10782
+ anyOf: [
10783
+ {
10784
+ additionalProperties: false,
10785
+ properties: {
10786
+ decidedAt: {
10787
+ anyOf: [
10788
+ {
10789
+ type: "string"
10790
+ },
10791
+ {
10792
+ type: "null"
10793
+ }
10794
+ ]
10795
+ },
10796
+ note: {
10797
+ anyOf: [
10798
+ {
10799
+ type: "string"
10800
+ },
10801
+ {
10802
+ type: "null"
10803
+ }
10804
+ ]
10805
+ },
10806
+ status: {
10807
+ type: "string"
10808
+ }
10809
+ },
10810
+ required: ["status", "decidedAt", "note"],
10811
+ type: "object"
10812
+ },
10813
+ {
10814
+ type: "null"
10815
+ }
10816
+ ]
10817
+ },
10818
+ launchGate: {
10819
+ additionalProperties: false,
10820
+ properties: {
10821
+ qaApproved: {
10822
+ type: "boolean"
10823
+ },
10824
+ treatmentArtifactsApproved: {
10825
+ type: "boolean"
10826
+ }
10827
+ },
10828
+ required: ["qaApproved", "treatmentArtifactsApproved"],
10829
+ type: "object"
10830
+ },
10831
+ readonly: {
10832
+ const: true,
10833
+ type: "boolean"
10834
+ },
10835
+ scheduledEndAt: {
10836
+ anyOf: [
10837
+ {
10838
+ type: "string"
10839
+ },
10840
+ {
10841
+ type: "null"
10842
+ }
10843
+ ]
10844
+ },
10845
+ scheduledStartAt: {
10846
+ anyOf: [
10847
+ {
10848
+ type: "string"
10849
+ },
10850
+ {
10851
+ type: "null"
10852
+ }
10853
+ ]
10854
+ },
10855
+ schemaVersion: {
10856
+ const: 1,
10857
+ type: "number"
10858
+ },
10859
+ startedAt: {
10860
+ anyOf: [
10861
+ {
10862
+ type: "string"
10863
+ },
10864
+ {
10865
+ type: "null"
10866
+ }
10867
+ ]
10868
+ },
10869
+ status: {
10870
+ type: "string"
10871
+ }
10872
+ },
10873
+ readOnly: true,
10874
+ required: [
10875
+ "schemaVersion",
10876
+ "readonly",
10877
+ "status",
10878
+ "scheduledStartAt",
10879
+ "scheduledEndAt",
10880
+ "startedAt",
10881
+ "endedAt",
10882
+ "launchApproval",
10883
+ "launchGate"
10884
+ ],
10885
+ type: "object"
10886
+ },
10887
+ "metrics.json": {
10888
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10889
+ additionalProperties: false,
10890
+ properties: {
10891
+ goals: {
10892
+ items: {
10893
+ additionalProperties: false,
10894
+ properties: {
10895
+ capture: {
10896
+ anyOf: [
10897
+ {},
10898
+ {
10899
+ type: "null"
10900
+ }
10901
+ ]
10902
+ },
10903
+ countOnce: {
10904
+ type: "boolean"
10905
+ },
10906
+ eventName: {
10907
+ anyOf: [
10908
+ {
10909
+ type: "string"
10910
+ },
10911
+ {
10912
+ type: "null"
10913
+ }
10914
+ ]
10915
+ },
10916
+ filter: {
10917
+ anyOf: [
10918
+ {},
10919
+ {
10920
+ type: "null"
10921
+ }
10922
+ ]
10923
+ },
10924
+ id: {
10925
+ anyOf: [
10926
+ {
10927
+ type: "string"
10928
+ },
10929
+ {
10930
+ type: "null"
10931
+ }
10932
+ ]
10933
+ },
10934
+ isPrimary: {
10935
+ type: "boolean"
10936
+ },
10937
+ measurement: {},
10938
+ name: {
10939
+ type: "string"
10940
+ },
10941
+ pageId: {
10942
+ anyOf: [
10943
+ {
10944
+ type: "string"
10945
+ },
10946
+ {
10947
+ type: "null"
10948
+ }
10949
+ ]
10950
+ },
10951
+ selector: {
10952
+ anyOf: [
10953
+ {
10954
+ type: "string"
10955
+ },
10956
+ {
10957
+ type: "null"
10958
+ }
10959
+ ]
10960
+ },
10961
+ textMatch: {
10962
+ anyOf: [
10963
+ {
10964
+ type: "string"
10965
+ },
10966
+ {
10967
+ type: "null"
10968
+ }
10969
+ ]
10970
+ },
10971
+ type: {
10972
+ enum: ["click", "pageview", "custom", "revenue"],
10973
+ type: "string"
10974
+ },
10975
+ urlPattern: {
10976
+ anyOf: [
10977
+ {
10978
+ type: "string"
10979
+ },
10980
+ {
10981
+ type: "null"
10982
+ }
10983
+ ]
10984
+ },
10985
+ valueProperty: {
10986
+ anyOf: [
10987
+ {
10988
+ type: "string"
10989
+ },
10990
+ {
10991
+ type: "null"
10992
+ }
10993
+ ]
10994
+ },
10995
+ valueUnit: {
10996
+ anyOf: [
10997
+ {
10998
+ type: "string"
10999
+ },
11000
+ {
11001
+ type: "null"
11002
+ }
11003
+ ]
11004
+ }
11005
+ },
11006
+ required: ["name", "type"],
11007
+ type: "object"
11008
+ },
11009
+ type: "array"
11010
+ },
11011
+ primaryGoalId: {
11012
+ anyOf: [
11013
+ {
11014
+ type: "string"
11015
+ },
11016
+ {
11017
+ type: "null"
11018
+ }
11019
+ ]
11020
+ },
11021
+ schemaVersion: {
11022
+ const: 1,
11023
+ type: "number"
11024
+ }
11025
+ },
11026
+ required: ["schemaVersion", "primaryGoalId", "goals"],
11027
+ type: "object"
11028
+ },
11029
+ "qa.json": {
11030
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11031
+ additionalProperties: false,
11032
+ properties: {
11033
+ latestRun: {
11034
+ anyOf: [
11035
+ {
11036
+ additionalProperties: false,
11037
+ properties: {
11038
+ approvedAt: {
11039
+ anyOf: [
11040
+ {
11041
+ type: "string"
11042
+ },
11043
+ {
11044
+ type: "null"
11045
+ }
11046
+ ]
11047
+ },
11048
+ completedAt: {
11049
+ anyOf: [
11050
+ {
11051
+ type: "string"
11052
+ },
11053
+ {
11054
+ type: "null"
11055
+ }
11056
+ ]
11057
+ },
11058
+ id: {
11059
+ type: "string"
11060
+ },
11061
+ source: {
11062
+ type: "string"
11063
+ },
11064
+ status: {
11065
+ type: "string"
11066
+ },
11067
+ summary: {
11068
+ anyOf: [
11069
+ {},
11070
+ {
11071
+ type: "null"
11072
+ }
11073
+ ]
11074
+ }
11075
+ },
11076
+ required: ["id", "status", "source", "approvedAt", "completedAt", "summary"],
11077
+ type: "object"
11078
+ },
11079
+ {
11080
+ type: "null"
11081
+ }
11082
+ ]
11083
+ },
11084
+ qaApprovedAt: {
11085
+ anyOf: [
11086
+ {
11087
+ type: "string"
11088
+ },
11089
+ {
11090
+ type: "null"
11091
+ }
11092
+ ]
11093
+ },
11094
+ qaApprovedBy: {
11095
+ anyOf: [
11096
+ {
11097
+ type: "string"
11098
+ },
11099
+ {
11100
+ type: "null"
11101
+ }
11102
+ ]
11103
+ },
11104
+ readonly: {
11105
+ const: true,
11106
+ type: "boolean"
11107
+ },
11108
+ schemaVersion: {
11109
+ const: 1,
11110
+ type: "number"
11111
+ }
11112
+ },
11113
+ readOnly: true,
11114
+ required: ["schemaVersion", "readonly", "qaApprovedAt", "qaApprovedBy", "latestRun"],
11115
+ type: "object"
11116
+ },
11117
+ "targeting.json": {
11118
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11119
+ additionalProperties: {},
11120
+ properties: {
11121
+ authorUrl: {
11122
+ items: {
11123
+ additionalProperties: false,
11124
+ properties: {
11125
+ include: {
11126
+ type: "boolean"
11127
+ },
11128
+ match_type: {
11129
+ enum: ["exact", "contains", "wildcard", "regex"],
11130
+ type: "string"
11131
+ },
11132
+ matchType: {
11133
+ enum: ["exact", "contains", "wildcard", "regex"],
11134
+ type: "string"
11135
+ },
11136
+ pattern: {
11137
+ minLength: 1,
11138
+ type: "string"
11139
+ },
11140
+ type: {
11141
+ enum: ["regex", "simple"],
11142
+ type: "string"
11143
+ }
11144
+ },
11145
+ required: ["type", "pattern"],
11146
+ type: "object"
11147
+ },
11148
+ type: "array"
11149
+ },
11150
+ custom: {},
11151
+ device: {},
11152
+ deviceType: {
11153
+ type: "string"
11154
+ },
11155
+ lifecycle: {},
11156
+ pageRules: {
11157
+ items: {},
11158
+ type: "array"
11159
+ },
11160
+ pages: {
11161
+ items: {},
11162
+ type: "array"
11163
+ },
11164
+ segments: {
11165
+ items: {},
11166
+ type: "array"
11167
+ },
11168
+ url: {
11169
+ items: {
11170
+ additionalProperties: false,
11171
+ properties: {
11172
+ include: {
11173
+ type: "boolean"
11174
+ },
11175
+ match_type: {
11176
+ enum: ["exact", "contains", "wildcard", "regex"],
11177
+ type: "string"
11178
+ },
11179
+ matchType: {
11180
+ enum: ["exact", "contains", "wildcard", "regex"],
11181
+ type: "string"
11182
+ },
11183
+ pattern: {
11184
+ minLength: 1,
11185
+ type: "string"
11186
+ },
11187
+ type: {
11188
+ enum: ["regex", "simple"],
11189
+ type: "string"
11190
+ }
11191
+ },
11192
+ required: ["type", "pattern"],
11193
+ type: "object"
11194
+ },
11195
+ type: "array"
11196
+ }
11197
+ },
11198
+ type: "object"
11199
+ },
11200
+ "variations/*/anti-flicker.json": {
11201
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11202
+ items: {
11203
+ type: "string"
11204
+ },
11205
+ type: "array"
11206
+ },
11207
+ "variations/*/index.css": {
11208
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11209
+ contentMediaType: "text/css",
11210
+ type: "string"
11211
+ },
11212
+ "variations/*/index.js": {
11213
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11214
+ contentMediaType: "text/javascript",
11215
+ type: "string"
11216
+ },
11217
+ "variations/*/mutations.json": {
11218
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11219
+ items: {
11220
+ additionalProperties: {},
11221
+ anyOf: [
11222
+ {
11223
+ required: ["selector"]
11224
+ },
11225
+ {
11226
+ properties: {
11227
+ action: {
11228
+ enum: [
11229
+ "priceTest",
11230
+ "shippingTest",
11231
+ "offerTest",
11232
+ "postPurchaseOffer",
11233
+ "checkoutBlock"
11234
+ ]
11235
+ }
11236
+ },
11237
+ required: ["action"]
11238
+ }
11239
+ ],
11240
+ properties: {
11241
+ action: {
11242
+ type: "string"
11243
+ },
11244
+ selector: {
11245
+ minLength: 1,
11246
+ type: "string"
11247
+ },
11248
+ type: {
11249
+ type: "string"
11250
+ }
11251
+ },
11252
+ type: "object"
11253
+ },
11254
+ type: "array"
11255
+ },
11256
+ "variations/*/reset.js": {
11257
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11258
+ contentMediaType: "text/javascript",
11259
+ type: "string"
11260
+ },
11261
+ "variations/*/url-blocks.json": {
11262
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11263
+ items: {},
11264
+ type: "array"
11265
+ },
11266
+ "variations/*/variation.json": {
11267
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11268
+ additionalProperties: false,
11269
+ properties: {
11270
+ artifactReview: {
11271
+ additionalProperties: false,
11272
+ properties: {
11273
+ hash: {
11274
+ anyOf: [
11275
+ {
11276
+ type: "string"
11277
+ },
11278
+ {
11279
+ type: "null"
11280
+ }
11281
+ ]
11282
+ },
11283
+ reviewedAt: {
11284
+ anyOf: [
11285
+ {
11286
+ type: "string"
11287
+ },
11288
+ {
11289
+ type: "null"
11290
+ }
11291
+ ]
11292
+ },
11293
+ reviewedBy: {
11294
+ anyOf: [
11295
+ {
11296
+ type: "string"
11297
+ },
11298
+ {
11299
+ type: "null"
11300
+ }
11301
+ ]
11302
+ },
11303
+ source: {
11304
+ anyOf: [
11305
+ {
11306
+ type: "string"
11307
+ },
11308
+ {
11309
+ type: "null"
11310
+ }
11311
+ ]
11312
+ },
11313
+ status: {
11314
+ type: "string"
11315
+ }
11316
+ },
11317
+ required: ["status"],
11318
+ type: "object"
11319
+ },
11320
+ codeDeliveryMode: {
11321
+ enum: ["inline", "external"],
11322
+ type: "string"
11323
+ },
11324
+ cssArtifactId: {
11325
+ anyOf: [
11326
+ {
11327
+ type: "string"
11328
+ },
11329
+ {
11330
+ type: "null"
11331
+ }
11332
+ ]
11333
+ },
11334
+ id: {
11335
+ anyOf: [
11336
+ {
11337
+ type: "string"
11338
+ },
11339
+ {
11340
+ type: "null"
11341
+ }
11342
+ ]
11343
+ },
11344
+ isControl: {
11345
+ type: "boolean"
11346
+ },
11347
+ jsArtifactId: {
11348
+ anyOf: [
11349
+ {
11350
+ type: "string"
11351
+ },
11352
+ {
11353
+ type: "null"
11354
+ }
11355
+ ]
11356
+ },
11357
+ name: {
11358
+ type: "string"
11359
+ },
11360
+ redirectUrl: {
11361
+ anyOf: [
11362
+ {
11363
+ type: "string"
11364
+ },
11365
+ {
11366
+ type: "null"
11367
+ }
11368
+ ]
11369
+ },
11370
+ schemaVersion: {
11371
+ const: 1,
11372
+ type: "number"
11373
+ },
11374
+ slug: {
11375
+ type: "string"
11376
+ },
11377
+ weight: {
11378
+ maximum: 1,
11379
+ minimum: 0,
11380
+ type: "number"
11381
+ }
11382
+ },
11383
+ required: ["schemaVersion"],
11384
+ type: "object"
11385
+ }
11386
+ },
11387
+ schemaVersion: 1,
11388
+ source: "experiment-filesystem"
11389
+ };
11390
+
11391
+ // src/commands/experiments.ts
11392
+ function formatDate(iso) {
11393
+ return new Date(iso).toLocaleDateString("en-US", {
11394
+ month: "short",
11395
+ day: "numeric",
11396
+ year: "numeric"
11397
+ });
11398
+ }
11399
+ function printExperiment(exp, dashboardUrl = null) {
11400
+ console.log();
11401
+ console.log(source_default.bold(exp.name));
11402
+ if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
11403
+ console.log(` ID: ${source_default.dim(exp.id)}`);
11404
+ if (exp.slug) console.log(` Slug: ${source_default.cyan(exp.slug)}`);
11405
+ console.log(` Status: ${statusBadge(exp.status)}`);
11406
+ console.log(` Classification: ${exp.counting_type}`);
11407
+ console.log(` Traffic: ${(exp.traffic_allocation * 100).toFixed(0)}%`);
11408
+ console.log(` QA approved: ${exp.qa_approved_at ?? "none"}`);
11409
+ console.log(` Scheduled: ${exp.scheduled_start_at ?? "none"}`);
11410
+ console.log(` Started: ${exp.started_at ?? "none"}`);
11411
+ if (exp.targeting !== void 0) {
11412
+ console.log(` Targeting: ${effectivePageTargetingLabel(exp.targeting)}`);
11413
+ const device = deviceLabel(exp.targeting);
11414
+ if (device) console.log(` Device: ${device}`);
11415
+ }
11416
+ console.log(` Created: ${formatDate(exp.created_at)}`);
11417
+ if (exp.linked_idea) {
11418
+ const label = exp.linked_idea.key ? `${exp.linked_idea.key}: ` : "";
11419
+ const stage = exp.linked_idea.stage ? ` (${exp.linked_idea.stage})` : "";
11420
+ console.log(` Idea: ${label}${exp.linked_idea.title}${stage}`);
11421
+ }
11422
+ if (exp.variations && exp.variations.length > 0) {
11423
+ console.log(` Variations:`);
11424
+ for (const v of exp.variations) {
11425
+ const tag = v.is_control ? source_default.dim(" (control)") : "";
11426
+ console.log(` - ${v.name}${tag} ${source_default.dim(`weight=${(v.weight * 100).toFixed(0)}%`)}`);
11427
+ }
11428
+ }
11429
+ console.log();
11430
+ }
11431
+ function formatPct(value, digits = 1) {
11432
+ return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(digits)}%` : "-";
11433
+ }
11434
+ function confidenceBasisLabel(value) {
11435
+ if (value === "fixed_horizon") return "fixed-horizon";
11436
+ if (value === "sequential") return "always-valid";
11437
+ return "unknown";
11438
+ }
11439
+ function statisticsStateLabel(value) {
11440
+ return value === "untrustworthy" ? "verdict withheld" : String(value ?? "unknown");
11441
+ }
11442
+ function formatRevenue(value, currency) {
11443
+ if (value === null || value === void 0) return "\u2014";
11444
+ if (!currency || typeof value !== "number" || !Number.isFinite(value)) return String(value);
11445
+ try {
11446
+ return new Intl.NumberFormat("en-US", {
11447
+ style: "currency",
11448
+ currency: currency.trim().toUpperCase()
11449
+ }).format(value);
11450
+ } catch {
11451
+ return String(value);
11452
+ }
11453
+ }
11454
+ function audienceLine(segmentApplied) {
11455
+ if (!segmentApplied || typeof segmentApplied !== "object") return null;
11456
+ const { requested, applied, reason: reason2 } = segmentApplied;
11457
+ if (!requested) return null;
11458
+ const label = `${requested.mode === "exclude" ? "excluding " : ""}${String(requested.dim)}=${String(requested.value)}`;
11459
+ return applied === true ? label : `all visitors (${label} not applied: ${typeof reason2 === "string" ? reason2 : "unknown reason"})`;
11460
+ }
11461
+ function printExperimentResults(results2, currency) {
11462
+ const summary = summarizeResults(results2);
11463
+ const { experiment: experiment2, totals, decision, verdictWithheld, statisticsState, presentedOutcome } = summary;
11464
+ const outcomeProvenance = summary.outcome.provenance === "manager_decided" ? " (manager decided)" : "";
11465
+ console.log();
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
+ }
11477
+ console.log(` Status: ${statusBadge(String(experiment2.status ?? "unknown"))}`);
11478
+ const audience = audienceLine(results2.segment_applied);
11479
+ if (audience) console.log(` Audience: ${audience}`);
11480
+ console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
11481
+ console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
11482
+ console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
11483
+ console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
11484
+ const resultsWeekdays = summary.resultsWeekdays;
11485
+ if (resultsWeekdays && Array.isArray(resultsWeekdays.days)) {
11486
+ const labels = {
11487
+ monday: "Mon",
11488
+ tuesday: "Tue",
11489
+ wednesday: "Wed",
11490
+ thursday: "Thu",
11491
+ friday: "Fri",
11492
+ saturday: "Sat",
11493
+ sunday: "Sun"
11494
+ };
10269
11495
  console.log(
10270
11496
  ` Judged on: ${resultsWeekdays.days.map((day) => labels[String(day)] ?? day).join(", ")} (${String(resultsWeekdays.timezone)})`
10271
11497
  );
@@ -10314,18 +11540,11 @@ function printExperimentResults(results2, currency) {
10314
11540
  }
10315
11541
  console.log();
10316
11542
  }
11543
+ var filesystemPaths = Object.keys(experiment_filesystem_schema_default.files);
10317
11544
  var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
10318
- topLevel: ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"],
10319
- experiment: ["index.css", "index.js", "reset.js"],
10320
- variation: [
10321
- "variation.json",
10322
- "mutations.json",
10323
- "url-blocks.json",
10324
- "index.css",
10325
- "index.js",
10326
- "reset.js",
10327
- "anti-flicker.json"
10328
- ]
11545
+ topLevel: filesystemPaths.filter((entry) => !entry.includes("/")),
11546
+ experiment: filesystemPaths.filter((entry) => entry.startsWith("experiment/")).map((entry) => entry.slice("experiment/".length)),
11547
+ variation: filesystemPaths.filter((entry) => entry.startsWith("variations/*/")).map((entry) => entry.slice("variations/*/".length))
10329
11548
  };
10330
11549
  var SERVER_OWNED_EXPERIMENT_FILES = /* @__PURE__ */ new Set(["qa.json", "launch.json"]);
10331
11550
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -10334,7 +11553,7 @@ var FILESYSTEM_FILE_RE = new RegExp(
10334
11553
  `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
10335
11554
  );
10336
11555
  function sha256Text(value) {
10337
- return import_node_crypto5.default.createHash("sha256").update(value).digest("hex");
11556
+ return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
10338
11557
  }
10339
11558
  function folderPath(folder) {
10340
11559
  return import_node_path6.default.resolve(process.cwd(), folder);
@@ -10453,7 +11672,7 @@ async function rewriteExperimentFilesystemAssets(input) {
10453
11672
  baseUrl: input.baseUrl,
10454
11673
  enabled: input.enabled,
10455
11674
  upload: async (file) => {
10456
- 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");
10457
11676
  const cached = input.assets[hash];
10458
11677
  if (cached) return cached;
10459
11678
  if (input.upload === null) return null;
@@ -10777,20 +11996,31 @@ var list = new Command("list").alias("ls").description(
10777
11996
  "List experiments in summary view by default; use --full for details or --fields for selected columns (archived experiments are hidden by default)"
10778
11997
  ).option(
10779
11998
  "-s, --status <status>",
10780
- "Filter by status (draft|awaiting_client_approval|changes_requested|client_approved|qa|ready_to_launch|running|paused|completed)"
11999
+ "Filter by status (draft|awaiting_client_approval|changes_requested|client_approved|qa|ready_to_launch|running|paused|completed) (applied server-side)"
12000
+ ).option(
12001
+ "--url <absolute-url>",
12002
+ "Filter by effective storefront URL targeting (applied server-side)"
10781
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(
10782
12004
  "--fields <list>",
10783
- "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)"
10784
12006
  ).action(async function action3(opts) {
10785
12007
  const creds = requireCredentials();
10786
12008
  const spinner = await createSpinner("Fetching experiments...");
10787
- const res = await listExperiments(creds, {
12009
+ const listDefault = () => listExperiments(creds, {
10788
12010
  status: opts.status,
10789
12011
  limit: parseInt(opts.limit, 10),
10790
12012
  includeArchived: opts.includeArchived,
10791
12013
  view: opts.full ? "full" : "summary",
10792
12014
  fields: opts.fields
10793
12015
  });
12016
+ const res = opts.url !== void 0 ? await listExperiments(creds, {
12017
+ status: opts.status,
12018
+ limit: parseInt(opts.limit, 10),
12019
+ includeArchived: opts.includeArchived,
12020
+ view: opts.full ? "full" : "summary",
12021
+ fields: opts.fields,
12022
+ url: opts.url
12023
+ }) : await listDefault();
10794
12024
  if (!res.ok) {
10795
12025
  spinner?.fail("Failed to fetch experiments");
10796
12026
  fail(this, res.error, classifyStatus(res.status), res.errorDetails);
@@ -10928,7 +12158,13 @@ ${JSON.stringify(data.order, null, 2)}`);
10928
12158
  console.log(`
10929
12159
  Definitions: ${data.definitions_url}`);
10930
12160
  }
10931
- 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("--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) {
12162
+ if (opts.explain && (opts.segment !== void 0 || opts.excludeSegment !== void 0))
12163
+ fail(
12164
+ this,
12165
+ "--segment and --exclude-segment are only available for results, without --explain",
12166
+ EXIT_CODES.validation
12167
+ );
10932
12168
  if (!opts.explain && (opts.goal || opts.order || opts.samples))
10933
12169
  fail(this, "--goal, --order and --samples require --explain", EXIT_CODES.validation);
10934
12170
  const creds = requireCredentials();
@@ -10939,12 +12175,38 @@ var results = new Command("results").description("Read experiment results and ti
10939
12175
  spinner?.fail(`Experiment "${idOrSlug}" not found`);
10940
12176
  fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
10941
12177
  }
10942
- const res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, { currency: opts.currency });
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, {
12184
+ currency: opts.currency,
12185
+ segment: opts.segment,
12186
+ exclude_segment: opts.excludeSegment
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
+ }
10943
12205
  if (!res.ok || !res.data) {
10944
12206
  spinner?.fail("Failed to fetch experiment results");
10945
12207
  fail(
10946
12208
  this,
10947
- res.error ?? "Failed to fetch experiment results",
12209
+ `${res.error ?? "Failed to fetch experiment results"}${isResultsComputing(res) ? "; increase --wait to keep retrying" : ""}`,
10948
12210
  classifyStatus(res.status),
10949
12211
  res.errorDetails
10950
12212
  );
@@ -10961,6 +12223,11 @@ var results = new Command("results").description("Read experiment results and ti
10961
12223
  fail(this, error instanceof Error ? error.message : String(error));
10962
12224
  }
10963
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
+ }
10964
12231
  var DAY_NAMES = {
10965
12232
  mon: "monday",
10966
12233
  monday: "monday",
@@ -11213,7 +12480,7 @@ var create = new Command("create").description(
11213
12480
  []
11214
12481
  ).option(
11215
12482
  "--goal <goal-id|new:fields>",
11216
- "Goal id or new:type=...,selector=...,name=... (repeatable)",
12483
+ "Goal id or new:type=...,selector=...,name=... (repeatable; omitted uses the shop default main goal)",
11217
12484
  collectCreateOption,
11218
12485
  []
11219
12486
  ).option(
@@ -11632,6 +12899,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
11632
12899
  output(
11633
12900
  this,
11634
12901
  {
12902
+ schemaVersion: 1,
11635
12903
  folder,
11636
12904
  experimentId: exp.id,
11637
12905
  files: res.data.files.map((file) => import_node_path6.default.join(folder, file.path))
@@ -12034,7 +13302,45 @@ var applyFilesystem = new Command("apply").option(
12034
13302
  acceptQaInvalidation = true;
12035
13303
  }
12036
13304
  const spinner = process.stderr.isTTY ? await createSpinner("Applying experiment filesystem...") : null;
12037
- 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({
12038
13344
  files,
12039
13345
  accept_qa_invalidation: acceptQaInvalidation,
12040
13346
  ...opts.confirmTargetingReplace ? { confirmTargetingReplace: true } : {}
@@ -12055,7 +13361,7 @@ var applyFilesystem = new Command("apply").option(
12055
13361
  output(this, { aborted: true }, () => console.log("Aborted."));
12056
13362
  return;
12057
13363
  }
12058
- res = await applyExperimentFilesystem(creds, session.experimentId, {
13364
+ res = await applyWithVerification({
12059
13365
  files,
12060
13366
  accept_qa_invalidation: acceptQaInvalidation,
12061
13367
  confirmTargetingReplace: true
@@ -12077,7 +13383,8 @@ var applyFilesystem = new Command("apply").option(
12077
13383
  plan: res.data?.plan
12078
13384
  });
12079
13385
  }
12080
- spinner?.succeed("Applied experiment filesystem");
13386
+ if (publicationUnconfirmed) spinner?.stop();
13387
+ else spinner?.succeed("Applied experiment filesystem");
12081
13388
  const synced = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
12082
13389
  if (!synced) {
12083
13390
  await refreshServerOwnedFilesIfNeeded(
@@ -12101,6 +13408,11 @@ var applyFilesystem = new Command("apply").option(
12101
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.");
12102
13409
  const previewResult = await getQaPreviewUrls(creds, session.experimentId);
12103
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
+ };
12104
13416
  output(
12105
13417
  this,
12106
13418
  {
@@ -12109,14 +13421,30 @@ var applyFilesystem = new Command("apply").option(
12109
13421
  applied: true,
12110
13422
  plan: planRes.data,
12111
13423
  requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
12112
- result: res.data,
13424
+ result: verifiedAfterTimeout ? null : res.data,
12113
13425
  synced,
12114
13426
  dashboardUrl,
12115
- receipt: res.data?.receipt ?? null,
12116
- 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
+ } : {}
12117
13436
  },
12118
13437
  () => {
12119
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
+ }
12120
13448
  const receipt = res.data?.receipt;
12121
13449
  if (receipt) {
12122
13450
  console.log("\nReceipt");
@@ -12277,6 +13605,9 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
12277
13605
  "[folder-or-id]",
12278
13606
  "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
12279
13607
  "."
13608
+ ).option(
13609
+ "--acknowledge-zero-traffic",
13610
+ "Acknowledge variations that receive no traffic for this split (Apex staff only)"
12280
13611
  ).action(async function action19(target) {
12281
13612
  const creds = requireCredentials();
12282
13613
  const spinner = await createSpinner("Approving QA...");
@@ -12284,7 +13615,40 @@ var qaApprove = new Command("approve").description("Approve QA and move the expe
12284
13615
  const resolved = await resolveExperimentForFilesystemOrId(target);
12285
13616
  const before = await resolveExperiment(resolved.experimentId);
12286
13617
  if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
12287
- 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
+ }
12288
13652
  if (!res.ok)
12289
13653
  throw new CliError(
12290
13654
  res.error ?? "Failed to approve QA",
@@ -12460,7 +13824,55 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
12460
13824
  throw error;
12461
13825
  }
12462
13826
  });
12463
- var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
13827
+ var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(
13828
+ new Command("events").description("List QA-tagged events for an experiment session").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--session <id>", "QA session id (defaults to the latest QA run)").option("--minutes <n>", "Lookback window in minutes", "60").option("--type <type>", "Event type: all, goal, or custom", "all").action(async function(target, opts) {
13829
+ const minutes = Number(opts.minutes);
13830
+ if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440)
13831
+ fail(this, "--minutes must be an integer between 5 and 1440", EXIT_CODES.validation);
13832
+ if (!["all", "goal", "custom"].includes(opts.type))
13833
+ fail(this, "--type must be all, goal, or custom", EXIT_CODES.validation);
13834
+ const creds = requireCredentials();
13835
+ const experiment2 = { id: (await resolveExperimentForFilesystemOrId(target)).experimentId };
13836
+ let session = opts.session;
13837
+ if (!session) {
13838
+ const status4 = await getExperimentQaRun(creds, experiment2.id);
13839
+ if (!status4.ok || !status4.data)
13840
+ throw new CliError(
13841
+ status4.error ?? "Failed to fetch QA run",
13842
+ classifyStatus(status4.status),
13843
+ status4.errorDetails
13844
+ );
13845
+ session = status4.data.run.id;
13846
+ }
13847
+ const response = await getExperimentQaEvents(creds, experiment2.id, {
13848
+ session,
13849
+ minutes,
13850
+ type: opts.type
13851
+ });
13852
+ if (!response.ok || !response.data)
13853
+ throw new CliError(
13854
+ response.error ?? "Failed to fetch QA events",
13855
+ classifyStatus(response.status),
13856
+ response.errorDetails
13857
+ );
13858
+ output(
13859
+ this,
13860
+ response.data,
13861
+ () => printTable(
13862
+ ["TYPE", "EVENT", "VARIATION", "SESSION", "COUNT", "LAST SEEN", "LAST URL"],
13863
+ response.data.data.map((row) => [
13864
+ row.event_type === "goal" ? "goal" : "custom",
13865
+ row.event_name || row.goal_id,
13866
+ row.variation_id,
13867
+ row.qa_session_id,
13868
+ String(row.events),
13869
+ row.last_event_at ?? "\u2014",
13870
+ row.last_url ?? "\u2014"
13871
+ ])
13872
+ )
13873
+ );
13874
+ })
13875
+ ).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
12464
13876
  var SCREENSHOT_DEVICES = [
12465
13877
  "desktop_chrome",
12466
13878
  "iphone_15_pro",
@@ -12541,7 +13953,7 @@ var screenshots = new Command("screenshots").description(
12541
13953
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
12542
13954
  )
12543
13955
  );
12544
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
13956
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
12545
13957
  continue;
12546
13958
  }
12547
13959
  if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
@@ -12551,7 +13963,7 @@ var screenshots = new Command("screenshots").description(
12551
13963
  Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
12552
13964
  )
12553
13965
  );
12554
- resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
13966
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 4e3);
12555
13967
  continue;
12556
13968
  }
12557
13969
  break;
@@ -12705,7 +14117,7 @@ var screenshots = new Command("screenshots").description(
12705
14117
  );
12706
14118
  consecutiveTransientFailures += 1;
12707
14119
  if (consecutiveTransientFailures > 3) throw error;
12708
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14120
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
12709
14121
  continue;
12710
14122
  }
12711
14123
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
@@ -12730,11 +14142,11 @@ var screenshots = new Command("screenshots").description(
12730
14142
  polled.errorDetails
12731
14143
  );
12732
14144
  }
12733
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14145
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
12734
14146
  continue;
12735
14147
  }
12736
14148
  consecutiveTransientFailures = 0;
12737
- delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
14149
+ delayMs = Math.min(Math.round(delayMs * 1.6), 4e3);
12738
14150
  }
12739
14151
  if (opts.attachQa !== false) {
12740
14152
  const attached = await attachExperimentQaEvidence(creds, experiment2.id, {
@@ -13112,6 +14524,8 @@ init_source();
13112
14524
  async function signedPreviewFromReadiness(creds, experimentId, variationId, targetUrl) {
13113
14525
  try {
13114
14526
  const client = createHttpClient(creds, {
14527
+ onResponse: recordApiResponse2,
14528
+ observeNetworkErrors: true,
13115
14529
  onNetworkError: (info) => {
13116
14530
  throw new Error(info.cause);
13117
14531
  }
@@ -13246,9 +14660,21 @@ var previewCommand = new Command("preview").description(
13246
14660
  'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.'
13247
14661
  );
13248
14662
  }
14663
+ const snapshot = await getExperimentFilesystem(creds, exp.id).catch(() => null);
14664
+ let qaRun;
14665
+ if (snapshot?.ok && snapshot.data) {
14666
+ const qaFile = snapshot.data.files.find((file) => file.path === "qa.json");
14667
+ if (qaFile) {
14668
+ try {
14669
+ const run3 = JSON.parse(qaFile.content)?.latestRun;
14670
+ if (run3 && typeof run3.id === "string" && run3.id.trim()) qaRun = run3;
14671
+ } catch {
14672
+ }
14673
+ }
14674
+ }
13249
14675
  let previewUrl;
13250
14676
  try {
13251
- previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id);
14677
+ previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id, qaRun?.id);
13252
14678
  } catch {
13253
14679
  fail(
13254
14680
  previewCommand,
@@ -13257,6 +14683,10 @@ var previewCommand = new Command("preview").description(
13257
14683
  }
13258
14684
  const signed = await signedPreviewFromReadiness(creds, exp.id, variation.id, storeUrl);
13259
14685
  const selectedPreviewUrl = signed?.signed_preview_url ?? previewUrl.toString();
14686
+ if (signed?.signed_preview_url) {
14687
+ const signedSessionId = signed.signed_preview_qa_session_id;
14688
+ qaRun = signedSessionId ? qaRun?.id === signedSessionId ? qaRun : { id: signedSessionId } : void 0;
14689
+ }
13260
14690
  const warning = signed?.signed_preview_url ? null : "Test images may not load on this unsigned link. Open Apex \u2192 QA for a signed link.";
13261
14691
  if (warning) console.error(warning);
13262
14692
  output(
@@ -13267,6 +14697,7 @@ var previewCommand = new Command("preview").description(
13267
14697
  previewUrl: selectedPreviewUrl,
13268
14698
  expiresAt: signed?.signed_preview_expires_at ?? null,
13269
14699
  ...warning ? { warning } : {},
14700
+ ...qaRun ? { qa_run: qaRun, qa_session_id: qaRun.id } : {},
13270
14701
  ...!opts?.storeUrl && exp.preview_target ? { previewTarget: exp.preview_target } : {}
13271
14702
  },
13272
14703
  () => {
@@ -13338,8 +14769,7 @@ function loadHarnessConfig(folder) {
13338
14769
  experimentName: experiment2.name ?? id,
13339
14770
  variationSlug: slugify(variation, "variant-a"),
13340
14771
  variationName: `Variant ${variation}`,
13341
- targetUrl,
13342
- antiFlickerSelectors: []
14772
+ targetUrl
13343
14773
  }
13344
14774
  };
13345
14775
  }
@@ -13423,14 +14853,15 @@ async function createSession(folder) {
13423
14853
  if (!apex.targetUrl) {
13424
14854
  throw new Error("Missing targetUrl. Run `apex harness init` and set targetUrl.");
13425
14855
  }
14856
+ const resetJs = cleanupResetJs(experiment2);
13426
14857
  const res = await createHarnessSession(creds, {
13427
14858
  experimentSlug: apex.experimentSlug,
13428
14859
  experimentName: apex.experimentName,
13429
14860
  variationSlug: apex.variationSlug,
13430
14861
  variationName: apex.variationName,
13431
14862
  targetUrl: apex.targetUrl,
13432
- resetJs: cleanupResetJs(experiment2),
13433
- antiFlickerSelectors: apex.antiFlickerSelectors ?? []
14863
+ ...typeof resetJs === "string" ? { resetJs } : {},
14864
+ ...Array.isArray(apex.antiFlickerSelectors) ? { antiFlickerSelectors: apex.antiFlickerSelectors } : {}
13434
14865
  });
13435
14866
  if (!res.ok)
13436
14867
  throw new CliError(
@@ -13558,7 +14989,7 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
13558
14989
 
13559
14990
  // src/commands/operator.ts
13560
14991
  init_source();
13561
- var import_node_crypto6 = __toESM(require("node:crypto"));
14992
+ var import_node_crypto7 = __toESM(require("node:crypto"));
13562
14993
  var import_node_fs11 = __toESM(require("node:fs"));
13563
14994
  var import_node_path9 = __toESM(require("node:path"));
13564
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)$/;
@@ -13603,8 +15034,13 @@ function formatWorkspaceBlockingIssues(value) {
13603
15034
  return lines.join("\n");
13604
15035
  }).join("\n");
13605
15036
  }
15037
+ function formatHarnessRefusal(message, details) {
15038
+ const issues = formatWorkspaceBlockingIssues(details);
15039
+ return issues ? `${message}
15040
+ ${issues}` : message;
15041
+ }
13606
15042
  function sha256Text2(value) {
13607
- return import_node_crypto6.default.createHash("sha256").update(value).digest("hex");
15043
+ return import_node_crypto7.default.createHash("sha256").update(value).digest("hex");
13608
15044
  }
13609
15045
  function byteLength(value) {
13610
15046
  return Buffer.byteLength(value, "utf8");
@@ -13797,12 +15233,11 @@ async function createExactHarnessSession(folder, session) {
13797
15233
  experimentName: expRes.data.name,
13798
15234
  variationSlug: config.variationSlug ?? variation.slug ?? slugify2(variation.name, "variation"),
13799
15235
  variationName: variation.name,
13800
- targetUrl,
13801
- antiFlickerSelectors: []
15236
+ targetUrl
13802
15237
  });
13803
15238
  if (!res.ok || !res.data)
13804
15239
  throw new CliError(
13805
- res.error ?? "Failed to create harness session",
15240
+ formatHarnessRefusal(res.error ?? "Failed to create harness session", res.errorDetails),
13806
15241
  classifyStatus(res.status),
13807
15242
  res.errorDetails
13808
15243
  );
@@ -13894,7 +15329,15 @@ async function runPublish(folderArg, opts = {}) {
13894
15329
  const harnessSession = await createExactHarnessSession(folder, session);
13895
15330
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
13896
15331
  const published = await publishHarnessSession(creds, harnessSession.sessionId);
13897
- 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
+ );
13898
15341
  external = {
13899
15342
  sessionId: harnessSession.sessionId,
13900
15343
  version: version.version,
@@ -14541,10 +15984,81 @@ trackingCommand.command("latency").description("Measure forced-preview versus na
14541
15984
 
14542
15985
  // src/commands/events.ts
14543
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
+ }
14544
16013
  var eventsCommand = new Command("events").description("Export raw tracking events");
14545
- 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) => {
14546
16020
  if (!Number.isFinite(Date.parse(opts.from)) || !Number.isFinite(Date.parse(opts.to))) {
14547
- 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
+ );
14548
16062
  }
14549
16063
  if (!["exclude", "include", "only"].includes(opts.qa)) {
14550
16064
  fail(command, "--qa must be exclude, include, or only", EXIT_CODES.validation);
@@ -14573,140 +16087,161 @@ eventsCommand.command("export").description("Export every raw event in an absolu
14573
16087
  fail(command, "--json-summary requires --out for the event rows", EXIT_CODES.validation);
14574
16088
  }
14575
16089
  const output2 = opts.out ? import_node_fs14.default.createWriteStream(opts.out, { flags: "wx" }) : null;
14576
- let outputError = null;
14577
- output2?.on("error", (error) => {
14578
- outputError = error;
14579
- });
14580
- if (output2) {
14581
- try {
14582
- await new Promise((resolve2, reject) => {
14583
- output2.once("open", () => resolve2());
14584
- output2.once("error", reject);
14585
- });
14586
- } catch (error) {
14587
- fail(
14588
- command,
14589
- `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
14590
- EXIT_CODES.general,
14591
- { code: "local_file_error" }
14592
- );
14593
- }
14594
- }
14595
- const writeLine = async (line) => {
14596
- const stream = output2 ?? process.stdout;
14597
- const accepted = output2 ? output2.write(line) : writeRawCommandOutput(line);
14598
- if (accepted) return;
14599
- await new Promise((resolve2, reject) => {
14600
- const onDrain = () => {
14601
- stream.off("error", onError);
14602
- resolve2();
14603
- };
14604
- const onError = (error) => {
14605
- stream.off("drain", onDrain);
14606
- reject(error);
14607
- };
14608
- stream.once("drain", onDrain);
14609
- stream.once("error", onError);
14610
- });
14611
- };
14612
- let cursor;
14613
- let written = 0;
14614
- let pages = 0;
14615
- let totals;
14616
- do {
14617
- const response = await exportEventsPage(requireCredentials(), {
14618
- from: opts.from,
14619
- to: opts.to,
14620
- experiment: opts.experiment,
14621
- eventTypes: opts.eventType?.split(",").map((value) => value.trim()).filter(Boolean),
14622
- qa: opts.qa,
14623
- limit,
14624
- cursor
14625
- });
14626
- if (!response.ok || !response.data) {
14627
- output2?.destroy();
16090
+ let outputError = null;
16091
+ output2?.on("error", (error) => {
16092
+ outputError = error;
16093
+ });
16094
+ if (output2) {
16095
+ try {
16096
+ await new Promise((resolve2, reject) => {
16097
+ output2.once("open", () => resolve2());
16098
+ output2.once("error", reject);
16099
+ });
16100
+ } catch (error) {
16101
+ const exists = error?.code === "EEXIST";
14628
16102
  fail(
14629
16103
  command,
14630
- response.error ?? "Event export page failed",
14631
- classifyStatus(response.status),
14632
- response.errorDetails
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 }
14633
16107
  );
14634
16108
  }
14635
- if (outputError)
14636
- fail(
14637
- command,
14638
- `Cannot write ${opts.out}: ${outputError.message}`,
14639
- EXIT_CODES.general,
14640
- {
14641
- code: "local_file_error"
16109
+ }
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) {
16143
+ fail(
16144
+ command,
16145
+ response.error ?? "Event export page failed",
16146
+ classifyStatus(response.status),
16147
+ response.errorDetails
16148
+ );
16149
+ }
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
+ );
14642
16172
  }
14643
- );
14644
- const page = response.data.data;
14645
- totals ??= page.totals;
14646
- for (const event of page.events) {
16173
+ written += 1;
16174
+ }
16175
+ pages += 1;
16176
+ cursor = page.next_cursor ?? void 0;
16177
+ } while (cursor);
16178
+ if (output2) {
14647
16179
  try {
14648
- await writeLine(`${JSON.stringify(event)}
14649
- `);
16180
+ await new Promise((resolve2, reject) => {
16181
+ if (outputError) return reject(outputError);
16182
+ output2.once("error", reject);
16183
+ output2.end(resolve2);
16184
+ });
14650
16185
  } catch (error) {
14651
16186
  fail(
14652
16187
  command,
14653
- `Cannot write ${opts.out ?? "stdout"}: ${error instanceof Error ? error.message : String(error)}`,
16188
+ `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
14654
16189
  EXIT_CODES.general,
14655
16190
  { code: "local_file_error" }
14656
16191
  );
14657
16192
  }
14658
- written += 1;
14659
- }
14660
- pages += 1;
14661
- cursor = page.next_cursor ?? void 0;
14662
- } while (cursor);
14663
- if (output2) {
14664
- try {
14665
- await new Promise((resolve2, reject) => {
14666
- if (outputError) return reject(outputError);
14667
- output2.once("error", reject);
14668
- output2.end(resolve2);
14669
- });
14670
- } catch (error) {
14671
- fail(
14672
- command,
14673
- `Cannot write ${opts.out}: ${error instanceof Error ? error.message : String(error)}`,
14674
- EXIT_CODES.general,
14675
- { code: "local_file_error" }
14676
- );
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
+ );
14677
16202
  }
14678
- 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)
14679
16211
  fail(
14680
16212
  command,
14681
- `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.",
14682
16214
  EXIT_CODES.general,
14683
16215
  {
14684
- code: "local_file_error"
16216
+ reason: "export_count_mismatch",
16217
+ ...summary
14685
16218
  }
14686
16219
  );
14687
- }
14688
- const matches = totals ? written === totals.events : false;
14689
- const summary = {
14690
- events_written: written,
14691
- pages,
14692
- totals: totals ?? null,
14693
- count_matches: matches
14694
- };
14695
- if (!matches)
14696
- fail(
14697
- command,
14698
- "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.",
14699
- EXIT_CODES.general,
14700
- {
14701
- reason: "export_count_mismatch",
14702
- ...summary
14703
- }
14704
- );
14705
- if (summaryOnStdout) writeEnvelope(summary, "apex events export");
14706
- process.stderr.write(
14707
- `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}.
14708
16223
  `
14709
- );
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
+ }
14710
16245
  });
14711
16246
 
14712
16247
  // src/commands/goals.ts
@@ -14757,6 +16292,10 @@ function sdkPageviewGoalMatch(urlPattern, href) {
14757
16292
  var INVENTORY_PAGE_SIZE = 100;
14758
16293
  var GOALS_PAGE_SIZE = 100;
14759
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." }];
14760
16299
  var NON_QUANTIFIABLE = /* @__PURE__ */ new Set(["(", "|", "^", "$"]);
14761
16300
  function urlPatternDependence(pattern) {
14762
16301
  let fragment = false;
@@ -14899,6 +16438,58 @@ function parseCapture(command, value) {
14899
16438
  function cleanBody(input) {
14900
16439
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
14901
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
+ }
14902
16493
  function printGoal(goal) {
14903
16494
  console.log();
14904
16495
  console.log(source_default.bold(goal.name));
@@ -14911,6 +16502,7 @@ function printGoal(goal) {
14911
16502
  if (goal.text_match) console.log(` Text match: ${goal.text_match}`);
14912
16503
  if (goal.url_pattern) console.log(` URL: ${goal.url_pattern}`);
14913
16504
  if (goal.event_name) console.log(` Event: ${goal.event_name}`);
16505
+ if (goal.page_id) console.log(` Page: ${goal.page_id}`);
14914
16506
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
14915
16507
  console.log();
14916
16508
  }
@@ -14938,8 +16530,22 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
14938
16530
  );
14939
16531
  });
14940
16532
  });
14941
- 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) {
14942
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;
14943
16549
  const body5 = cleanBody({
14944
16550
  name: opts.name,
14945
16551
  type: opts.type,
@@ -14950,6 +16556,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
14950
16556
  text_match: opts.textMatch,
14951
16557
  url_pattern: opts.urlPattern,
14952
16558
  event_name: opts.eventName,
16559
+ page_id: pageId,
14953
16560
  count_once: opts.countOnce === true,
14954
16561
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
14955
16562
  });
@@ -14967,8 +16574,9 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
14967
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(
14968
16575
  "--value-property <property>",
14969
16576
  "Numeric event property for value goals; pass null to clear"
14970
- ).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) {
14971
16578
  const creds = requireCredentials();
16579
+ const pageId = opts.page === "null" ? null : typeof opts.page === "string" ? await resolveGoalPageId(this, creds, opts.page) : void 0;
14972
16580
  const body5 = cleanBody({
14973
16581
  name: opts.name,
14974
16582
  type: opts.type,
@@ -14979,6 +16587,7 @@ var update2 = new Command("update").description("Update a goal").argument("<id>"
14979
16587
  text_match: opts.textMatch,
14980
16588
  url_pattern: opts.urlPattern,
14981
16589
  event_name: opts.eventName,
16590
+ page_id: pageId,
14982
16591
  count_once: opts.countOnce,
14983
16592
  capture: parseCapture(this, typeof opts.capture === "string" ? opts.capture : void 0)
14984
16593
  });
@@ -16623,13 +18232,13 @@ var NAMED_HTML_ENTITIES = {
16623
18232
  nbsp: "\xA0"
16624
18233
  };
16625
18234
  function decodeHtmlEntities(value) {
16626
- return value.replace(/&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z]+);/g, (match2, entity) => {
16627
- if (entity.startsWith("#")) {
16628
- const hex = entity[1] === "x" || entity[1] === "X";
16629
- 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);
16630
18239
  return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : match2;
16631
18240
  }
16632
- return NAMED_HTML_ENTITIES[entity.toLowerCase()] ?? match2;
18241
+ return NAMED_HTML_ENTITIES[entity2.toLowerCase()] ?? match2;
16633
18242
  });
16634
18243
  }
16635
18244
  function printSettings(settings2) {
@@ -17399,9 +19008,6 @@ function resolveWorkerBase(options) {
17399
19008
  const raw = options.worker ?? process.env.DRIP_WORKER_URL ?? PRODUCTION_WORKER_BASE_URL;
17400
19009
  return raw.trim().replace(/\/+$/, "");
17401
19010
  }
17402
- function resolveShopId(options) {
17403
- return (options.shopId ?? process.env.DRIP_SHOP_ID ?? "").trim() || null;
17404
- }
17405
19011
  function servedConfigUrl(workerBase, shopId) {
17406
19012
  return `${workerBase}/config?shopId=${encodeURIComponent(shopId)}&env=${PRODUCTION_ENVIRONMENT_KEY}`;
17407
19013
  }
@@ -17574,7 +19180,8 @@ async function readGoals(creds) {
17574
19180
  return { goals };
17575
19181
  }
17576
19182
  async function runDoctor(options = {}) {
17577
- const creds = requireCredentials();
19183
+ const resolved = resolveCheckShop(options.shopId, process.env.DRIP_SHOP_ID);
19184
+ const { creds } = resolved;
17578
19185
  if (options.check === "purchase_capture") {
17579
19186
  const domainsRes2 = await getInstallDomains(creds);
17580
19187
  if (!domainsRes2.ok || !domainsRes2.data)
@@ -17584,7 +19191,9 @@ async function runDoctor(options = {}) {
17584
19191
  classifyStatus(domainsRes2.status),
17585
19192
  domainsRes2.errorDetails
17586
19193
  );
17587
- 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;
17588
19197
  const servedResult2 = shopId2 ? await fetchServedConfig(resolveWorkerBase(options), shopId2) : {};
17589
19198
  const readTracking = async (minutes) => {
17590
19199
  try {
@@ -17635,6 +19244,7 @@ async function runDoctor(options = {}) {
17635
19244
  apiFailureOf(res)
17636
19245
  );
17637
19246
  }
19247
+ assertResponseShop(domainsRes.data.shop_id, resolved.shopId, creds.credentialKind);
17638
19248
  for (const [label, res] of [
17639
19249
  ["url inventory", inventory],
17640
19250
  ["goals", goals]
@@ -17678,7 +19288,7 @@ async function runDoctor(options = {}) {
17678
19288
  withCaveat(devPollutionCheck(eventHosts, envsRes.data, unparsedRows), inventoryCaveat)
17679
19289
  );
17680
19290
  const primary = hostOnly(domainsRes.data.domain) ?? eventHosts[0];
17681
- const shopId = resolveShopId(options) ?? domainsRes.data.shop_id;
19291
+ const shopId = resolved.shopId ?? domainsRes.data.shop_id;
17682
19292
  const workerBase = resolveWorkerBase(options);
17683
19293
  const [page, servedResult] = await Promise.all([
17684
19294
  primary ? fetchPrimaryHtml(primary) : Promise.resolve({}),
@@ -17899,7 +19509,7 @@ function consentRateCheck(items) {
17899
19509
  }
17900
19510
  var doctorCommand = new Command("doctor").description("Check shop configuration against observed analytics").option(
17901
19511
  "--shop-id <uuid>",
17902
- "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)"
17903
19513
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
17904
19514
  "--worker <url>",
17905
19515
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
@@ -17908,7 +19518,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
17908
19518
  if (options.check && data.checks.length === 0)
17909
19519
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
17910
19520
  output(this, data, () => printDoctorReport(data.checks));
17911
- 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;
17912
19522
  });
17913
19523
  function printDoctorReport(checks) {
17914
19524
  printTable(
@@ -18092,7 +19702,7 @@ Examples:
18092
19702
  );
18093
19703
 
18094
19704
  // src/commands/install.ts
18095
- var import_promises4 = require("node:dns/promises");
19705
+ var import_promises3 = require("node:dns/promises");
18096
19706
  var import_node_http2 = require("node:http");
18097
19707
  var import_node_https = require("node:https");
18098
19708
  var import_node_net = require("node:net");
@@ -18150,7 +19760,7 @@ var testResolver;
18150
19760
  var testFetch;
18151
19761
  async function resolveHost(hostname) {
18152
19762
  const mocked = testResolver?.(hostname) ?? null;
18153
- const addresses = mocked ?? await (0, import_promises4.lookup)(hostname, { all: true });
19763
+ const addresses = mocked ?? await (0, import_promises3.lookup)(hostname, { all: true });
18154
19764
  return addresses.map((entry) => entry.address);
18155
19765
  }
18156
19766
  async function validateUrl(raw, allowedHosts) {
@@ -18630,16 +20240,16 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
18630
20240
  };
18631
20241
  }
18632
20242
  var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description(
18633
- "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"
18634
20244
  ).option("--url <url>", "Check one explicit bound storefront URL").option(
18635
20245
  "--shop-id <id>",
18636
- "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)"
18637
20247
  ).option(
18638
20248
  "--worker-url <url>",
18639
20249
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
18640
20250
  ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action65(opts) {
18641
- const creds = requireCredentials();
18642
- 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;
18643
20253
  if (!shopId)
18644
20254
  fail(
18645
20255
  this,
@@ -18647,6 +20257,10 @@ var installCommand = new Command("install").description("Check Apex storefront i
18647
20257
  EXIT_CODES.validation,
18648
20258
  { field: "shop-id" }
18649
20259
  );
20260
+ if (overriddenShopId)
20261
+ console.error(
20262
+ `Checking shop ${shopId} from --shop-id instead of the selected shop ${overriddenShopId}.`
20263
+ );
18650
20264
  const domains = await getInstallDomains(creds);
18651
20265
  if (!domains.ok || !domains.data)
18652
20266
  fail(
@@ -18655,6 +20269,7 @@ var installCommand = new Command("install").description("Check Apex storefront i
18655
20269
  classifyStatus(domains.status),
18656
20270
  domains.errorDetails
18657
20271
  );
20272
+ assertResponseShop(domains.data.shop_id, shopId, creds.credentialKind);
18658
20273
  const mockStoreOrigins = cliTestSetting("STORE_ORIGINS");
18659
20274
  const storeOrigins = cliTestSetting("API") === "1" && mockStoreOrigins ? mockStoreOrigins.split(",") : domains.data.storeOrigins;
18660
20275
  const origins = opts.url ? [opts.url] : storeOrigins;
@@ -18686,7 +20301,16 @@ var installCommand = new Command("install").description("Check Apex storefront i
18686
20301
  });
18687
20302
  continue;
18688
20303
  }
18689
- 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
+ }
18690
20314
  if (error instanceof BodyDecodeError) {
18691
20315
  pages.push({
18692
20316
  url,
@@ -18723,10 +20347,12 @@ var installCommand = new Command("install").description("Check Apex storefront i
18723
20347
  );
18724
20348
  let tracking;
18725
20349
  let trackingExitCode = null;
20350
+ let trackingApiFailure;
18726
20351
  try {
18727
20352
  const result = await verifyTracking(creds);
18728
20353
  if (!result.ok || !result.data) {
18729
20354
  trackingExitCode = classifyStatus(result.status);
20355
+ trackingApiFailure = apiFailureOf(result);
18730
20356
  tracking = {
18731
20357
  status: "FAIL",
18732
20358
  message: result.error ?? "Failed to verify tracking",
@@ -18750,6 +20376,11 @@ var installCommand = new Command("install").description("Check Apex storefront i
18750
20376
  if (servedConfig.status !== "PASS") warnings.push(servedConfig.message);
18751
20377
  if (tracking.status === "WARN") warnings.push(tracking.message);
18752
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
+ ];
18753
20384
  const data = { ok: !failed, pages, warnings, servedConfig, tracking };
18754
20385
  const renderReport = () => {
18755
20386
  printTable(
@@ -18781,12 +20412,24 @@ var installCommand = new Command("install").description("Check Apex storefront i
18781
20412
  };
18782
20413
  if (failed) {
18783
20414
  if (!shouldUseJson(this)) renderReport();
18784
- fail(
18785
- this,
18786
- "Apex install check found a definite failure",
18787
- trackingExitCode ?? EXIT_CODES.general,
18788
- data
18789
- );
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
+ });
18790
20433
  }
18791
20434
  output(this, data, renderReport);
18792
20435
  }).parent;
@@ -19383,60 +21026,312 @@ function detail4(x) {
19383
21026
  console.log(` State: ${x.status}`);
19384
21027
  console.log();
19385
21028
  }
19386
- var list9 = new Command("list").alias("ls").description("List personalizations").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
19387
- const r = await listManagedDeliveries(requireCredentials(), kind4, Number.parseInt(o.limit, 10));
19388
- if (!r.ok || !r.data)
19389
- fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
19390
- output(
19391
- this,
19392
- r.data,
19393
- () => printTable(
19394
- ["KEY/NAME", "STATE", "UPDATED"],
19395
- r.data.data.map((x) => [x.key || x.name, x.status, x.updated_at ?? "-"]),
19396
- [36, 12, 28]
19397
- )
19398
- );
19399
- });
19400
- var get8 = new Command("get").description("Get a personalization").argument("<id>").action(async function(id) {
19401
- const r = await getManagedDelivery(requireCredentials(), kind4, id);
19402
- if (!r.ok || !r.data)
19403
- fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
19404
- output(this, r.data, () => detail4(r.data));
19405
- });
19406
- function write4(name) {
19407
- const c = new Command(name).description(
19408
- `${name === "create" ? "Create" : "Update"} a personalization`
19409
- );
19410
- if (name === "update") c.argument("<id>");
19411
- c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").option("--key <key>").option("--name <name>").option("--description <description>");
19412
- c.action(async function(...args) {
19413
- const o = this.opts();
19414
- const id = name === "update" ? String(args[0]) : void 0;
19415
- const value = o.file ? `@${o.file}` : String(args[name === "update" ? 1 : 0] ?? "") || void 0;
19416
- const b = body2(this, value, o);
19417
- if (name === "update" && !Object.keys(b).length)
19418
- fail(this, "Provide at least one field to update.", EXIT_CODES.validation);
19419
- const r = name === "create" ? await createManagedDelivery(requireCredentials(), kind4, b) : await updateManagedDelivery(requireCredentials(), kind4, id, b);
19420
- if (!r.ok || !r.data)
19421
- fail(this, r.error ?? `Failed to ${name}`, classifyStatus(r.status), r.errorDetails);
19422
- output(this, r.data, () => detail4(r.data));
19423
- });
19424
- return c;
21029
+ var list9 = new Command("list").alias("ls").description("List personalizations").option("-n, --limit <n>", "Max results", "50").action(async function(o) {
21030
+ const r = await listManagedDeliveries(requireCredentials(), kind4, Number.parseInt(o.limit, 10));
21031
+ if (!r.ok || !r.data)
21032
+ fail(this, r.error ?? "Failed to list", classifyStatus(r.status), r.errorDetails);
21033
+ output(
21034
+ this,
21035
+ r.data,
21036
+ () => printTable(
21037
+ ["KEY/NAME", "STATE", "UPDATED"],
21038
+ r.data.data.map((x) => [x.key || x.name, x.status, x.updated_at ?? "-"]),
21039
+ [36, 12, 28]
21040
+ )
21041
+ );
21042
+ });
21043
+ var get8 = new Command("get").description("Get a personalization").argument("<id>").action(async function(id) {
21044
+ const r = await getManagedDelivery(requireCredentials(), kind4, id);
21045
+ if (!r.ok || !r.data)
21046
+ fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
21047
+ output(this, r.data, () => detail4(r.data));
21048
+ });
21049
+ function write4(name) {
21050
+ const c = new Command(name).description(
21051
+ `${name === "create" ? "Create" : "Update"} a personalization`
21052
+ );
21053
+ if (name === "update") c.argument("<id>");
21054
+ c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").option("--key <key>").option("--name <name>").option("--description <description>");
21055
+ c.action(async function(...args) {
21056
+ const o = this.opts();
21057
+ const id = name === "update" ? String(args[0]) : void 0;
21058
+ const value = o.file ? `@${o.file}` : String(args[name === "update" ? 1 : 0] ?? "") || void 0;
21059
+ const b = body2(this, value, o);
21060
+ if (name === "update" && !Object.keys(b).length)
21061
+ fail(this, "Provide at least one field to update.", EXIT_CODES.validation);
21062
+ const r = name === "create" ? await createManagedDelivery(requireCredentials(), kind4, b) : await updateManagedDelivery(requireCredentials(), kind4, id, b);
21063
+ if (!r.ok || !r.data)
21064
+ fail(this, r.error ?? `Failed to ${name}`, classifyStatus(r.status), r.errorDetails);
21065
+ output(this, r.data, () => detail4(r.data));
21066
+ });
21067
+ return c;
21068
+ }
21069
+ function lifecycle2(action78) {
21070
+ return new Command(action78).description(`${action78[0].toUpperCase()}${action78.slice(1)} a personalization`).argument("<id>").action(async function(id) {
21071
+ const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action78);
21072
+ if (!r.ok || !r.data)
21073
+ fail(this, r.error ?? `Failed to ${action78}`, classifyStatus(r.status), r.errorDetails);
21074
+ output(this, r.data, () => detail4(r.data));
21075
+ });
21076
+ }
21077
+ var personalizationsCommand = new Command("personalizations").description("Manage personalizations").addCommand(list9).addCommand(get8).addCommand(write4("create")).addCommand(write4("update")).addCommand(lifecycle2("activate")).addCommand(lifecycle2("pause")).addCommand(lifecycle2("archive"));
21078
+
21079
+ // src/commands/targeting-resources.ts
21080
+ init_source();
21081
+ var import_node_fs23 = __toESM(require("node:fs"));
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
+ };
19425
21290
  }
19426
- function lifecycle2(action78) {
19427
- return new Command(action78).description(`${action78[0].toUpperCase()}${action78.slice(1)} a personalization`).argument("<id>").action(async function(id) {
19428
- const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action78);
19429
- if (!r.ok || !r.data)
19430
- fail(this, r.error ?? `Failed to ${action78}`, classifyStatus(r.status), r.errorDetails);
19431
- output(this, r.data, () => detail4(r.data));
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
+ });
19432
21308
  });
19433
21309
  }
19434
- var personalizationsCommand = new Command("personalizations").description("Manage personalizations").addCommand(list9).addCommand(get8).addCommand(write4("create")).addCommand(write4("update")).addCommand(lifecycle2("activate")).addCommand(lifecycle2("pause")).addCommand(lifecycle2("archive"));
19435
21310
 
19436
21311
  // src/commands/targeting-resources.ts
19437
- init_source();
19438
- var import_node_fs23 = __toESM(require("node:fs"));
19439
- var import_node_path20 = __toESM(require("node:path"));
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
+ }
19440
21335
  function body3(command, value) {
19441
21336
  if (!value) return {};
19442
21337
  const source = value.startsWith("@") ? value.slice(1) : value;
@@ -19514,7 +21409,10 @@ function targetingResourceCommand(kind5) {
19514
21409
  `${action78 === "create" ? "Create" : "Update"} a ${singular3}`
19515
21410
  );
19516
21411
  if (action78 === "update") command.argument("<id>");
19517
- 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) {
19518
21416
  const opts = this.opts();
19519
21417
  const id = action78 === "update" ? String(args[0]) : void 0;
19520
21418
  const value = opts.file ? `@${opts.file}` : String(args[action78 === "update" ? 1 : 0] ?? "") || void 0;
@@ -19533,10 +21431,11 @@ function targetingResourceCommand(kind5) {
19533
21431
  this,
19534
21432
  response.error ?? `Failed to ${action78}`,
19535
21433
  classifyStatus(response.status),
19536
- response.errorDetails
21434
+ exclusionFailureDetails(kind5, action78, response.status, response.errorDetails)
19537
21435
  );
19538
21436
  output(this, response.data, () => console.log(source_default.green(`${action78}d ${singular3}`)));
19539
21437
  });
21438
+ if (kind5 === "exclusion-groups") withBodyHelp(command, action78);
19540
21439
  root.addCommand(command);
19541
21440
  }
19542
21441
  const deleteCommand = new Command("delete").description(`Delete a ${singular3}`).argument("<id>");
@@ -19575,9 +21474,9 @@ function targetingResourceCommand(kind5) {
19575
21474
  output(this, response.data ?? null, () => console.log(source_default.green(`Deleted ${singular3}`)));
19576
21475
  })
19577
21476
  );
19578
- if (kind5 === "exclusion-groups")
21477
+ if (kind5 === "exclusion-groups") {
19579
21478
  root.addCommand(
19580
- 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) {
19581
21480
  const response = await requestTargetingResource(
19582
21481
  requireCredentials(),
19583
21482
  "PUT",
@@ -19590,11 +21489,18 @@ function targetingResourceCommand(kind5) {
19590
21489
  this,
19591
21490
  response.error ?? "Failed to replace members",
19592
21491
  classifyStatus(response.status),
19593
- response.errorDetails
21492
+ exclusionFailureDetails(
21493
+ kind5,
21494
+ "replace-members",
21495
+ response.status,
21496
+ response.errorDetails
21497
+ )
19594
21498
  );
19595
21499
  output(this, response.data, () => console.log(source_default.green("Replaced members")));
19596
21500
  })
19597
21501
  );
21502
+ withBodyHelp(root.commands.at(-1), "replace-members");
21503
+ }
19598
21504
  return root;
19599
21505
  }
19600
21506
  var pagesCommand = targetingResourceCommand("pages");
@@ -19683,7 +21589,7 @@ var test2 = new Command("test").description("Evaluate experiment URL targeting f
19683
21589
  var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
19684
21590
 
19685
21591
  // src/commands/sos.ts
19686
- var import_node_crypto7 = require("node:crypto");
21592
+ var import_node_crypto8 = require("node:crypto");
19687
21593
  init_source();
19688
21594
  var import_node_fs24 = __toESM(require("node:fs"));
19689
21595
  var import_node_path21 = __toESM(require("node:path"));
@@ -19794,7 +21700,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
19794
21700
  const bytes = file.bytes;
19795
21701
  await uploadSosAttachment(grant.signed_url, file.content_type, bytes);
19796
21702
  attachmentPaths.push(grant.path);
19797
- 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"));
19798
21704
  }
19799
21705
  }
19800
21706
  const description = readText(opts.description) ?? descriptionArg ?? `Apex CLI ${opts.type} SOS request${opts.loom ? ` with Loom evidence: ${opts.loom}` : "."}`;
@@ -20107,54 +22013,6 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
20107
22013
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
20108
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);
20109
22015
 
20110
- // src/commands/schema.ts
20111
- function optionSchema(option) {
20112
- return {
20113
- flags: option.flags,
20114
- ...option.argChoices ? { choices: option.argChoices } : {},
20115
- description: option.description,
20116
- required: option.required,
20117
- optional: option.optional,
20118
- mandatory: Boolean(option.mandatory),
20119
- variadic: option.variadic,
20120
- ...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
20121
- };
20122
- }
20123
- function commandSchema(command) {
20124
- return {
20125
- name: command.name(),
20126
- aliases: command.aliases(),
20127
- description: command.description(),
20128
- usage: command.usage(),
20129
- arguments: command.registeredArguments.map((arg) => ({
20130
- name: arg.name(),
20131
- required: arg.required,
20132
- variadic: arg.variadic,
20133
- description: arg.description,
20134
- ...arg.defaultValue === void 0 ? {} : { defaultValue: arg.defaultValue }
20135
- })),
20136
- options: command.options.map(optionSchema),
20137
- commands: command.commands.map(commandSchema)
20138
- };
20139
- }
20140
- function createSchemaCommand(program3) {
20141
- return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
20142
- const schema = {
20143
- schemaVersion: 2,
20144
- contract: {
20145
- optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
20146
- mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
20147
- requiredArguments: "An argument's required field means the positional is mandatory."
20148
- },
20149
- ...commandSchema(program3)
20150
- };
20151
- output(this, schema, () => {
20152
- process.stdout.write(`${JSON.stringify(schema, null, 2)}
20153
- `);
20154
- });
20155
- });
20156
- }
20157
-
20158
22016
  // src/lib/gtm-miner.ts
20159
22017
  function extractContainerReferences(html) {
20160
22018
  const results2 = /* @__PURE__ */ new Map();
@@ -20787,8 +22645,15 @@ var skillsCommand = new Command("skills").description("Print or install the Apex
20787
22645
  });
20788
22646
  });
20789
22647
 
22648
+ // ../../ops/org-split/move-plan.mjs
22649
+ var uuid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
22650
+ function validateMoveInput(shopIds, toOrgId) {
22651
+ if (typeof toOrgId !== "string" || !uuid.test(toOrgId) || !Array.isArray(shopIds) || !shopIds.length || shopIds.some((id) => typeof id !== "string" || !uuid.test(id))) throw new Error("Choose valid store and destination UUIDs");
22652
+ return { shopIds: [...new Set(shopIds.map((id) => id.toLowerCase()))].sort(), toOrgId: toOrgId.toLowerCase() };
22653
+ }
22654
+
20790
22655
  // src/commands/admin.ts
20791
- var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22656
+ var uuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
20792
22657
  function switchValue(value) {
20793
22658
  if (value === "on") return true;
20794
22659
  if (value === "off") return false;
@@ -20915,7 +22780,7 @@ var create6 = new Command("create").description("Create an internal, demo or dog
20915
22780
  fail(this, "--name must be 1 to 60 characters", 3);
20916
22781
  if (opts.reason.trim().length < 8 || opts.reason.trim().length > 500)
20917
22782
  fail(this, "--reason must be 8 to 500 characters", 3);
20918
- if (opts.claimShop && !uuid.test(opts.claimShop)) fail(this, "--claim-shop must be a UUID", 3);
22783
+ if (opts.claimShop && !uuid2.test(opts.claimShop)) fail(this, "--claim-shop must be a UUID", 3);
20919
22784
  if (opts.claimShop && (opts.domain || opts.platform))
20920
22785
  fail(this, "Use --claim-shop or --domain with --platform", 3);
20921
22786
  if (!!opts.domain !== !!opts.platform)
@@ -20936,7 +22801,7 @@ var create6 = new Command("create").description("Create an internal, demo or dog
20936
22801
  fail(this, "--owner requires a store for the invitation", 3);
20937
22802
  if (opts.plan !== "self_managed" && (!opts.consultant || !opts.consultantTitle?.trim()))
20938
22803
  fail(this, "Managed plans require --consultant and --consultant-title", 3);
20939
- if (opts.consultant && !uuid.test(opts.consultant))
22804
+ if (opts.consultant && !uuid2.test(opts.consultant))
20940
22805
  fail(this, "--consultant must be a platform access UUID", 3);
20941
22806
  if (!!opts.consultant !== !!opts.consultantTitle?.trim())
20942
22807
  fail(this, "Provide --consultant and --consultant-title together", 3);
@@ -21001,7 +22866,7 @@ var create6 = new Command("create").description("Create an internal, demo or dog
21001
22866
  });
21002
22867
  });
21003
22868
  var setCategory = new Command("set-category").description("Change an existing account's category with an audit reason").argument("<org-id>", "Organization UUID").requiredOption("--category <category>", "internal, demo, dogfood or customer").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm category change").option("--json", "Output JSON").action(async function(orgId, opts) {
21004
- if (!uuid.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
22869
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
21005
22870
  if (!["internal", "demo", "dogfood", "customer"].includes(opts.category))
21006
22871
  fail(this, "--category must be internal, demo, dogfood or customer", 3);
21007
22872
  const reason2 = opts.reason.trim();
@@ -21032,14 +22897,137 @@ var setCategory = new Command("set-category").description("Change an existing ac
21032
22897
  printTable(["RESULT"], [[`Customer ${result.customerId} moved to ${result.category}`]]);
21033
22898
  });
21034
22899
  });
21035
- var customers = new Command("customers").description("Create engineering accounts and manage existing account categories").addCommand(create6).addCommand(setCategory);
22900
+ var setManagedBy = new Command("set-managed-by").description("Assign or clear a managing agency with an audit reason").argument("<org-id>", "Organization UUID").option("--by <org-id>", "Managing agency UUID").option("--clear", "Clear the managing agency").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm manager change").option("--json", "Output JSON").action(async function(orgId, opts) {
22901
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
22902
+ if (!!opts.by === !!opts.clear) fail(this, "Use exactly one of --by or --clear", 3);
22903
+ if (opts.by && !uuid2.test(opts.by)) fail(this, "--by must be a UUID", 3);
22904
+ const reason2 = opts.reason.trim();
22905
+ if (reason2.length < 8 || reason2.length > 500)
22906
+ fail(this, "--reason must be 8 to 500 characters", 3);
22907
+ if (!opts.confirm) {
22908
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
22909
+ confirmationRequired(
22910
+ this,
22911
+ "--confirm is required to change a managing agency",
22912
+ `apex admin customers set-managed-by ${orgId} ${opts.by ? `--by ${opts.by}` : "--clear"} --reason ${quote(reason2)} --confirm`
22913
+ );
22914
+ }
22915
+ const res = await setAdminCustomerManagedBy(requireUserCredentials(), orgId, {
22916
+ managed_by_org_id: opts.by ?? null,
22917
+ reason: reason2
22918
+ });
22919
+ if (!res.ok || !res.data)
22920
+ fail(
22921
+ this,
22922
+ res.error ?? "Failed to change managing agency",
22923
+ classifyStatus(res.status),
22924
+ res.errorDetails
22925
+ );
22926
+ output(
22927
+ this,
22928
+ res.data,
22929
+ () => printTable(
22930
+ ["CUSTOMER", "MANAGING AGENCY"],
22931
+ [[res.data.data.customerId, res.data.data.managed_by_org_id ?? "None"]]
22932
+ )
22933
+ );
22934
+ });
22935
+ var setWorkspaceType = new Command("set-workspace-type").description("Change a workspace type with an audit reason").argument("<org-id>", "Organization UUID").requiredOption("--type <company|agency>", "Workspace type").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm workspace type change").option("--json", "Output JSON").action(async function(orgId, opts) {
22936
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
22937
+ if (opts.type !== "company" && opts.type !== "agency")
22938
+ fail(this, "--type must be company or agency", 3);
22939
+ const reason2 = opts.reason.trim();
22940
+ if (reason2.length < 8 || reason2.length > 500)
22941
+ fail(this, "--reason must be 8 to 500 characters", 3);
22942
+ if (!opts.confirm) {
22943
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
22944
+ confirmationRequired(
22945
+ this,
22946
+ "--confirm is required to change a workspace type",
22947
+ `apex admin customers set-workspace-type ${orgId} --type ${opts.type} --reason ${quote(reason2)} --confirm`
22948
+ );
22949
+ }
22950
+ const res = await setAdminCustomerWorkspaceType(requireUserCredentials(), orgId, {
22951
+ workspace_type: opts.type,
22952
+ reason: reason2
22953
+ });
22954
+ if (!res.ok || !res.data)
22955
+ fail(
22956
+ this,
22957
+ res.error ?? "Failed to change workspace type",
22958
+ classifyStatus(res.status),
22959
+ res.errorDetails
22960
+ );
22961
+ output(
22962
+ this,
22963
+ res.data,
22964
+ () => printTable(
22965
+ ["CUSTOMER", "WORKSPACE TYPE"],
22966
+ [[res.data.data.customerId, res.data.data.workspace_type]]
22967
+ )
22968
+ );
22969
+ });
22970
+ function storeMoveRequest(opts) {
22971
+ const { shopIds, toOrgId } = validateMoveInput(opts.shop, opts.to);
22972
+ const reason2 = opts.reason.trim();
22973
+ if (reason2.length < 8 || reason2.length > 500)
22974
+ throw new Error("--reason must be 8 to 500 characters");
22975
+ return {
22976
+ toOrgId,
22977
+ body: {
22978
+ shop_ids: shopIds,
22979
+ reason: reason2,
22980
+ keep_grants: !!opts.keepGrants,
22981
+ dry_run: !opts.confirm
22982
+ }
22983
+ };
22984
+ }
22985
+ var moveStores = new Command("move-stores").description("Preview or confirm an audited set of store moves").requiredOption("--to <org-id>", "Destination organization UUID").requiredOption(
22986
+ "--shop <shop-id>",
22987
+ "Store UUID, repeat for each store",
22988
+ (value, previous) => [...previous, value],
22989
+ []
22990
+ ).requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--keep-grants", "Preserve explicit store roles and overrides").option("--confirm", "Perform the move; otherwise return a read-only plan").option("--json", "Output JSON").action(async function(opts) {
22991
+ let request2;
22992
+ try {
22993
+ request2 = storeMoveRequest(opts);
22994
+ } catch (error) {
22995
+ fail(this, error instanceof Error ? error.message : "Invalid move request", 3);
22996
+ }
22997
+ const res = await moveAdminCustomerStores(
22998
+ requireUserCredentials(),
22999
+ request2.toOrgId,
23000
+ request2.body
23001
+ );
23002
+ if (!res.ok || !res.data)
23003
+ fail(
23004
+ this,
23005
+ res.error ?? "Failed to move stores",
23006
+ classifyStatus(res.status),
23007
+ res.errorDetails
23008
+ );
23009
+ output(this, res.data, () => {
23010
+ printTable(
23011
+ ["FIELD", "VALUE"],
23012
+ Object.entries(res.data.data).map(([key, value]) => [key, JSON.stringify(value)])
23013
+ );
23014
+ });
23015
+ });
23016
+ var customers = new Command("customers").description("Create engineering accounts and manage existing account categories").addCommand(create6).addCommand(setCategory).addCommand(setManagedBy).addCommand(setWorkspaceType).addCommand(moveStores);
21036
23017
  var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops).addCommand(customers);
21037
23018
 
21038
23019
  // src/commands/membership.ts
21039
23020
  init_source();
21040
23021
  async function run2(c, p, label) {
21041
23022
  const r = await p;
21042
- if (!r.ok) fail(c, r.error ?? `Failed to ${label}`, classifyStatus(r.status), r.errorDetails);
23023
+ if (!r.ok)
23024
+ fail(
23025
+ c,
23026
+ r.error ?? `Failed to ${label}`,
23027
+ classifyStatus(r.status),
23028
+ r.errorDetails,
23029
+ apiFailureOf(r)
23030
+ );
21043
23031
  output(c, r.data ?? {}, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
21044
23032
  `));
21045
23033
  }
@@ -21059,7 +23047,7 @@ async function confirm(c, force, label, retry) {
21059
23047
  }
21060
23048
  return true;
21061
23049
  }
21062
- var orgCommand = new Command("org").description("Manage the current organization");
23050
+ var orgCommand = new Command("org").alias("orgs").description("Manage the current organization");
21063
23051
  orgCommand.command("get").action(async function() {
21064
23052
  await run2(this, getOrg(requireUserCredentials()), "get organization");
21065
23053
  });
@@ -21075,7 +23063,27 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
21075
23063
  );
21076
23064
  });
21077
23065
  orgCommand.command("members").action(async function() {
21078
- await run2(this, listOrgMembers(requireUserCredentials()), "list members");
23066
+ const r = await listOrgMembers(requireUserCredentials());
23067
+ if (!r.ok)
23068
+ fail(
23069
+ this,
23070
+ r.error ?? "Failed to list members",
23071
+ classifyStatus(r.status),
23072
+ r.errorDetails,
23073
+ apiFailureOf(r)
23074
+ );
23075
+ const rows = r.data?.data ?? [];
23076
+ output(this, r.data ?? {}, () => {
23077
+ printTable(
23078
+ ["ID", "MEMBER", "ROLE", "SUSPENDED"],
23079
+ rows.map((row) => [
23080
+ String(row.id),
23081
+ String(row.email ?? row.name ?? "-"),
23082
+ String(row.role),
23083
+ String(row.suspended_at ?? "-")
23084
+ ])
23085
+ );
23086
+ });
21079
23087
  });
21080
23088
  orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
21081
23089
  new Option(
@@ -21112,6 +23120,26 @@ orgCommand.command("set-role").argument("<memberId>").argument("<role>").option(
21112
23120
  "set role"
21113
23121
  );
21114
23122
  });
23123
+ orgCommand.command("suspend-member").argument("<memberId>").option("--reason <text>").option("-f, --force").action(async function(id, o) {
23124
+ if (!await confirm(
23125
+ this,
23126
+ o.force,
23127
+ `Suspend organization member ${id}`,
23128
+ `apex org suspend-member ${id} --force`
23129
+ ))
23130
+ return;
23131
+ await run2(this, suspendOrgMember(requireUserCredentials(), id, o.reason), "suspend member");
23132
+ });
23133
+ orgCommand.command("reinstate-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
23134
+ if (!await confirm(
23135
+ this,
23136
+ o.force,
23137
+ `Reinstate organization member ${id}`,
23138
+ `apex org reinstate-member ${id} --force`
23139
+ ))
23140
+ return;
23141
+ await run2(this, reinstateOrgMember(requireUserCredentials(), id), "reinstate member");
23142
+ });
21115
23143
  orgCommand.command("remove-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
21116
23144
  if (!await confirm(
21117
23145
  this,
@@ -21122,23 +23150,38 @@ orgCommand.command("remove-member").argument("<memberId>").option("-f, --force")
21122
23150
  return;
21123
23151
  await run2(this, removeOrgMember(requireUserCredentials(), id), "remove member");
21124
23152
  });
21125
- var teamCommand = new Command("team").description("Manage a shop team");
21126
- 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);
21127
23168
  await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
21128
23169
  });
21129
- teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
23170
+ teamCommand.command("invite").argument("<email>").option("--shop <id>", teamShopDescription).addOption(
21130
23171
  new Option(
21131
23172
  "--role <role>",
21132
23173
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
21133
23174
  ).choices(["editor", "client_viewer"]).makeOptionMandatory()
21134
23175
  ).action(async function(email, o) {
23176
+ o.shop = teamShop(this, o.shop);
21135
23177
  await run2(
21136
23178
  this,
21137
23179
  inviteTeam(requireUserCredentials(), email, o.shop, o.role),
21138
23180
  "invite team member"
21139
23181
  );
21140
23182
  });
21141
- 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);
21142
23185
  await run2(
21143
23186
  this,
21144
23187
  revokeTeamInvite(requireUserCredentials(), id, o.shop),
@@ -21150,10 +23193,12 @@ teamCommand.command("set-role").argument("<memberId>").addArgument(
21150
23193
  "<role>",
21151
23194
  "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
21152
23195
  ).choices(["editor", "client_viewer"])
21153
- ).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);
21154
23198
  await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
21155
23199
  });
21156
- 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);
21157
23202
  if (!await confirm(
21158
23203
  this,
21159
23204
  o.force,
@@ -21164,6 +23209,38 @@ teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>
21164
23209
  await run2(this, removeTeamMember(requireUserCredentials(), id, o.shop), "remove member");
21165
23210
  });
21166
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
+
21167
23244
  // src/commands/account.ts
21168
23245
  function userCredentials2() {
21169
23246
  return requireUserCredentials();
@@ -21769,7 +23846,7 @@ var import_node_fs29 = __toESM(require("node:fs"));
21769
23846
  var import_node_path26 = __toESM(require("node:path"));
21770
23847
 
21771
23848
  // src/commands/landing-pages.ts
21772
- var import_node_crypto8 = __toESM(require("node:crypto"));
23849
+ var import_node_crypto9 = __toESM(require("node:crypto"));
21773
23850
  var import_node_fs28 = __toESM(require("node:fs"));
21774
23851
  var import_node_path25 = __toESM(require("node:path"));
21775
23852
  function apiFailure(command, response, fallback2) {
@@ -21783,7 +23860,7 @@ function apiFailure(command, response, fallback2) {
21783
23860
  }
21784
23861
  }
21785
23862
  function sha256(body5) {
21786
- return import_node_crypto8.default.createHash("sha256").update(body5).digest("hex");
23863
+ return import_node_crypto9.default.createHash("sha256").update(body5).digest("hex");
21787
23864
  }
21788
23865
  function markerPath(folder) {
21789
23866
  return import_node_path25.default.join(folder, LANDING_PAGE_CHECKOUT_MARKER);
@@ -22532,7 +24609,10 @@ function addJsonOption(command) {
22532
24609
  if (!command.options.some((option) => option.long === "--shop"))
22533
24610
  command.option("--shop <id>", "Use a shop for this invocation");
22534
24611
  }
22535
- command.exitOverride((err) => interceptCliError(err, command));
24612
+ command.exitOverride((err) => {
24613
+ recordCommanderExit(err, command);
24614
+ interceptCliError(err, command);
24615
+ });
22536
24616
  for (const child of command.commands) addJsonOption(child);
22537
24617
  }
22538
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);
@@ -22588,7 +24668,9 @@ program2.addCommand(skillsCommand);
22588
24668
  program2.addCommand(adminCommand, { hidden: true });
22589
24669
  program2.addCommand(orgCommand);
22590
24670
  program2.addCommand(teamCommand);
24671
+ program2.addCommand(upgradeCommand);
22591
24672
  addJsonOption(program2);
24673
+ registerTelemetryProgram(program2);
22592
24674
  var optionTerminatorIndex = process.argv.indexOf("--");
22593
24675
  var optionArgv = optionTerminatorIndex === -1 ? process.argv : process.argv.slice(0, optionTerminatorIndex);
22594
24676
  if (optionArgv.includes("--json")) program2.setOptionValue("json", true);