@drip-apex/apex-cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +50 -1
  2. package/dist/cli.js +689 -1991
  3. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -1159,8 +1159,8 @@ var require_command = __commonJS({
1159
1159
  "node_modules/commander/lib/command.js"(exports2) {
1160
1160
  var EventEmitter = require("node:events").EventEmitter;
1161
1161
  var childProcess2 = require("node:child_process");
1162
- var path20 = require("node:path");
1163
- var fs24 = require("node:fs");
1162
+ var path21 = require("node:path");
1163
+ var fs25 = require("node:fs");
1164
1164
  var process16 = require("node:process");
1165
1165
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1166
1166
  var { CommanderError: CommanderError2 } = require_error();
@@ -2141,7 +2141,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2141
2141
  * @param {string} subcommandName
2142
2142
  */
2143
2143
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2144
- if (fs24.existsSync(executableFile)) return;
2144
+ if (fs25.existsSync(executableFile)) return;
2145
2145
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2146
2146
  const executableMissing = `'${executableFile}' does not exist
2147
2147
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2159,11 +2159,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2159
2159
  let launchWithNode = false;
2160
2160
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2161
2161
  function findFile(baseDir, baseName) {
2162
- const localBin = path20.resolve(baseDir, baseName);
2163
- if (fs24.existsSync(localBin)) return localBin;
2164
- if (sourceExt.includes(path20.extname(baseName))) return void 0;
2162
+ const localBin = path21.resolve(baseDir, baseName);
2163
+ if (fs25.existsSync(localBin)) return localBin;
2164
+ if (sourceExt.includes(path21.extname(baseName))) return void 0;
2165
2165
  const foundExt = sourceExt.find(
2166
- (ext) => fs24.existsSync(`${localBin}${ext}`)
2166
+ (ext) => fs25.existsSync(`${localBin}${ext}`)
2167
2167
  );
2168
2168
  if (foundExt) return `${localBin}${foundExt}`;
2169
2169
  return void 0;
@@ -2175,21 +2175,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2175
2175
  if (this._scriptPath) {
2176
2176
  let resolvedScriptPath;
2177
2177
  try {
2178
- resolvedScriptPath = fs24.realpathSync(this._scriptPath);
2178
+ resolvedScriptPath = fs25.realpathSync(this._scriptPath);
2179
2179
  } catch {
2180
2180
  resolvedScriptPath = this._scriptPath;
2181
2181
  }
2182
- executableDir = path20.resolve(
2183
- path20.dirname(resolvedScriptPath),
2182
+ executableDir = path21.resolve(
2183
+ path21.dirname(resolvedScriptPath),
2184
2184
  executableDir
2185
2185
  );
2186
2186
  }
2187
2187
  if (executableDir) {
2188
2188
  let localFile = findFile(executableDir, executableFile);
2189
2189
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2190
- const legacyName = path20.basename(
2190
+ const legacyName = path21.basename(
2191
2191
  this._scriptPath,
2192
- path20.extname(this._scriptPath)
2192
+ path21.extname(this._scriptPath)
2193
2193
  );
2194
2194
  if (legacyName !== this._name) {
2195
2195
  localFile = findFile(
@@ -2200,7 +2200,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2200
2200
  }
2201
2201
  executableFile = localFile || executableFile;
2202
2202
  }
2203
- launchWithNode = sourceExt.includes(path20.extname(executableFile));
2203
+ launchWithNode = sourceExt.includes(path21.extname(executableFile));
2204
2204
  let proc;
2205
2205
  if (process16.platform !== "win32") {
2206
2206
  if (launchWithNode) {
@@ -3047,7 +3047,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3047
3047
  * @return {Command}
3048
3048
  */
3049
3049
  nameFromFilename(filename) {
3050
- this._name = path20.basename(filename, path20.extname(filename));
3050
+ this._name = path21.basename(filename, path21.extname(filename));
3051
3051
  return this;
3052
3052
  }
3053
3053
  /**
@@ -3061,9 +3061,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3061
3061
  * @param {string} [path]
3062
3062
  * @return {(string|null|Command)}
3063
3063
  */
3064
- executableDir(path21) {
3065
- if (path21 === void 0) return this._executableDir;
3066
- this._executableDir = path21;
3064
+ executableDir(path22) {
3065
+ if (path22 === void 0) return this._executableDir;
3066
+ this._executableDir = path22;
3067
3067
  return this;
3068
3068
  }
3069
3069
  /**
@@ -3854,7 +3854,7 @@ var init_source = __esm({
3854
3854
  // node_modules/is-docker/index.js
3855
3855
  function hasDockerEnv() {
3856
3856
  try {
3857
- import_node_fs2.default.statSync("/.dockerenv");
3857
+ import_node_fs3.default.statSync("/.dockerenv");
3858
3858
  return true;
3859
3859
  } catch {
3860
3860
  return false;
@@ -3862,7 +3862,7 @@ function hasDockerEnv() {
3862
3862
  }
3863
3863
  function hasDockerCGroup() {
3864
3864
  try {
3865
- return import_node_fs2.default.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
3865
+ return import_node_fs3.default.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
3866
3866
  } catch {
3867
3867
  return false;
3868
3868
  }
@@ -3873,10 +3873,10 @@ function isDocker() {
3873
3873
  }
3874
3874
  return isDockerCached;
3875
3875
  }
3876
- var import_node_fs2, isDockerCached;
3876
+ var import_node_fs3, isDockerCached;
3877
3877
  var init_is_docker = __esm({
3878
3878
  "node_modules/is-docker/index.js"() {
3879
- import_node_fs2 = __toESM(require("node:fs"), 1);
3879
+ import_node_fs3 = __toESM(require("node:fs"), 1);
3880
3880
  }
3881
3881
  });
3882
3882
 
@@ -3887,14 +3887,14 @@ function isInsideContainer() {
3887
3887
  }
3888
3888
  return cachedResult;
3889
3889
  }
3890
- var import_node_fs3, cachedResult, hasContainerEnv;
3890
+ var import_node_fs4, cachedResult, hasContainerEnv;
3891
3891
  var init_is_inside_container = __esm({
3892
3892
  "node_modules/is-inside-container/index.js"() {
3893
- import_node_fs3 = __toESM(require("node:fs"), 1);
3893
+ import_node_fs4 = __toESM(require("node:fs"), 1);
3894
3894
  init_is_docker();
3895
3895
  hasContainerEnv = () => {
3896
3896
  try {
3897
- import_node_fs3.default.statSync("/run/.containerenv");
3897
+ import_node_fs4.default.statSync("/run/.containerenv");
3898
3898
  return true;
3899
3899
  } catch {
3900
3900
  return false;
@@ -3904,25 +3904,25 @@ var init_is_inside_container = __esm({
3904
3904
  });
3905
3905
 
3906
3906
  // node_modules/is-wsl/index.js
3907
- var import_node_process2, import_node_os3, import_node_fs4, isWsl, is_wsl_default;
3907
+ var import_node_process2, import_node_os4, import_node_fs5, isWsl, is_wsl_default;
3908
3908
  var init_is_wsl = __esm({
3909
3909
  "node_modules/is-wsl/index.js"() {
3910
3910
  import_node_process2 = __toESM(require("node:process"), 1);
3911
- import_node_os3 = __toESM(require("node:os"), 1);
3912
- import_node_fs4 = __toESM(require("node:fs"), 1);
3911
+ import_node_os4 = __toESM(require("node:os"), 1);
3912
+ import_node_fs5 = __toESM(require("node:fs"), 1);
3913
3913
  init_is_inside_container();
3914
3914
  isWsl = () => {
3915
3915
  if (import_node_process2.default.platform !== "linux") {
3916
3916
  return false;
3917
3917
  }
3918
- if (import_node_os3.default.release().toLowerCase().includes("microsoft")) {
3918
+ if (import_node_os4.default.release().toLowerCase().includes("microsoft")) {
3919
3919
  if (isInsideContainer()) {
3920
3920
  return false;
3921
3921
  }
3922
3922
  return true;
3923
3923
  }
3924
3924
  try {
3925
- return import_node_fs4.default.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isInsideContainer() : false;
3925
+ return import_node_fs5.default.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft") ? !isInsideContainer() : false;
3926
3926
  } catch {
3927
3927
  return false;
3928
3928
  }
@@ -4191,12 +4191,12 @@ function detectPlatformBinary({ [platform]: platformBinary }, { wsl }) {
4191
4191
  }
4192
4192
  return detectArchBinary(platformBinary);
4193
4193
  }
4194
- var import_node_process7, import_node_buffer, import_node_path2, 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;
4194
+ var import_node_process7, import_node_buffer, import_node_path3, 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;
4195
4195
  var init_open = __esm({
4196
4196
  "node_modules/open/index.js"() {
4197
4197
  import_node_process7 = __toESM(require("node:process"), 1);
4198
4198
  import_node_buffer = require("node:buffer");
4199
- import_node_path2 = __toESM(require("node:path"), 1);
4199
+ import_node_path3 = __toESM(require("node:path"), 1);
4200
4200
  import_node_url = require("node:url");
4201
4201
  import_node_util5 = require("node:util");
4202
4202
  import_node_child_process5 = __toESM(require("node:child_process"), 1);
@@ -4207,8 +4207,8 @@ var init_open = __esm({
4207
4207
  init_is_inside_container();
4208
4208
  import_meta = {};
4209
4209
  execFile5 = (0, import_node_util5.promisify)(import_node_child_process5.default.execFile);
4210
- __dirname2 = import_node_path2.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
4211
- localXdgOpenPath = import_node_path2.default.join(__dirname2, "xdg-open");
4210
+ __dirname2 = import_node_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
4211
+ localXdgOpenPath = import_node_path3.default.join(__dirname2, "xdg-open");
4212
4212
  ({ platform, arch } = import_node_process7.default);
4213
4213
  pTryEach = async (array, mapper) => {
4214
4214
  let latestError;
@@ -6743,16 +6743,16 @@ __export(ora_exports, {
6743
6743
  function ora(options) {
6744
6744
  return new Ora(options);
6745
6745
  }
6746
- async function oraPromise(action70, options) {
6747
- const actionIsFunction = typeof action70 === "function";
6748
- const actionIsPromise = typeof action70.then === "function";
6746
+ async function oraPromise(action69, options) {
6747
+ const actionIsFunction = typeof action69 === "function";
6748
+ const actionIsPromise = typeof action69.then === "function";
6749
6749
  if (!actionIsFunction && !actionIsPromise) {
6750
6750
  throw new TypeError("Parameter `action` must be a Function or a Promise");
6751
6751
  }
6752
6752
  const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
6753
6753
  const spinner = ora(options).start();
6754
6754
  try {
6755
- const promise = actionIsFunction ? action70(spinner) : action70;
6755
+ const promise = actionIsFunction ? action69(spinner) : action69;
6756
6756
  const result = await promise;
6757
6757
  spinner.succeed(
6758
6758
  successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
@@ -7085,25 +7085,34 @@ var {
7085
7085
  Help
7086
7086
  } = import_index.default;
7087
7087
 
7088
+ // src/test-harness.ts
7089
+ var readSetting;
7090
+ function cliTestSetting(name) {
7091
+ return readSetting?.(name);
7092
+ }
7093
+ function cliTestHarnessActive() {
7094
+ return readSetting !== void 0;
7095
+ }
7096
+
7088
7097
  // src/cli.ts
7089
7098
  init_source();
7090
7099
 
7091
7100
  // src/commands/login.ts
7092
7101
  init_source();
7093
- var import_node_crypto2 = __toESM(require("node:crypto"));
7102
+ var import_node_crypto = __toESM(require("node:crypto"));
7094
7103
  var import_node_http = __toESM(require("node:http"));
7095
- var import_node_os4 = __toESM(require("node:os"));
7104
+ var import_node_os5 = __toESM(require("node:os"));
7096
7105
  var import_node_readline2 = __toESM(require("node:readline"));
7097
7106
 
7098
7107
  // src/config.ts
7099
- var import_node_fs = __toESM(require("node:fs"));
7100
- var import_node_path = __toESM(require("node:path"));
7101
- var import_node_os2 = __toESM(require("node:os"));
7108
+ var import_node_fs2 = __toESM(require("node:fs"));
7109
+ var import_node_path2 = __toESM(require("node:path"));
7110
+ var import_node_os3 = __toESM(require("node:os"));
7102
7111
 
7103
7112
  // package.json
7104
7113
  var package_default = {
7105
7114
  name: "@drip-apex/apex-cli",
7106
- version: "0.4.0",
7115
+ version: "0.5.0",
7107
7116
  license: "MIT",
7108
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7109
7118
  private: false,
@@ -7124,14 +7133,14 @@ var package_default = {
7124
7133
  "package.json"
7125
7134
  ],
7126
7135
  scripts: {
7127
- build: "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=cjs --outfile=dist/cli.js --banner:js='#!/usr/bin/env node' --external:esbuild --external:fsevents",
7136
+ build: "rm -rf dist dist-test && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=cjs --outfile=dist/cli.js --banner:js='#!/usr/bin/env node' --external:esbuild --external:fsevents && esbuild src/cli-test.ts --bundle --platform=node --target=node18 --format=cjs --outfile=dist-test/cli.js --banner:js='#!/usr/bin/env node' --external:esbuild --external:fsevents",
7128
7137
  "check:docs-command-index": "node scripts/check-docs-command-index.mjs",
7129
7138
  dev: "tsx src/cli.ts",
7130
7139
  test: 'npm run build && node --test "test/**/*.test.mjs"',
7131
7140
  typecheck: "tsc --noEmit"
7132
7141
  },
7133
7142
  engines: {
7134
- node: ">=18"
7143
+ node: ">=22.22.0"
7135
7144
  },
7136
7145
  publishConfig: {
7137
7146
  access: "public"
@@ -7146,7 +7155,7 @@ var package_default = {
7146
7155
  devDependencies: {
7147
7156
  "@types/node": "^22.15.0",
7148
7157
  tsx: "^4.22.4",
7149
- typescript: "^5.6.3"
7158
+ typescript: "^5.9.3"
7150
7159
  }
7151
7160
  };
7152
7161
 
@@ -7337,6 +7346,18 @@ async function handleCliError(err, command) {
7337
7346
  process.exit(cliError.exitCode);
7338
7347
  }
7339
7348
 
7349
+ // ../api-client/src/credentials.ts
7350
+ var import_node_fs = __toESM(require("node:fs"));
7351
+ var import_node_os2 = __toESM(require("node:os"));
7352
+ var import_node_path = __toESM(require("node:path"));
7353
+ function readStoredCredentials(file = import_node_path.default.join(import_node_os2.default.homedir(), ".drip", "credentials.json")) {
7354
+ try {
7355
+ return JSON.parse(import_node_fs.default.readFileSync(file, "utf8"));
7356
+ } catch {
7357
+ return null;
7358
+ }
7359
+ }
7360
+
7340
7361
  // src/config.ts
7341
7362
  function argvOption(name) {
7342
7363
  const flag = `--${name}`;
@@ -7347,6 +7368,21 @@ function argvOption(name) {
7347
7368
  }
7348
7369
  return void 0;
7349
7370
  }
7371
+ var didWarnStoredContext = false;
7372
+ function warnStoredContextFallback(creds, flags, context) {
7373
+ if (didWarnStoredContext) return;
7374
+ const storedOrg = !flags.orgId && !process.env.DRIP_ORG && Boolean(creds?.context?.orgId);
7375
+ const storedShop = !flags.shopId && !process.env.DRIP_SHOP && Boolean(creds?.context?.shopId);
7376
+ if (!storedOrg && !storedShop) return;
7377
+ const selections = [
7378
+ storedOrg ? `org ${context.orgId}` : null,
7379
+ storedShop ? `shop ${context.shopId}` : null
7380
+ ].filter(Boolean).join(" / ");
7381
+ console.error(
7382
+ `Using ${selections} from 'apex use' context (pass --org/--shop to override).`
7383
+ );
7384
+ didWarnStoredContext = true;
7385
+ }
7350
7386
  function defaultCredentialKind(creds) {
7351
7387
  return Boolean(process.env.DRIP_API_KEY) || !creds?.userApiKey ? "shop" : "user";
7352
7388
  }
@@ -7361,9 +7397,12 @@ function resolveContext(creds = getCredentials(), flags = { orgId: argvOption("o
7361
7397
  }
7362
7398
  return { orgId: flags.orgId || process.env.DRIP_ORG || void 0, shopId: flags.shopId || process.env.DRIP_SHOP || void 0 };
7363
7399
  }
7364
- const orgId = flags.orgId || process.env.DRIP_ORG || creds?.context?.orgId;
7365
- const shopId = flags.shopId || process.env.DRIP_SHOP || creds?.context?.shopId;
7366
- return { orgId, shopId };
7400
+ const context = {
7401
+ orgId: flags.orgId || process.env.DRIP_ORG || creds?.context?.orgId,
7402
+ shopId: flags.shopId || process.env.DRIP_SHOP || creds?.context?.shopId
7403
+ };
7404
+ warnStoredContextFallback(creds, flags, context);
7405
+ return context;
7367
7406
  }
7368
7407
  function configureInvocationContext() {
7369
7408
  const creds = getCredentials();
@@ -7371,14 +7410,10 @@ function configureInvocationContext() {
7371
7410
  const context = resolveContext(creds, void 0, credential);
7372
7411
  setEnvelopeContext({ credential, org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7373
7412
  }
7374
- var DRIP_DIR = import_node_path.default.join(import_node_os2.default.homedir(), ".drip");
7375
- var CREDS_FILE = import_node_path.default.join(DRIP_DIR, "credentials.json");
7413
+ var DRIP_DIR = import_node_path2.default.join(import_node_os3.default.homedir(), ".drip");
7414
+ var CREDS_FILE = import_node_path2.default.join(DRIP_DIR, "credentials.json");
7376
7415
  function getStoredCredentials() {
7377
- try {
7378
- return JSON.parse(import_node_fs.default.readFileSync(CREDS_FILE, "utf-8"));
7379
- } catch {
7380
- return null;
7381
- }
7416
+ return readStoredCredentials(CREDS_FILE);
7382
7417
  }
7383
7418
  function getCredentials() {
7384
7419
  const envKey = process.env.DRIP_API_KEY;
@@ -7390,1446 +7425,147 @@ function getCredentials() {
7390
7425
  // An env-provided key pairs only with an env-provided URL or the
7391
7426
  // production default — never a stored custom URL, so a credential is
7392
7427
  // never silently sent to a host it was not issued for.
7393
- baseUrl: envUrl ?? "https://app.drip-apex.com",
7394
- userApiKey: fileCreds?.userApiKey,
7395
- userBaseUrl: fileCreds?.userBaseUrl ?? fileCreds?.baseUrl,
7396
- context: fileCreds?.context
7397
- };
7398
- }
7399
- return fileCreds;
7400
- }
7401
- function saveCredentials(creds) {
7402
- const { credentialKind: _kind, resolvedContext: _resolved, ...persisted } = creds;
7403
- import_node_fs.default.mkdirSync(DRIP_DIR, { recursive: true });
7404
- import_node_fs.default.writeFileSync(CREDS_FILE, JSON.stringify(persisted, null, 2) + "\n", {
7405
- mode: 384
7406
- });
7407
- }
7408
- function clearCredentials() {
7409
- try {
7410
- import_node_fs.default.unlinkSync(CREDS_FILE);
7411
- } catch {
7412
- }
7413
- }
7414
- function requireCredentials() {
7415
- const creds = getCredentials();
7416
- if (!creds) {
7417
- throw new CliError(
7418
- 'Not logged in. Run "apex login" first, or set DRIP_API_KEY env var.',
7419
- EXIT_CODES.auth
7420
- );
7421
- }
7422
- const context = resolveContext(creds, void 0, defaultCredentialKind(creds));
7423
- if (!process.env.DRIP_API_KEY && creds.userApiKey) {
7424
- setEnvelopeContext({ credential: "user", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7425
- if (!context.shopId) {
7426
- throw new CliError(
7427
- 'Shop context is required. Run "apex use", or pass --shop <id>.',
7428
- EXIT_CODES.validation,
7429
- { code: "shop_context_required", retry: "apex use <org>/<shop>", flag: "--shop" }
7430
- );
7431
- }
7432
- return { ...creds, apiKey: creds.userApiKey, baseUrl: creds.userBaseUrl ?? creds.baseUrl, credentialKind: "user", resolvedContext: context };
7433
- }
7434
- setEnvelopeContext({ credential: "shop", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7435
- return { ...creds, credentialKind: "shop", resolvedContext: context };
7436
- }
7437
- function requireUserCredentials() {
7438
- const creds = getCredentials();
7439
- if (!creds?.userApiKey) {
7440
- throw new CliError(
7441
- 'This command needs a user credential (apx_uk_...). Run "apex login" first.',
7442
- EXIT_CODES.auth
7443
- );
7444
- }
7445
- const context = resolveContext(creds, void 0, "user");
7446
- setEnvelopeContext({ credential: "user", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7447
- return { ...creds, apiKey: creds.userApiKey, baseUrl: creds.userBaseUrl ?? creds.baseUrl, credentialKind: "user", resolvedContext: context };
7448
- }
7449
- var CONFIG_NAMES = [
7450
- ".drip.config.ts",
7451
- ".drip.config.js",
7452
- "drip.config.ts",
7453
- "drip.config.js"
7454
- ];
7455
- function findConfigPath() {
7456
- const cwd = process.cwd();
7457
- for (const name of CONFIG_NAMES) {
7458
- const p = import_node_path.default.join(cwd, name);
7459
- if (import_node_fs.default.existsSync(p)) return p;
7460
- }
7461
- return null;
7462
- }
7463
-
7464
- // src/api.ts
7465
- var import_node_crypto = require("node:crypto");
7466
- var APEX_API_VERSION_HEADER = "x-apex-api-version";
7467
- var SUPPORTED_APEX_API_VERSION = "1";
7468
- var warnedAboutApiVersionSkew = false;
7469
- function warnAboutApiVersionSkew(headers) {
7470
- const serverVersion = headers.get(APEX_API_VERSION_HEADER);
7471
- if (!serverVersion || warnedAboutApiVersionSkew) return;
7472
- const numericServerVersion = Number(serverVersion);
7473
- const numericSupportedVersion = Number(SUPPORTED_APEX_API_VERSION);
7474
- if (!Number.isFinite(numericServerVersion) || numericServerVersion === numericSupportedVersion) return;
7475
- warnedAboutApiVersionSkew = true;
7476
- process.stderr.write(
7477
- numericServerVersion > numericSupportedVersion ? "CLI is behind the server \u2014 update.\n" : "Server API is older than this CLI supports.\n"
7478
- );
7479
- }
7480
- var mockEnvironmentPatchAttempts = 0;
7481
- var mockEnvironmentReads = 0;
7482
- var mockDomainsPatchAttempts = 0;
7483
- var mockScreenshotPolls = 0;
7484
- function extractErrorMessage(json, status2) {
7485
- if (!json) return `HTTP ${status2}`;
7486
- const err = json.error;
7487
- if (typeof err === "string" && err.trim()) return err;
7488
- if (err && typeof err === "object") {
7489
- const msg2 = err.message;
7490
- if (typeof msg2 === "string" && msg2.trim()) return msg2;
7491
- }
7492
- const msg = json.message;
7493
- if (typeof msg === "string" && msg.trim()) return msg;
7494
- return `HTTP ${status2}`;
7495
- }
7496
- function extractErrorDetails(json) {
7497
- if (!json) return void 0;
7498
- const candidates = [
7499
- json.details,
7500
- json.meta,
7501
- json.error && typeof json.error === "object" ? json.error.details : void 0
7502
- ];
7503
- const nestedError = json.error && typeof json.error === "object" && !Array.isArray(json.error) ? json.error : null;
7504
- const code = typeof json.code === "string" ? json.code : typeof nestedError?.code === "string" ? nestedError.code : void 0;
7505
- for (const candidate of candidates) {
7506
- if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
7507
- return { ...candidate, ...code ? { code } : {} };
7508
- }
7509
- }
7510
- const limitKeys = ["limit", "current", "max", "actual", "field", "code"];
7511
- const details = Object.fromEntries(
7512
- limitKeys.filter((key) => key in json).map((key) => [key, json[key]])
7513
- );
7514
- if (code) details.code = code;
7515
- return Object.keys(details).length > 0 ? details : void 0;
7516
- }
7517
- function mockExperiment() {
7518
- const collisionFixture = ["collision", "collision-partial"].includes(process.env.APEX_CLI_MOCK_SCREENSHOTS ?? "");
7519
- const collidingVariation = collisionFixture ? [{
7520
- id: "deadbeef-0000-4000-8000-000000000002",
7521
- name: "Trust Badge",
7522
- weight: 0,
7523
- is_control: false,
7524
- mutations: []
7525
- }] : [];
7526
- return {
7527
- id: "00000000-0000-4000-8000-000000000001",
7528
- slug: "fixture-test",
7529
- name: "Fixture Test",
7530
- status: "draft",
7531
- traffic_allocation: 1,
7532
- store_url: "https://example.com/products/demo",
7533
- created_at: "2026-07-04T00:00:00.000Z",
7534
- archived_at: null,
7535
- variations: [
7536
- {
7537
- id: "00000000-0000-4000-8000-0000000000c0",
7538
- slug: "control",
7539
- name: "Control",
7540
- weight: 0.5,
7541
- is_control: true,
7542
- mutations: []
7543
- },
7544
- ...collidingVariation,
7545
- {
7546
- id: "00000000-0000-4000-8000-0000000000b1",
7547
- slug: collisionFixture ? void 0 : "badge",
7548
- name: "Trust Badge",
7549
- weight: 0.5,
7550
- is_control: false,
7551
- mutations: []
7552
- }
7553
- ]
7554
- };
7555
- }
7556
- var SECOND_PAGE_GOAL_ID = "00000000-0000-4000-8000-0000000000f2";
7557
- function mockFunnel() {
7558
- return {
7559
- id: "00000000-0000-4000-8000-0000000000aa",
7560
- name: "Fixture funnel",
7561
- steps: [
7562
- { order: 1, label: "Collection", url_pattern: "/collections/*" },
7563
- { order: 2, label: "Product", url_pattern: "/products/*" }
7564
- ],
7565
- created_at: "2026-07-04T00:00:00.000Z"
7566
- };
7567
- }
7568
- function mockGoal() {
7569
- return {
7570
- id: "00000000-0000-4000-8000-0000000000f1",
7571
- name: "Fixture purchase",
7572
- type: "custom",
7573
- measurement: "conversion",
7574
- count_once: false,
7575
- selector: null,
7576
- text_match: null,
7577
- url_pattern: null,
7578
- page_id: null,
7579
- event_name: "purchase",
7580
- capture: null,
7581
- created_at: "2026-07-04T00:00:00.000Z"
7582
- };
7583
- }
7584
- var PAGE_SCOPED_GOAL_ID = "00000000-0000-4000-8000-0000000000f3";
7585
- function mockPageScopedGoal() {
7586
- return {
7587
- ...mockGoal(),
7588
- id: PAGE_SCOPED_GOAL_ID,
7589
- name: "Fixture page-scoped pageview",
7590
- type: "pageview",
7591
- event_name: null,
7592
- url_pattern: "bestellung-abgeschlossen",
7593
- page_id: "00000000-0000-4000-8000-0000000000e1"
7594
- };
7595
- }
7596
- function mockBrandContext() {
7597
- if (process.env.APEX_CLI_MOCK_BRAND_PROFILE === "none") {
7598
- return {
7599
- hasProfile: false,
7600
- mode: "off",
7601
- status: "",
7602
- summary: "",
7603
- voice: {
7604
- adjectives: [],
7605
- weAlways: [],
7606
- weNever: [],
7607
- bannedPhrases: [],
7608
- ctaStyle: ""
7609
- },
7610
- guardrails: [],
7611
- proofRequirements: [],
7612
- clientNotes: "",
7613
- clientAssetExcerpts: [],
7614
- guidelinesText: "",
7615
- promptBlock: ""
7616
- };
7617
- }
7618
- return {
7619
- hasProfile: true,
7620
- mode: "balanced",
7621
- status: "ready",
7622
- summary: "Fixture brand profile",
7623
- voice: {
7624
- adjectives: ["clear", "practical"],
7625
- weAlways: ["Use proof-backed claims"],
7626
- weNever: ["Overpromise"],
7627
- bannedPhrases: ["revolutionary"],
7628
- ctaStyle: "Direct and specific"
7629
- },
7630
- guardrails: ["Avoid unsupported urgency"],
7631
- proofRequirements: ["Mention only verified store facts"],
7632
- clientNotes: "Prefer concise launch copy.",
7633
- clientAssetExcerpts: [
7634
- { name: "brand.pdf", excerpt: "Use plain language." }
7635
- ],
7636
- guidelinesText: "Keep customer-facing copy concrete.",
7637
- promptBlock: [
7638
- "## Brand context (mode: balanced)",
7639
- "Summary: Fixture brand profile",
7640
- "Voice: clear, practical",
7641
- "Always: Use proof-backed claims"
7642
- ].join("\n")
7643
- };
7644
- }
7645
- function mockResponse(method, path20, body5) {
7646
- if (process.env.APEX_CLI_MOCK_API !== "1") return null;
7647
- const url = new URL(path20, "https://mock.apex.local");
7648
- const exp = mockExperiment();
7649
- if (method === "POST" && url.pathname === `/api/v1/experiments/${exp.id}/screenshots`) {
7650
- mockScreenshotPolls = 0;
7651
- return { ok: true, status: 202, data: { job_id: "screenshot_job_1", status: "queued", poll_url: "/api/v1/screenshots/screenshot_job_1" } };
7652
- }
7653
- if (method === "GET" && url.pathname === "/api/v1/screenshots/screenshot_job_1") {
7654
- mockScreenshotPolls += 1;
7655
- if (process.env.APEX_CLI_MOCK_SCREENSHOTS === "transient" && mockScreenshotPolls === 1) {
7656
- return { ok: false, status: 503, error: "Service temporarily unavailable" };
7657
- }
7658
- if (process.env.APEX_CLI_MOCK_SCREENSHOTS === "timeout" || process.env.APEX_CLI_MOCK_SCREENSHOTS === "deadline-processing" || mockScreenshotPolls === 1 && process.env.APEX_CLI_MOCK_SCREENSHOTS !== "deadline-completed") {
7659
- return { ok: true, status: 200, data: { job_id: "screenshot_job_1", experiment_id: exp.id, status: "processing", devices: ["desktop_chrome"], variation_ids: exp.variations.map((variation) => variation.id) } };
7660
- }
7661
- const collisionPartial = process.env.APEX_CLI_MOCK_SCREENSHOTS === "collision-partial";
7662
- const results2 = {
7663
- [exp.variations[0].id]: { desktop_chrome: "data:image/png;base64,iVBORw0KGgo=" }
7664
- };
7665
- if (process.env.APEX_CLI_MOCK_SCREENSHOTS !== "partial") {
7666
- for (const variation of exp.variations.slice(collisionPartial ? 2 : 1)) {
7667
- results2[variation.id] = { desktop_chrome: "data:image/png;base64,iVBORw0KGgo=" };
7668
- }
7669
- }
7670
- return { ok: true, status: 200, data: { job_id: "screenshot_job_1", experiment_id: exp.id, status: "completed", devices: ["desktop_chrome"], variation_ids: exp.variations.map((variation) => variation.id), results: results2 } };
7671
- }
7672
- if (url.pathname.startsWith("/api/v1/webhooks")) {
7673
- const input = body5 ?? {};
7674
- const webhook = {
7675
- id: url.pathname.split("/")[4] || "webhook_1",
7676
- url: String(input.url ?? "https://hooks.example.test/apex"),
7677
- events: Array.isArray(input.events) ? input.events.map(String) : ["project.published"],
7678
- active: input.active === void 0 ? true : input.active === true,
7679
- hasSecret: true,
7680
- consecutiveFailures: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? 3 : 1,
7681
- disabledAt: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "2026-08-05T12:00:00.000Z" : null,
7682
- created_at: "2026-08-05T00:00:00.000Z"
7683
- };
7684
- const availableEvents = ["experiment.started", "experiment.completed", "experiment.significant", "experiment.guardian_triggered", "experiment.guardian_paused", "variation.winning", "screenshots.completed", "project.published"];
7685
- if (method === "GET" && url.pathname === "/api/v1/webhooks") return { ok: true, status: 200, data: { data: [webhook], availableEvents, pagination: { has_more: false, next_cursor: null } } };
7686
- if (method === "POST" && url.pathname === "/api/v1/webhooks") return { ok: true, status: 201, data: { ...webhook, secret: "whsec_mock_secret" } };
7687
- if (method === "PATCH") return { ok: true, status: 200, data: webhook };
7688
- if (method === "DELETE") return { ok: true, status: 200, data: { deleted: true, id: webhook.id } };
7689
- if (method === "GET" && url.pathname.endsWith("/deliveries")) return { ok: true, status: 200, data: { webhook: { id: webhook.id, active: webhook.active, consecutiveFailures: webhook.consecutiveFailures, disabledAt: webhook.disabledAt }, data: [{ id: "delivery_1", webhook_id: webhook.id, event: "project.published", status: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "dead" : "delivered", attempts: 1, response_status: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? 500 : 200, last_error: process.env.APEX_CLI_MOCK_WEBHOOK_DISABLED === "1" ? "Webhook responded with 500" : null, next_attempt_at: null, processing_started_at: null, delivered_at: "2026-08-05T00:01:00.000Z", created_at: "2026-08-05T00:00:00.000Z", updated_at: "2026-08-05T00:01:00.000Z" }], pagination: { has_more: false, next_cursor: null } } };
7690
- if (method === "POST" && url.pathname.endsWith("/test")) return { ok: true, status: 200, data: { webhook: { id: webhook.id, active: webhook.active, consecutiveFailures: webhook.consecutiveFailures, disabledAt: webhook.disabledAt }, delivery: { id: "delivery_test", webhook_id: webhook.id, event: "test", status: "delivered", attempts: 1, response_status: 200, last_error: null, next_attempt_at: null, processing_started_at: null, delivered_at: "2026-08-05T00:01:00.000Z", created_at: "2026-08-05T00:00:00.000Z", updated_at: "2026-08-05T00:01:00.000Z" } } };
7691
- }
7692
- const forcedStatus = Number(process.env.APEX_CLI_MOCK_ERROR_STATUS ?? 0);
7693
- if (forcedStatus > 0) {
7694
- return {
7695
- ok: false,
7696
- status: forcedStatus,
7697
- error: forcedStatus === 401 ? "Invalid API key" : "Resource not found",
7698
- errorDetails: { mocked: true }
7699
- };
7700
- }
7701
- if (method === "POST" && url.pathname.endsWith("/build") && process.env.APEX_CLI_MOCK_WORKSPACE_BUILD === "blocked-evidence") {
7702
- return {
7703
- ok: false,
7704
- status: 400,
7705
- error: "Generated custom JavaScript includes an external network or asset call.",
7706
- data: {
7707
- ok: false,
7708
- error: "Generated custom JavaScript includes an external network or asset call.",
7709
- blockingIssues: [
7710
- {
7711
- code: "external_network_call",
7712
- severity: "blocking",
7713
- field: "custom_js",
7714
- message: "Generated custom JavaScript includes an external network or asset call.",
7715
- evidence: {
7716
- construct: "String-capable timer execution (fail-closed)",
7717
- snippet: "setTimeout(unknownHandler, 10)",
7718
- line: 7,
7719
- column: 2
7720
- }
7721
- },
7722
- {
7723
- code: "external_network_call",
7724
- severity: "blocking",
7725
- field: "mutations[1].html",
7726
- message: "Generated HTML includes an external network or asset URL.",
7727
- evidence: {
7728
- construct: "External HTML asset URL",
7729
- url: "https://assets.example.test/pixel.png",
7730
- matchIndex: 14
7731
- }
7732
- },
7733
- {
7734
- code: "invalid_custom_code_type",
7735
- severity: "blocking",
7736
- field: "custom_css",
7737
- message: "custom_css must be a string."
7738
- },
7739
- {
7740
- code: "price_node_mutation",
7741
- severity: "blocking",
7742
- field: ".product-price",
7743
- message: "This mutation replaces a price-display node.",
7744
- evidence: {
7745
- selector: ".product-price"
7746
- }
7747
- },
7748
- {
7749
- code: "unknown_validation_family",
7750
- severity: "blocking",
7751
- field: "future_guardrail_field",
7752
- message: "A future guardrail blocked this workspace."
7753
- }
7754
- ]
7755
- }
7756
- };
7757
- }
7758
- if (method === "GET" && url.pathname === "/api/v1/roadmap/approved") {
7759
- return {
7760
- ok: true,
7761
- status: 200,
7762
- data: {
7763
- data: [{ id: "roadmap_1", title: "Fixture roadmap idea", linked_experiments: [] }],
7764
- pagination: { has_more: false, next_cursor: null }
7765
- }
7766
- };
7767
- }
7768
- if (method === "GET" && url.pathname === "/api/v1/roadmap/items/roadmap_1/handoff") {
7769
- return {
7770
- ok: true,
7771
- status: 200,
7772
- data: {
7773
- id: "roadmap_1",
7774
- title: "Fixture roadmap idea",
7775
- hypothesis: "The fixture improves conversion",
7776
- instructions: "Build the fixture"
7777
- }
7778
- };
7779
- }
7780
- if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/filesystem`) {
7781
- if (process.env.APEX_CLI_MOCK_FILESYSTEM_REFRESH === "failed") {
7782
- throw new Error("Mock filesystem refresh failed");
7783
- }
7784
- const fixtures = [
7785
- ["experiment.json", '{"schemaVersion":1,"name":"Fixture Test"}\n'],
7786
- ["targeting.json", '{"url":[],"segments":[]}\n'],
7787
- ["experiment/index.js", "window.__apexExperiment = true;\n"],
7788
- ["experiment/reset.js", "delete window.__apexExperiment;\n"],
7789
- ["experiment/index.css", ".apex-experiment { display: block; }\n"],
7790
- ["metrics.json", '{"primary":null,"secondary":[]}\n'],
7791
- ["qa.json", '{"status":"pending"}\n'],
7792
- ["launch.json", '{"status":"draft"}\n'],
7793
- ["variations/control/variation.json", '{"slug":"control","isControl":true}\n'],
7794
- ["variations/control/mutations.json", "[]\n"],
7795
- ["variations/control/url-blocks.json", "[]\n"],
7796
- ["variations/control/index.css", ".control { color: inherit; }\n"],
7797
- ["variations/control/index.js", "window.__apexControl = true;\n"],
7798
- ["variations/control/reset.js", "delete window.__apexControl;\n"],
7799
- ["variations/control/anti-flicker.json", "[]\n"]
7800
- ];
7801
- const invalidPaths = {
7802
- invalid: "../secret",
7803
- traversal: "variations/../../x",
7804
- absolute: "/abs/path"
7805
- };
7806
- const invalidPath = invalidPaths[process.env.APEX_CLI_MOCK_FILESYSTEM_PATHS ?? ""];
7807
- if (invalidPath) {
7808
- fixtures.push([invalidPath, "must not be written\n"]);
7809
- }
7810
- return {
7811
- ok: true,
7812
- status: 200,
7813
- data: {
7814
- rootName: "fixture-test",
7815
- files: fixtures.map(([filePath, content]) => ({
7816
- path: filePath,
7817
- content,
7818
- sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex")
7819
- }))
7820
- }
7821
- };
7822
- }
7823
- const workspaceMatch = url.pathname.match(
7824
- /^\/api\/v1\/experiments\/([^/]+)\/variations\/([^/]+)\/workspace$/
7825
- );
7826
- if (method === "GET" && workspaceMatch) {
7827
- const content = "window.__apexOperator = true;\n";
7828
- return {
7829
- ok: true,
7830
- status: 200,
7831
- data: {
7832
- workspace: {
7833
- id: "workspace_1",
7834
- schemaVersion: 1,
7835
- status: "draft",
7836
- currentRevisionId: "revision_1",
7837
- updatedAt: "2026-08-05T00:00:00.000Z"
7838
- },
7839
- files: [
7840
- {
7841
- path: "src/index.js",
7842
- kind: "custom_js",
7843
- contentType: "application/javascript",
7844
- content,
7845
- sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex"),
7846
- sizeBytes: Buffer.byteLength(content),
7847
- updatedAt: "2026-08-05T00:00:00.000Z"
7848
- }
7849
- ]
7850
- }
7851
- };
7852
- }
7853
- const configMatch = url.pathname.match(
7854
- /^\/api\/v1\/(assets|landing-pages)(?:\/([^/]+))?$/
7855
- );
7856
- if (configMatch) {
7857
- const [, family, id = family === "assets" ? "asset_1" : "landing_page_1"] = configMatch;
7858
- const item = {
7859
- id,
7860
- name: family === "assets" ? "fixture.png" : "Fixture landing page",
7861
- status: "draft",
7862
- created_at: "2026-07-20T00:00:00.000Z"
7863
- };
7864
- if (method === "GET" && !configMatch[2])
7865
- return {
7866
- ok: true,
7867
- status: 200,
7868
- data: {
7869
- data: [item],
7870
- pagination: {
7871
- has_more: false,
7872
- next_cursor: url.searchParams.get("cursor")
7873
- }
7874
- }
7875
- };
7876
- if (method === "POST" && family === "assets" && body5 instanceof FormData) {
7877
- if (process.env.APEX_CLI_MOCK_ASSET_STATUS === "413") {
7878
- return {
7879
- ok: false,
7880
- status: 413,
7881
- error: "Asset exceeds the upload size limit"
7882
- };
7883
- }
7884
- const file = body5.get("files");
7885
- return {
7886
- ok: true,
7887
- status: 201,
7888
- data: {
7889
- assets: [{ ...item, mime_type: file instanceof Blob ? file.type : "" }]
7890
- }
7891
- };
7892
- }
7893
- if (method === "DELETE" && family === "assets")
7894
- return { ok: true, status: 200, data: { ok: true } };
7895
- return { ok: true, status: 200, data: item };
7896
- }
7897
- const targetingMatch = url.pathname.match(
7898
- /^\/api\/v1\/(pages|segments|exclusion-groups)(?:\/([^/]+))?(?:\/members)?$/
7899
- );
7900
- if (targetingMatch) {
7901
- const [, kind5, id = `${kind5.replace(/s$/, "")}_1`] = targetingMatch;
7902
- const input = body5 ?? {};
7903
- const item = {
7904
- id,
7905
- name: String(input.name ?? `Fixture ${kind5}`),
7906
- ...kind5 === "exclusion-groups" ? { updatedAt: "2026-07-20T00:00:00.000Z" } : { updated_at: "2026-07-20T00:00:00.000Z" },
7907
- ...input
7908
- };
7909
- if (method === "GET" && !targetingMatch[2]) {
7910
- return {
7911
- ok: true,
7912
- status: 200,
7913
- data: {
7914
- data: [item],
7915
- pagination: {
7916
- has_more: false,
7917
- next_cursor: url.searchParams.get("cursor")
7918
- }
7919
- }
7920
- };
7921
- }
7922
- return { ok: true, status: method === "POST" ? 201 : 200, data: item };
7923
- }
7924
- if (method === "POST" && url.pathname === "/api/v1/external-pixels/snippet")
7925
- return {
7926
- ok: true,
7927
- status: 200,
7928
- data: {
7929
- snippet: "window.apexExternalPurchase=function(){};",
7930
- provider: "external"
7931
- }
7932
- };
7933
- const membershipMatch = url.pathname.match(/^\/api\/v1\/(org|team)(?:\/(members|invitations))?(?:\/([^/]+))?$/);
7934
- if (membershipMatch) {
7935
- const [, plane, collection, id] = membershipMatch;
7936
- const input = body5 ?? {};
7937
- const org = { id: "org_1", name: String(input.name ?? "Fixture organization"), slug: String(input.slug ?? "fixture-organization") };
7938
- const member = { id: id ?? "member_1", user_id: "user_1", role: String(input.role ?? "member") };
7939
- const invitation = { id: id ?? "invite_1", email: String(input.email ?? "member@example.com"), role: String(input.role ?? input.org_role ?? "viewer"), status: "pending" };
7940
- if (plane === "org" && !collection) return { ok: true, status: 200, data: { data: org } };
7941
- if (method === "DELETE") return { ok: true, status: 200, data: { success: true, id } };
7942
- const value = collection === "invitations" ? invitation : member;
7943
- return { ok: true, status: method === "POST" ? 201 : 200, data: method === "GET" ? { data: [value] } : { data: value } };
7944
- }
7945
- if (method === "POST" && url.pathname === "/api/v1/preview-sessions") return { ok: true, status: 200, data: { session_id: "preview_fixture", preview_url: "https://preview.drip-apex.com/api/preview?pst=fixture", expires_at: "2026-07-20T11:00:00.000Z" } };
7946
- if (url.pathname === "/api/v1/admin/settings") return { ok: true, status: 200, data: { data: { guardian_enabled: method === "PATCH" ? Boolean(body5?.guardian_enabled ?? true) : true, srm_check_enabled: method === "PATCH" ? Boolean(body5?.srm_check_enabled ?? true) : true } } };
7947
- if (method === "GET" && url.pathname === "/api/v1/admin/tracking/quarantine") return { ok: true, status: 200, data: { rows: [{ id: "q_1", shop_name: "Fixture shop", reason_code: "invalid_payload", verdict: "quarantined", created_at: "2026-07-20T10:00:00.000Z" }] } };
7948
- if (method === "GET" && url.pathname === "/api/v1/audit-logs") return { ok: true, status: 200, data: { data: [{ id: "audit_1", created_at: "2026-07-20T10:00:00.000Z", action: "experiment.updated", entity_type: "experiment", shop_id: "11111111-1111-4111-8111-111111111112", org_id: null, user_id: "11111111-1111-4111-8111-111111111111", entity_id: exp.id, entity_name: exp.name, diff: null }], pagination: { total: 1, limit: 50, offset: 0, has_more: false } } };
7949
- const shopMatch = url.pathname.match(
7950
- /^\/api\/v1\/shops(?:\/([^/]+))?(?:\/(.*))?$/
7951
- );
7952
- if (shopMatch) {
7953
- const input = body5 ?? {};
7954
- const shop = {
7955
- id: shopMatch[1] ?? "11111111-1111-4111-8111-111111111112",
7956
- name: String(input.name ?? "Fixture Shop"),
7957
- domain: String(input.domain ?? "fixture.example.com"),
7958
- ...input.platform ? { platform: String(input.platform) } : {},
7959
- ...input.plan ? { plan: String(input.plan) } : {},
7960
- tier: String(input.tier ?? (input.plan === "self_serve" ? "self_managed" : "apex_managed")),
7961
- created_at: "2026-07-20T00:00:00.000Z"
7962
- };
7963
- if (!shopMatch[1] && method === "GET")
7964
- return { ok: true, status: 200, data: { data: [shop] } };
7965
- if (shopMatch[2] === "store-profile")
7966
- return {
7967
- ok: true,
7968
- status: 200,
7969
- data: { schemaReady: true, shopId: shop.id, facts: [] }
7970
- };
7971
- if (shopMatch[2])
7972
- return {
7973
- ok: true,
7974
- status: method === "POST" ? 201 : 200,
7975
- data: { ok: true, area: shopMatch[2], ...input }
7976
- };
7977
- return {
7978
- ok: true,
7979
- status: method === "POST" ? 201 : 200,
7980
- data: method === "PATCH" ? { ok: true, shop } : shop
7981
- };
7982
- }
7983
- if (method === "GET" && url.pathname === "/api/v1/audit-logs")
7984
- return {
7985
- ok: true,
7986
- status: 200,
7987
- data: {
7988
- data: [
7989
- {
7990
- id: "audit_1",
7991
- created_at: "2026-07-20T10:00:00.000Z",
7992
- action: "experiment.updated",
7993
- entity_type: "experiment",
7994
- shop_id: "11111111-1111-4111-8111-111111111112",
7995
- org_id: null,
7996
- user_id: "11111111-1111-4111-8111-111111111111",
7997
- entity_id: exp.id,
7998
- entity_name: exp.name,
7999
- diff: null
8000
- }
8001
- ],
8002
- pagination: { total: 1, limit: 50, offset: 0, has_more: false }
8003
- }
8004
- };
8005
- if (url.pathname === "/api/v1/inbox/messages") {
8006
- const input = body5 ?? {};
8007
- const message = {
8008
- id: "message_1",
8009
- role: "user",
8010
- body: String(input.body ?? "Fixture inbox message"),
8011
- sender_id: "11111111-1111-4111-8111-111111111111",
8012
- created_at: "2026-07-20T10:00:00.000Z"
8013
- };
8014
- return {
8015
- ok: true,
8016
- status: method === "POST" ? 201 : 200,
8017
- data: method === "POST" ? { message } : { messages: [message] }
8018
- };
8019
- }
8020
- if (method === "POST" && url.pathname === "/api/v1/inbox/read-state")
8021
- return {
8022
- ok: true,
8023
- status: 200,
8024
- data: {
8025
- ok: true,
8026
- channelKeys: body5.channelKey ? [body5.channelKey] : ["account:account"],
8027
- lastReadAt: "2026-07-20T10:00:00.000Z"
8028
- }
8029
- };
8030
- const managedMatch = url.pathname.match(
8031
- /^\/api\/v1\/(flags|personalizations)(?:\/([^/]+))?(?:\/(activate|pause|archive|status))?$/
8032
- );
8033
- if (managedMatch) {
8034
- const [, kind5, id = `${kind5.slice(0, -1)}_1`, action70] = managedMatch;
8035
- const input = body5 ?? {};
8036
- const item = {
8037
- id,
8038
- key: String(input.key ?? `fixture-${kind5.slice(0, -1)}`),
8039
- name: String(input.name ?? `Fixture ${kind5.slice(0, -1)}`),
8040
- status: action70 === "activate" ? "active" : action70 === "pause" ? "paused" : action70 === "archive" ? "archived" : "draft",
8041
- updated_at: "2026-07-19T00:00:00.000Z"
8042
- };
8043
- if (kind5 === "flags" && action70 === "status")
8044
- return {
8045
- ok: true,
8046
- status: 200,
8047
- data: {
8048
- flag: item,
8049
- publication_revision: 2,
8050
- generated_at: "2026-07-19T00:00:00.000Z",
8051
- config_urls: [
8052
- "data:application/json,%7B%22publication_revision%22%3A2%2C%22generated_at%22%3A%222026-07-19T00%3A00%3A00.000Z%22%7D"
8053
- ]
8054
- }
8055
- };
8056
- if (method === "GET" && !managedMatch[2]) {
8057
- return {
8058
- ok: true,
8059
- status: 200,
8060
- data: {
8061
- data: [item],
8062
- pagination: { has_more: false, next_cursor: null }
8063
- }
8064
- };
8065
- }
8066
- return {
8067
- ok: true,
8068
- status: method === "POST" && !managedMatch[2] ? 201 : 200,
8069
- data: item
8070
- };
8071
- }
8072
- const insightMatch = url.pathname.match(
8073
- /^\/api\/v1\/(backlog|signals)(?:\/([^/]+))?(?:\/(archive|restore))?$/
8074
- );
8075
- if (insightMatch) {
8076
- const [, kind5, id = `${kind5}_1`, action70] = insightMatch;
8077
- const input = body5 ?? {};
8078
- if (action70 && (kind5 !== "backlog" || method !== "POST"))
8079
- return { ok: false, status: 405, error: `${action70} requires POST` };
8080
- const archived = action70 === "archive" || method === "DELETE" || url.searchParams.get("include_archived") === "true";
8081
- const item = {
8082
- id,
8083
- ...kind5 === "backlog" ? {
8084
- title: String(input.title ?? "Fixture backlog item"),
8085
- archived_at: archived ? "2026-07-20T00:00:00.000Z" : null
8086
- } : {
8087
- key: String(input.key ?? "fixture-signal"),
8088
- label: String(input.label ?? "Fixture signal")
8089
- },
8090
- status: archived ? "archived" : "active",
8091
- updated_at: "2026-07-20T00:00:00.000Z"
8092
- };
8093
- if (method === "GET" && !insightMatch[2])
8094
- return {
8095
- ok: true,
8096
- status: 200,
8097
- data: {
8098
- data: [item],
8099
- pagination: { has_more: false, next_cursor: null }
8100
- }
8101
- };
8102
- if (method === "DELETE")
8103
- return { ok: true, status: 200, data: { ok: true } };
8104
- return {
8105
- ok: true,
8106
- status: method === "POST" && !action70 ? 201 : 200,
8107
- data: item
8108
- };
8109
- }
8110
- if (method === "GET" && url.pathname === "/api/v1/experiments") {
8111
- const experiment = url.searchParams.get("archived") === "1" ? { ...exp, archived_at: "2026-07-20T00:00:00.000Z" } : { ...exp, archived_at: null };
8112
- return {
8113
- ok: true,
8114
- status: 200,
8115
- data: {
8116
- data: [experiment],
8117
- pagination: { has_more: false, next_cursor: null }
8118
- }
8119
- };
8120
- }
8121
- if (method === "POST" && url.pathname === `/api/v1/experiments/${exp.id}/start`) {
8122
- return { ok: true, status: 200, data: { ...exp, status: "running" } };
8123
- }
8124
- if (method === "POST" && url.pathname === "/api/v1/experiments") {
8125
- const input = body5 ?? {};
8126
- if (input.experiment_type === "redirect" && process.env.APEX_CLI_MOCK_REDIRECT_DISABLED === "1") {
8127
- return {
8128
- ok: false,
8129
- status: 403,
8130
- error: "Redirect tests are not enabled for this shop"
8131
- };
8132
- }
8133
- const requestedVariations = Array.isArray(input.variations) ? input.variations : exp.variations ?? [];
8134
- return {
8135
- ok: true,
8136
- status: 201,
8137
- data: {
8138
- ...exp,
8139
- name: String(input.name ?? exp.name),
8140
- slug: typeof input.slug === "string" || input.slug === null ? input.slug : exp.slug,
8141
- experiment_type: input.experiment_type === "redirect" ? "redirect" : "mutation",
8142
- traffic_allocation: typeof input.traffic_allocation === "number" ? input.traffic_allocation : exp.traffic_allocation,
8143
- store_url: typeof input.store_url === "string" ? input.store_url : null,
8144
- targeting: input.targeting ?? null,
8145
- variations: requestedVariations.map((variation, index) => ({
8146
- id: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
8147
- name: String(variation.name ?? `Variation ${index + 1}`),
8148
- weight: typeof variation.weight === "number" ? variation.weight : 0,
8149
- is_control: variation.is_control === true,
8150
- redirect_url: typeof variation.redirect_url === "string" ? variation.redirect_url : null,
8151
- mutations: []
8152
- }))
8153
- }
8154
- };
8155
- }
8156
- if (method === "GET" && (url.pathname === `/api/v1/experiments/${exp.id}` || url.pathname === `/api/v1/experiments/by-slug/${exp.slug}`)) {
8157
- return { ok: true, status: 200, data: exp };
8158
- }
8159
- if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/results`) {
8160
- const withheld = process.env.APEX_CLI_MOCK_RESULTS_STATE === "untrustworthy";
8161
- return {
8162
- ok: true,
8163
- status: 200,
8164
- data: {
8165
- experiment: { id: exp.id, name: exp.name, status: exp.status },
8166
- totals: { visitors: 128, primaryConversions: 18, revenue: 4200 },
8167
- arms: [
8168
- {
8169
- variationId: exp.variations?.[0]?.id,
8170
- name: "Control",
8171
- isControl: true,
8172
- visitors: 64,
8173
- primaryConversions: 8,
8174
- primaryRate: 0.125,
8175
- revenue: 2e3
8176
- },
8177
- {
8178
- variationId: exp.variations?.[1]?.id,
8179
- name: "Trust Badge",
8180
- isControl: false,
8181
- visitors: 64,
8182
- primaryConversions: 10,
8183
- primaryRate: 0.15625,
8184
- revenue: 2200
8185
- }
8186
- ],
8187
- decision: {
8188
- state: withheld ? "untrustworthy" : "collecting",
8189
- treatmentName: "Trust Badge",
8190
- lift: 25,
8191
- sequentialConfidence: withheld ? 0 : 62.5,
8192
- fixedConfidence: withheld ? 0 : 84,
8193
- judgedConfidence: withheld ? 0 : 84,
8194
- basis: withheld ? "sequential" : "fixed_horizon",
8195
- blockingGates: withheld ? ["mixed-currency"] : []
8196
- },
8197
- effective_outcome: {
8198
- outcome: withheld ? "needs_readout" : "winner",
8199
- statisticsOutcome: withheld ? "needs_readout" : "collecting",
8200
- provenance: withheld ? "statistics" : "manager_decided"
8201
- },
8202
- manager_decision: withheld ? null : {
8203
- outcome: "winner",
8204
- reason: "The business case is strong enough to ship.",
8205
- decided_by: { id: "user_1", name: "Maya Manager" },
8206
- decided_at: "2026-07-31T09:00:00.000Z",
8207
- version: 1
8208
- },
8209
- metrics: [],
8210
- series: [],
8211
- warnings: withheld ? [{ code: "mixed-currency", message: "Mixed currencies block the verdict." }] : []
8212
- }
8213
- };
8214
- }
8215
- if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/qa/run`) {
8216
- return {
8217
- ok: true,
8218
- status: 200,
8219
- data: {
8220
- run: {
8221
- id: "qa_run_1",
8222
- status: "in_review",
8223
- checklist: [],
8224
- urls: [],
8225
- automatedEvidence: null,
8226
- acceptanceCriteriaSummary: null,
8227
- goalVerification: null,
8228
- summary: {
8229
- status: "in_review",
8230
- required: 0,
8231
- completedRequired: 0,
8232
- failedRequired: 0,
8233
- optional: 0,
8234
- completedOptional: 0,
8235
- blockers: []
8236
- },
8237
- notes: null,
8238
- createdAt: "2026-07-04T00:00:00.000Z",
8239
- updatedAt: "2026-07-04T00:00:00.000Z",
8240
- approvedAt: null,
8241
- approvedBy: null,
8242
- completedAt: null
8243
- }
8244
- }
8245
- };
8246
- }
8247
- if (method === "PUT" && url.pathname === `/api/v1/experiments/by-slug/${exp.slug}`) {
8248
- const failureStatus = Number(
8249
- process.env.APEX_CLI_MOCK_UPSERT_FAILURE_STATUS ?? 0
8250
- );
8251
- if (failureStatus > 0) {
8252
- return {
8253
- ok: false,
8254
- status: failureStatus,
8255
- error: `Mock upsert failed with HTTP ${failureStatus}`
8256
- };
8257
- }
8258
- return { ok: true, status: 200, data: exp };
8259
- }
8260
- if (method === "POST" && url.pathname.endsWith("/filesystem/plan")) {
8261
- const files = body5?.files ?? [];
8262
- if (process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "invalid") {
8263
- return {
8264
- ok: true,
8265
- status: 200,
8266
- data: {
8267
- ok: false,
8268
- experimentId: exp.id,
8269
- status: "invalid",
8270
- changes: [],
8271
- errors: ["Mock filesystem plan error"],
8272
- warnings: [],
8273
- invalidatesQa: false,
8274
- precompileRequired: false
8275
- }
8276
- };
8277
- }
8278
- if (["blocking-only", "blocking-many"].includes(process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN ?? "")) {
8279
- const blockingIssues = [{
8280
- code: "custom_js_parse_error",
8281
- severity: "blocking",
8282
- message: "Custom JavaScript does not parse.",
8283
- path: "variations/control/index.js",
8284
- variation: "control"
8285
- }];
8286
- if (process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "blocking-many") {
8287
- blockingIssues.push(
8288
- { ...blockingIssues[0], code: "second_error", message: "Second blocking issue." },
8289
- { ...blockingIssues[0], code: "third_error", message: "Third blocking issue." }
8290
- );
8291
- }
8292
- return {
8293
- ok: true,
8294
- status: 200,
8295
- data: {
8296
- ok: false,
8297
- experimentId: exp.id,
8298
- status: "invalid",
8299
- changes: [],
8300
- errors: [],
8301
- warnings: [],
8302
- blockingIssues,
8303
- invalidatesQa: false,
8304
- precompileRequired: false
8305
- }
8306
- };
8307
- }
8308
- const observedPaths = files.map((file) => file.path).filter((filePath) => typeof filePath === "string");
8309
- return {
8310
- ok: true,
8311
- status: 200,
8312
- data: {
8313
- ok: true,
8314
- experimentId: exp.id,
8315
- status: "ok",
8316
- changes: files.length ? [
8317
- {
8318
- path: "experiment.json",
8319
- entity: "experiment",
8320
- action: "update",
8321
- summary: "Fixture plan change",
8322
- invalidatesQa: false
8323
- }
8324
- ] : [],
8325
- errors: [],
8326
- warnings: [],
8327
- invalidatesQa: false,
8328
- precompileRequired: false,
8329
- ...process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "echo-paths" ? { observedPaths } : {}
8330
- }
8331
- };
8332
- }
8333
- if (method === "POST" && url.pathname.endsWith("/filesystem/apply")) {
8334
- if (process.env.APEX_CLI_MOCK_FILESYSTEM_APPLY === "failed-with-plan") {
8335
- return {
8336
- ok: false,
8337
- status: 409,
8338
- error: "Mock apply failed",
8339
- data: {
8340
- applied: false,
8341
- plan: {
8342
- ok: false,
8343
- experimentId: exp.id,
8344
- status: "invalid",
8345
- changes: [],
8346
- errors: ["Mock apply plan error"],
8347
- warnings: [],
8348
- invalidatesQa: false,
8349
- precompileRequired: false
8350
- }
8351
- }
8352
- };
8353
- }
8354
- return {
8355
- ok: true,
8356
- status: 200,
8357
- data: {
8358
- applied: true,
8359
- plan: {
8360
- ok: true,
8361
- experimentId: exp.id,
8362
- status: "ok",
8363
- changes: [],
8364
- errors: [],
8365
- warnings: [],
8366
- invalidatesQa: false,
8367
- precompileRequired: false
8368
- }
8369
- }
8370
- };
8371
- }
8372
- if (url.pathname === "/api/v1/environments" && (method === "GET" || method === "PATCH")) {
8373
- if (method === "GET") mockEnvironmentReads += 1;
8374
- if (method === "PATCH") {
8375
- mockEnvironmentPatchAttempts += 1;
8376
- const conflictMode = process.env.APEX_CLI_MOCK_ENVIRONMENT_CONFLICT;
8377
- if (conflictMode === "always" || conflictMode === "once" && mockEnvironmentPatchAttempts === 1) {
8378
- return {
8379
- ok: false,
8380
- status: 409,
8381
- error: "Runtime settings changed since read \u2014 re-read and retry"
8382
- };
8383
- }
8384
- }
8385
- const defaultItems = [
8386
- {
8387
- key: "production",
8388
- name: "Production",
8389
- isDefault: true,
8390
- subSnippetEnabled: false,
8391
- domains: ["www.example.com"]
8392
- },
8393
- {
8394
- key: "staging",
8395
- name: "Staging",
8396
- isDefault: false,
8397
- subSnippetEnabled: true,
8398
- // The dev-bound doctor fixture binds a development host to a
8399
- // non-production environment so the doctor can prove it reports the
8400
- // binding instead of staying silent.
8401
- domains: process.env.APEX_CLI_MOCK_DOCTOR_STATE === "dev-bound" ? ["staging.example.dev"] : []
8402
- }
8403
- ];
8404
- const items = body5?.environments?.items ?? defaultItems;
8405
- const precompile = process.env.APEX_CLI_MOCK_PRECOMPILE_ERROR === "1" ? { status: "error", summary: "Mock edge precompile failed" } : { status: "ok", summary: "Mock edge precompile succeeded" };
8406
- return {
8407
- ok: true,
8408
- status: 200,
8409
- data: {
8410
- environments: { activeKey: "production", items },
8411
- version: `2026-07-19T10:0${mockEnvironmentReads}:00.000Z`,
8412
- ...method === "PATCH" ? { precompile } : {}
8413
- }
8414
- };
8415
- }
8416
- if (method === "GET" && url.pathname === "/api/v1/analytics/url-inventory") {
8417
- if (process.env.APEX_CLI_MOCK_URL_INVENTORY === "paged") {
8418
- const offset2 = Number(url.searchParams.get("offset") ?? "0");
8419
- const pages = {
8420
- "0": { items: [{ host: "example.com", path: "/products/page-one", pageviews: 42, visitors: 30 }], total: 2, truncated: true },
8421
- "200": { items: [{ host: "example.com", path: "/products/page-two", pageviews: 8, visitors: 6 }], total: 2, truncated: false }
8422
- };
8423
- const page = pages[String(offset2)] ?? { items: [{ host: "example.com", path: "/products/overrun", pageviews: 1, visitors: 1 }], total: 2, truncated: true };
8424
- return { ok: true, status: 200, data: page };
8425
- }
8426
- const state = process.env.APEX_CLI_MOCK_DOCTOR_STATE ?? "green";
8427
- const inventories = {
8428
- prefix: [
8429
- { host: "getkong.shop", path: "/", pageviews: 6e4, visitors: 2e4 },
8430
- { host: "getkong.delivery", path: "/checkout", pageviews: 48e3, visitors: 17e3 },
8431
- { host: "preview-abc-drip.vercel.app", path: "/", pageviews: 42, visitors: 12 }
8432
- ],
8433
- // Production-labeled traffic from one development host bound to the
8434
- // staging environment and one bound nowhere at all.
8435
- "dev-bound": [
8436
- { host: "www.example.com", path: "/", pageviews: 100, visitors: 80 },
8437
- // A production storefront on the public .dev TLD: real traffic, not
8438
- // development pollution.
8439
- { host: "store.example.dev", path: "/", pageviews: 90, visitors: 70 },
8440
- { host: "staging.example.dev", path: "/", pageviews: 22, visitors: 9 },
8441
- { host: "preview-xyz-drip.vercel.app", path: "/", pageviews: 15, visitors: 6 }
8442
- ]
8443
- };
8444
- const offset = Number(url.searchParams.get("offset") ?? 0);
8445
- if (state === "paginated") {
8446
- const pages = [
8447
- { items: [{ host: "www.example.com", path: "/", pageviews: 100, visitors: 80 }], truncated: true },
8448
- { items: [{ host: "preview-page-two.vercel.app", path: "/", pageviews: 12, visitors: 5 }], truncated: false }
8449
- ];
8450
- const page = pages[Math.min(Math.floor(offset / 500), pages.length - 1)];
8451
- return { ok: true, status: 200, data: { items: page.items, total: 2, truncated: page.truncated } };
8452
- }
8453
- if (state === "truncated") {
8454
- const items2 = Array.from({ length: 500 }, (_, index) => ({ host: "www.example.com", path: `/p/${offset + index}`, pageviews: 3, visitors: 2 }));
8455
- return { ok: true, status: 200, data: { items: items2, total: 1e5, truncated: true } };
8456
- }
8457
- const baseItems = [
8458
- { host: "www.example.com", path: "/", pageviews: 100, visitors: 80 },
8459
- { host: "example.com", path: "/products/demo", pageviews: 40, visitors: 30 }
8460
- ];
8461
- const bestellung = url.searchParams.get("days") === "30" ? { host: "example.com", path: "/bestellung-abgeschlossen", pageviews: 12, visitors: 10 } : { host: "getkong.test", path: "/bestellung-abgeschlossen", pageviews: 12, visitors: 10 };
8462
- const items = inventories[state] ?? [...baseItems, bestellung];
8463
- const hostFilter = url.searchParams.get("host")?.trim().toLowerCase();
8464
- const pathContains = url.searchParams.get("pathContains") ?? "";
8465
- const filtered = items.filter(
8466
- (item) => (!hostFilter || item.host.toLowerCase() === hostFilter) && (!pathContains || item.path.includes(pathContains))
8467
- );
8468
- return { ok: true, status: 200, data: { items: filtered, total: filtered.length, truncated: false } };
8469
- }
8470
- if (method === "GET" && url.pathname === "/api/v1/analytics/visitor-series") {
8471
- if (process.env.APEX_CLI_MOCK_DOCTOR_STATE === "series-down") {
8472
- return { ok: false, status: 503, error: "Analytics is temporarily unavailable" };
8473
- }
8474
- return { ok: true, status: 200, data: { items: [
8475
- { date: "2026-07-28", visitors: 100, pageviews: 200 },
8476
- { date: "2026-07-29", visitors: 110, pageviews: 220 },
8477
- { date: "2026-08-03", visitors: 70, pageviews: 140 },
8478
- { date: "2026-08-04", visitors: 77, pageviews: 154 }
8479
- ] } };
8480
- }
8481
- if (url.pathname === "/api/v1/consent" && (method === "GET" || method === "PATCH")) {
8482
- const patch = body5;
8483
- const enabled = typeof patch?.consentMode?.enabled === "boolean" ? patch.consentMode.enabled : true;
8484
- const provider = typeof patch?.consentMode?.provider === "string" ? patch.consentMode.provider : "cookiescript";
8485
- const requiredGroup = typeof patch?.consentMode?.requiredGroup === "string" ? patch.consentMode.requiredGroup : "analytics";
8486
- const getter = typeof patch?.consentMode?.getter === "string" ? patch.consentMode.getter : "";
8487
- if (method === "PATCH" && patch?.applyCanonicalSplit === false) {
8488
- return { ok: false, status: 403, error: "applyCanonicalSplit: false is not permitted with the consent:write scope \u2014 clear those prerequisites in the dashboard (shop:settings)." };
8489
- }
8490
- return { ok: true, status: 200, data: {
8491
- consentMode: { enabled, provider, requiredGroup, getter },
8492
- prerequisites: {
8493
- showVariationsRule: "return true;",
8494
- startTrackingRule: "return drip.get('trackingConsentEnabled');"
8495
- },
8496
- ...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {}
8497
- } };
8498
- }
8499
- if (url.pathname === "/api/v1/install-domains" && (method === "GET" || method === "PATCH")) {
8500
- const conflictMode = process.env.APEX_CLI_MOCK_DOMAINS_CONFLICT;
8501
- const serverDomains = conflictMode === "once" && mockDomainsPatchAttempts >= 1 ? ["shop.example.com", "peer.example.com"] : ["shop.example.com"];
8502
- if (method === "PATCH") {
8503
- mockDomainsPatchAttempts += 1;
8504
- if (conflictMode === "always" || conflictMode === "once" && mockDomainsPatchAttempts === 1) {
8505
- return { ok: false, status: 409, error: "Install domains changed since read \u2014 re-read and retry" };
8506
- }
8507
- }
8508
- const domains = body5?.domains ?? serverDomains;
8509
- const additionalDomains = method === "PATCH" ? domains : serverDomains;
8510
- return { ok: true, status: 200, data: {
8511
- domain: "example.com",
8512
- additionalDomains,
8513
- storeOrigins: ["https://example.com", "https://www.example.com", ...additionalDomains.map((domain) => `https://${domain}`)],
8514
- revision: method === "PATCH" ? "rev_patched" : `rev_${serverDomains.length}`,
8515
- ...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {}
8516
- } };
8517
- }
8518
- if (url.pathname === "/api/v1/external-tracking" && (method === "GET" || method === "PATCH")) {
8519
- const current = { domains: ["www.example.com", "shop.example.com"], handoverQueryParam: "drip_bucket", handoverTtlSeconds: 120, handoverMintTimeoutMs: 250, signed: true };
8520
- const patch = body5?.externalTracking;
8521
- const next = { ...current, ...patch ?? {} };
8522
- return { ok: true, status: 200, data: { externalTracking: next, revision: "2026-08-05T10:00:00.000Z", ...method === "PATCH" ? { precompile: { status: "ok", summary: "Mock edge precompile succeeded" } } : {} } };
8523
- }
8524
- const analyticsMatch = url.pathname.match(/^\/api\/v1\/analytics\/(stats|timeseries|traffic|pages|geo|sessions|web-vitals|realtime|entry-exit)$/);
8525
- if (method === "GET" && analyticsMatch) {
8526
- const fixtures = {
8527
- stats: [{ date: "2026-07-19", visitors: 120, conversions: 12, revenue: 540, foreign_revenue_buckets: [], converted_revenue: null, excluded_foreign_revenue: null, unconverted_revenue: null, unknown_currency_orders: null, unknown_currency_revenue: null }],
8528
- timeseries: [{ date: "2026-07-19", visitors: 120, sessions: 132, pageviews: 280, conversions: 12, revenue: 540 }],
8529
- traffic: [{ referrer: "google.com", utm_source: "google", utm_medium: "organic", utm_campaign: "", visitors: 80, sessions: 88, pageviews: 160 }],
8530
- pages: [{ page_url: "/products/demo", unique_visitors: 90, pageviews: 140, planning_days: Number(url.searchParams.get("planningDays") ?? 180) }],
8531
- geo: [{ country: "DE", device: "desktop", visitors: 70, sessions: 76, pageviews: 150 }],
8532
- sessions: [{ total_sessions: 132, avg_pages_per_session: 2.1, avg_session_duration_seconds: 94, bounce_rate: 0.32, median_pages_per_session: 2 }],
8533
- "web-vitals": [{ metric: "LCP", route: "/products/demo", p75: 1800, sample_count: 40, good_count: 35, poor_count: 2 }],
8534
- realtime: { summary: { active_visitors: 4, active_sessions: 5, events_last_5min: 18 }, activeVisitors: [], recentEvents: [] },
8535
- "entry-exit": [{ type: "entry", page_url: "/", sessions: 45 }]
8536
- };
8537
- return { ok: true, status: 200, data: { data: fixtures[analyticsMatch[1]] } };
8538
- }
8539
- if (method === "POST" && url.pathname === "/api/v1/analytics/suggest-goals") {
8540
- return { ok: true, status: 200, data: { goals: [{ name: "Purchase", type: "pageview", urlPattern: "/bestellung-abgeschlossen", explanation: "Fixture" }], funnels: [], triggers: [] } };
8541
- }
8542
- if (method === "GET" && url.pathname === "/api/v1/funnels") {
8543
- return { ok: true, status: 200, data: { data: [mockFunnel()] } };
8544
- }
8545
- if (method === "POST" && url.pathname === "/api/v1/funnels") {
8546
- return { ok: true, status: 201, data: { ...mockFunnel(), ...body5 } };
8547
- }
8548
- if (method === "GET" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
8549
- return { ok: true, status: 200, data: mockFunnel() };
8550
- }
8551
- if (method === "PATCH" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
8552
- return { ok: true, status: 200, data: { ...mockFunnel(), ...body5 } };
8553
- }
8554
- if (method === "DELETE" && url.pathname === `/api/v1/funnels/${mockFunnel().id}`) {
8555
- return { ok: true, status: 200, data: { ok: true } };
8556
- }
8557
- if (method === "GET" && url.pathname === `/api/v1/funnels/${mockFunnel().id}/run`) {
8558
- return { ok: true, status: 200, data: { total_sessions_analyzed: 2, steps: [{ step: 1, label: "Collection", url_pattern: "/collections/*", visitors: 2, drop_off_rate: 0, matched_pageviews: 2 }] } };
8559
- }
8560
- if (method === "GET" && url.pathname.endsWith("/workspace")) {
8561
- const files = [
8562
- ["experiment.config.json", JSON.stringify({ name: exp.name, storeUrl: exp.store_url, experimentSlug: exp.slug, variationSlug: "badge" }, null, 2) + "\n", "application/json", "config"],
8563
- ["metrics.json", '{"primary":null,"secondary":[]}\n', "application/json", "metrics"],
8564
- ["targeting.json", '{"url":[],"segments":[]}\n', "application/json", "targeting"],
8565
- ["src/index.js", "window.__apexOperatorFixture = true;\n", "application/javascript", "custom_js"],
8566
- ["src/index.scss", ".apex-operator-fixture { display: block; }\n", "text/x-scss", "custom_css"],
8567
- ["src/mutations.json", "[]\n", "application/json", "mutations"],
8568
- ["dev-doc.md", "# Fixture workspace\n", "text/markdown", "doc"],
8569
- ["brand.md", "# Fixture brand\n", "text/markdown", "brand"]
8570
- ];
8571
- return {
8572
- ok: true,
8573
- status: 200,
8574
- data: {
8575
- workspace: {
8576
- id: "workspace_1",
8577
- schemaVersion: 1,
8578
- status: "draft",
8579
- currentRevisionId: "revision_1",
8580
- updatedAt: "2026-07-30T00:00:00.000Z"
8581
- },
8582
- files: files.map(([filePath, content, contentType, kind5]) => ({
8583
- path: filePath,
8584
- kind: kind5,
8585
- contentType,
8586
- content,
8587
- sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex"),
8588
- sizeBytes: Buffer.byteLength(content, "utf8"),
8589
- updatedAt: "2026-07-30T00:00:00.000Z"
8590
- }))
8591
- }
8592
- };
8593
- }
8594
- if (method === "PUT" && url.pathname.includes("/variations/") && url.pathname.includes("/files/")) {
8595
- return { ok: true, status: 200, data: { ok: true } };
8596
- }
8597
- if (method === "POST" && url.pathname.endsWith("/build")) {
8598
- return {
8599
- ok: true,
8600
- status: 200,
8601
- data: { ok: true, mutationCount: 0, customCssBytes: 42, customJsBytes: 58 }
8602
- };
8603
- }
8604
- if (method === "POST" && url.pathname === "/api/v1/harness/sessions") {
8605
- return { ok: true, status: 201, data: { sessionId: "harness_1", sessionToken: "token", experimentId: exp.id, variationId: exp.variations?.[1]?.id ?? "variation_1", expiresAt: "2026-07-30T01:00:00.000Z", previewUrl: "https://preview.drip-apex.com/api/preview?pst=harness_1" } };
8606
- }
8607
- if (method === "POST" && url.pathname === "/api/v1/harness/sessions/harness_1/versions") {
8608
- return { ok: true, status: 201, data: { sessionId: "harness_1", version: 1, expiresAt: "2026-07-30T01:00:00.000Z", jsArtifactId: "asset_js", cssArtifactId: "asset_css" } };
8609
- }
8610
- if (method === "POST" && url.pathname === "/api/v1/harness/sessions/harness_1/publish") {
8611
- return { ok: true, status: 200, data: { ok: true } };
8612
- }
8613
- if (method === "GET" && url.pathname === "/api/v1/goals") {
8614
- if (process.env.APEX_CLI_MOCK_DOCTOR_STATE === "goals-paginated") {
8615
- const cursor = url.searchParams.get("cursor");
8616
- return cursor === "goal-page-2" ? {
8617
- ok: true,
8618
- status: 200,
8619
- data: {
8620
- data: [{ ...mockGoal(), id: "00000000-0000-4000-8000-0000000000f2", name: "Unreachable landing", type: "pageview", url_pattern: "^/never-visited-page$" }],
8621
- pagination: { has_more: false, next_cursor: null }
8622
- }
8623
- } : {
8624
- ok: true,
8625
- status: 200,
8626
- data: {
8627
- data: [mockGoal()],
8628
- pagination: { has_more: true, next_cursor: "goal-page-2" }
8629
- }
8630
- };
8631
- }
8632
- if (process.env.APEX_CLI_MOCK_GOAL_PAGES === "2") {
8633
- if (!url.searchParams.get("cursor")) {
8634
- return {
8635
- ok: true,
8636
- status: 200,
8637
- data: {
8638
- data: [mockGoal()],
8639
- pagination: { has_more: true, next_cursor: "goals_page_2" }
8640
- }
8641
- };
8642
- }
8643
- return {
8644
- ok: true,
8645
- status: 200,
8646
- data: {
8647
- data: [{
8648
- ...mockGoal(),
8649
- id: SECOND_PAGE_GOAL_ID,
8650
- name: "Fixture pageview",
8651
- type: "pageview",
8652
- event_name: null,
8653
- url_pattern: "bestellung-abgeschlossen"
8654
- }],
8655
- pagination: { has_more: false, next_cursor: null }
8656
- }
8657
- };
8658
- }
8659
- return {
8660
- ok: true,
8661
- status: 200,
8662
- data: {
8663
- data: [mockGoal(), mockPageScopedGoal()],
8664
- pagination: { has_more: false, next_cursor: null }
8665
- }
8666
- };
8667
- }
8668
- if (method === "GET" && url.pathname === "/api/v1/brand-context") {
8669
- return {
8670
- ok: true,
8671
- status: 200,
8672
- data: { data: mockBrandContext() }
8673
- };
8674
- }
8675
- if (method === "POST" && url.pathname === "/api/v1/goals") {
8676
- return {
8677
- ok: true,
8678
- status: 201,
8679
- data: { ...mockGoal(), ...body5 }
8680
- };
8681
- }
8682
- if (method === "PATCH" && url.pathname === `/api/v1/goals/${mockGoal().id}`) {
8683
- return {
8684
- ok: true,
8685
- status: 200,
8686
- data: { ...mockGoal(), ...body5 }
8687
- };
8688
- }
8689
- if (method === "GET" && url.pathname === "/api/v1/tracking/verify") {
8690
- if (process.env.APEX_CLI_MOCK_TRACKING_VERIFY === "unreachable") {
8691
- throw new Error("Mock tracking verify transport failure");
8692
- }
8693
- return {
8694
- ok: true,
8695
- status: 200,
8696
- data: {
8697
- ok: true,
8698
- window: { minutes: Number(url.searchParams.get("minutes") ?? 120) },
8699
- filters: {
8700
- experiment_id: url.searchParams.get("experiment_id"),
8701
- event_types: []
8702
- },
8703
- totals: {
8704
- events: 2,
8705
- unique_visitors: 2,
8706
- last_event_at: "2026-07-04T12:00:00.000Z",
8707
- event_types: { pageview: 2 }
8708
- },
8709
- recent_events: [
8710
- {
8711
- event_type: "pageview",
8712
- visitor_id: "visitor_1",
8713
- url: "https://example.com/products/demo",
8714
- timestamp: "2026-07-04T12:00:00.000Z"
8715
- }
8716
- ]
8717
- }
8718
- };
8719
- }
8720
- if (method === "GET" && url.pathname === "/api/v1/targeting/test") {
8721
- return {
8722
- ok: true,
8723
- status: 200,
8724
- data: {
8725
- url: url.searchParams.get("url"),
8726
- statuses: ["draft", "running", "paused"],
8727
- matching_count: 1,
8728
- experiments: [
8729
- {
8730
- experiment: exp,
8731
- matches: true,
8732
- has_url_rules: true,
8733
- rules: [
8734
- {
8735
- source: "url",
8736
- pattern: "/products/*",
8737
- type: "simple",
8738
- include: true,
8739
- matched: true,
8740
- matchType: "wildcard"
8741
- }
8742
- ],
8743
- caveats: [],
8744
- variations: exp.variations
8745
- }
8746
- ]
8747
- }
8748
- };
8749
- }
8750
- if (method === "POST" && url.pathname === "/api/v1/sos") {
8751
- return {
8752
- ok: true,
8753
- status: 201,
8754
- data: {
8755
- ticket: {
8756
- id: "00000000-0000-4000-8000-0000000000aa",
8757
- jira_key: "AWF-123",
8758
- jira_status: "Queued",
8759
- mapped_status: "queued",
8760
- type: body5?.type ?? "qa",
8761
- sub_class: null,
8762
- loom_url: body5?.loom_url ?? null,
8763
- description: body5?.description ?? "Fixture SOS",
8764
- created_at: "2026-07-04T12:00:00.000Z",
8765
- updated_at: "2026-07-04T12:00:00.000Z"
8766
- },
8767
- reused: false,
8768
- jira: {
8769
- key: "AWF-123",
8770
- url: "https://jira.example.test/browse/AWF-123"
8771
- }
8772
- }
8773
- };
8774
- }
8775
- if (method === "GET" && url.pathname === "/api/v1/sos/mine") {
8776
- return {
8777
- ok: true,
8778
- status: 200,
8779
- data: {
8780
- tickets: [
8781
- {
8782
- id: "00000000-0000-4000-8000-0000000000aa",
8783
- jira_key: "AWF-123",
8784
- mapped_status: "queued",
8785
- my_status: "queued",
8786
- type: "qa",
8787
- description: "Fixture SOS",
8788
- created_at: "2026-07-04T12:00:00.000Z",
8789
- updated_at: "2026-07-04T12:00:00.000Z"
8790
- }
8791
- ]
8792
- }
7428
+ baseUrl: envUrl ?? "https://app.drip-apex.com",
7429
+ userApiKey: fileCreds?.userApiKey,
7430
+ userBaseUrl: fileCreds?.userBaseUrl ?? fileCreds?.baseUrl,
7431
+ context: fileCreds?.context
8793
7432
  };
8794
7433
  }
8795
- if (method === "GET" && url.pathname === "/api/v1/me") {
8796
- return {
8797
- ok: true,
8798
- status: 200,
8799
- data: {
8800
- data: {
8801
- user_id: "00000000-0000-4000-8000-000000000001",
8802
- org_id: "00000000-0000-4000-8000-000000000002",
8803
- org_role: "owner",
8804
- scopes: ["account:read"],
8805
- expires_at: "2026-10-18T00:00:00.000Z"
8806
- }
8807
- }
8808
- };
7434
+ return fileCreds;
7435
+ }
7436
+ function saveCredentials(creds) {
7437
+ const { credentialKind: _kind, resolvedContext: _resolved, ...persisted } = creds;
7438
+ import_node_fs2.default.mkdirSync(DRIP_DIR, { recursive: true });
7439
+ import_node_fs2.default.writeFileSync(CREDS_FILE, JSON.stringify(persisted, null, 2) + "\n", {
7440
+ mode: 384
7441
+ });
7442
+ }
7443
+ function clearCredentials() {
7444
+ try {
7445
+ import_node_fs2.default.unlinkSync(CREDS_FILE);
7446
+ } catch {
8809
7447
  }
8810
- if (method === "GET" && url.pathname === "/api/v1/account/profile") {
8811
- return { ok: true, status: 200, data: { data: { displayName: "Fixture User", avatarDataUrl: null } } };
7448
+ }
7449
+ function requireCredentials() {
7450
+ const creds = getCredentials();
7451
+ if (!creds) {
7452
+ throw new CliError(
7453
+ 'Not logged in. Run "apex login" first, or set DRIP_API_KEY env var.',
7454
+ EXIT_CODES.auth
7455
+ );
8812
7456
  }
8813
- if (method === "GET" && url.pathname === "/api/v1/me/context") {
8814
- return { ok: true, status: 200, data: { data: { user_id: "00000000-0000-4000-8000-0000000000a1", key: { id: "00000000-0000-4000-8000-0000000000a2", account_scoped: true, org_id: null, expires_at: "2027-01-01T00:00:00.000Z" }, orgs: Array.from({ length: 6 }, (_, index) => ({ id: `00000000-0000-4000-8000-0000000001${index}`, name: `Fixture Org ${index + 1}`, role: index ? "member" : "owner", shops: [{ id: `00000000-0000-4000-8000-0000000002${index}`, name: `Fixture Shop ${index + 1}`, domain: `shop-${index + 1}.example.test`, role: index ? "viewer" : "admin" }, ...index === 0 ? [{ id: "00000000-0000-4000-8000-0000000003a0", name: "Twin Shop", domain: "twin-a.example.test", role: "admin" }, { id: "00000000-0000-4000-8000-0000000003a1", name: "Twin Shop", domain: "twin-b.example.test", role: "admin" }] : []] })) } } };
7457
+ const context = resolveContext(creds, void 0, defaultCredentialKind(creds));
7458
+ if (!process.env.DRIP_API_KEY && creds.userApiKey) {
7459
+ setEnvelopeContext({ credential: "user", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7460
+ if (!context.shopId) {
7461
+ throw new CliError(
7462
+ 'Shop context is required. Run "apex use", or pass --shop <id>.',
7463
+ EXIT_CODES.validation,
7464
+ { code: "shop_context_required", retry: "apex use <org>/<shop>", flag: "--shop" }
7465
+ );
7466
+ }
7467
+ return { ...creds, apiKey: creds.userApiKey, baseUrl: creds.userBaseUrl ?? creds.baseUrl, credentialKind: "user", resolvedContext: context };
8815
7468
  }
8816
- if (method === "PATCH" && url.pathname === "/api/v1/account/profile") {
8817
- return { ok: true, status: 200, data: { data: { displayName: body5?.displayName ?? "Fixture User", avatarDataUrl: body5?.avatarDataUrl ?? null } } };
7469
+ setEnvelopeContext({ credential: "shop", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7470
+ return { ...creds, credentialKind: "shop", resolvedContext: context };
7471
+ }
7472
+ function requireUserCredentials() {
7473
+ const creds = getCredentials();
7474
+ if (!creds?.userApiKey) {
7475
+ throw new CliError(
7476
+ 'This command needs a user credential (apx_uk_...). Run "apex login" first.',
7477
+ EXIT_CODES.auth
7478
+ );
8818
7479
  }
8819
- if (method === "GET" && url.pathname === "/api/v1/keys") {
8820
- return { ok: true, status: 200, data: { data: [{ id: "00000000-0000-4000-8000-000000000011", name: "Fixture key", key_prefix: "apx_sk_live_fixture", type: "live_secret", permissions: ["*"], expires_at: null, created_at: "2026-07-20T00:00:00.000Z" }] } };
7480
+ const context = resolveContext(creds, void 0, "user");
7481
+ setEnvelopeContext({ credential: "user", org_id: context.orgId ?? null, shop_id: context.shopId ?? null });
7482
+ return { ...creds, apiKey: creds.userApiKey, baseUrl: creds.userBaseUrl ?? creds.baseUrl, credentialKind: "user", resolvedContext: context };
7483
+ }
7484
+ var CONFIG_NAMES = [
7485
+ ".drip.config.ts",
7486
+ ".drip.config.js",
7487
+ "drip.config.ts",
7488
+ "drip.config.js"
7489
+ ];
7490
+ function findConfigPath() {
7491
+ const cwd = process.cwd();
7492
+ for (const name of CONFIG_NAMES) {
7493
+ const p = import_node_path2.default.join(cwd, name);
7494
+ if (import_node_fs2.default.existsSync(p)) return p;
8821
7495
  }
8822
- if (method === "POST" && url.pathname === "/api/v1/keys") {
8823
- return { ok: true, status: 201, data: { data: { id: "00000000-0000-4000-8000-000000000011", name: body5?.name ?? "Fixture key", key: "apx_sk_live_fixture-value-once", key_prefix: "apx_sk_live_fixture", type: body5?.type ?? "live_secret", created_at: "2026-07-20T00:00:00.000Z" } } };
7496
+ return null;
7497
+ }
7498
+
7499
+ // ../api-client/src/http.ts
7500
+ var APEX_API_VERSION_HEADER = "x-apex-api-version";
7501
+ var SUPPORTED_APEX_API_VERSION = "1";
7502
+ function extractErrorMessage(json, status2) {
7503
+ if (!json) return `HTTP ${status2}`;
7504
+ const error = json.error;
7505
+ if (typeof error === "string" && error.trim()) return error;
7506
+ if (error && typeof error === "object") {
7507
+ const message = error.message;
7508
+ if (typeof message === "string" && message.trim()) return message;
8824
7509
  }
8825
- if (method === "DELETE" && url.pathname.startsWith("/api/v1/keys/")) {
8826
- return { ok: true, status: 200, data: { data: { deleted: true, id: url.pathname.split("/").pop() } } };
7510
+ return typeof json.message === "string" && json.message.trim() ? json.message : `HTTP ${status2}`;
7511
+ }
7512
+ function extractErrorDetails(json) {
7513
+ if (!json) return void 0;
7514
+ const nested = json.error && typeof json.error === "object" && !Array.isArray(json.error) ? json.error : null;
7515
+ const code = typeof json.code === "string" ? json.code : typeof nested?.code === "string" ? nested.code : void 0;
7516
+ for (const value of [json.details, json.meta, nested?.details]) if (value && typeof value === "object" && !Array.isArray(value)) return { ...value, ...code ? { code } : {} };
7517
+ const details = Object.fromEntries(["limit", "current", "max", "actual", "field", "code"].filter((key) => key in json).map((key) => [key, json[key]]));
7518
+ if (code) details.code = code;
7519
+ return Object.keys(details).length ? details : void 0;
7520
+ }
7521
+ var warnedAboutVersionSkew = false;
7522
+ function createHttpClient(creds, options) {
7523
+ const warn3 = options.warn ?? ((message) => process.stderr.write(message));
7524
+ function inspectVersion(headers) {
7525
+ const value = headers.get(APEX_API_VERSION_HEADER);
7526
+ if (!value || warnedAboutVersionSkew) return;
7527
+ const version = Number(value);
7528
+ if (!Number.isFinite(version) || version === Number(SUPPORTED_APEX_API_VERSION)) return;
7529
+ warnedAboutVersionSkew = true;
7530
+ warn3(version > 1 ? "CLI is behind the server \u2014 update.\n" : "Server API is older than this CLI supports.\n");
7531
+ }
7532
+ async function request2(method, path21, body5, requestTimeoutMs, form = false) {
7533
+ const mock = options.transport?.(method, path21, body5);
7534
+ if (mock !== void 0) {
7535
+ const version = options.testTransportVersion?.();
7536
+ if (version) inspectVersion(new Headers({ [APEX_API_VERSION_HEADER]: version }));
7537
+ return mock;
7538
+ }
7539
+ const fallback2 = form ? 12e4 : 3e4;
7540
+ const configured = Number(process.env.APEX_TIMEOUT_MS ?? fallback2);
7541
+ const defaultMs = Number.isFinite(configured) && configured > 0 ? configured : fallback2;
7542
+ const timeoutMs = requestTimeoutMs === void 0 ? defaultMs : Math.max(1, Math.min(defaultMs, requestTimeoutMs));
7543
+ const headers = { Authorization: `Bearer ${creds.apiKey}` };
7544
+ if (!form) headers["Content-Type"] = "application/json";
7545
+ if (creds.credentialKind === "user" || creds.apiKey.startsWith("apx_uk_")) {
7546
+ if (creds.resolvedContext?.orgId) headers["X-Apex-Org"] = creds.resolvedContext.orgId;
7547
+ if (creds.resolvedContext?.shopId) headers["X-Apex-Shop"] = creds.resolvedContext.shopId;
7548
+ }
7549
+ let response;
7550
+ try {
7551
+ response = await fetch(`${creds.baseUrl}${path21}`, { method, headers, body: form ? body5 : body5 ? JSON.stringify(body5) : void 0, signal: AbortSignal.timeout(timeoutMs) });
7552
+ } catch (error) {
7553
+ return options.onNetworkError({ baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) });
7554
+ }
7555
+ inspectVersion(response.headers);
7556
+ const json = await response.json().catch(() => null);
7557
+ if (!response.ok) return { ok: false, status: response.status, data: form ? void 0 : json, error: extractErrorMessage(json, response.status), errorDetails: extractErrorDetails(json), requestId: response.headers.get("x-request-id"), rateLimit: { limit: response.headers.get("x-ratelimit-limit"), remaining: response.headers.get("x-ratelimit-remaining"), reset: response.headers.get("x-ratelimit-reset") } };
7558
+ return { ok: true, status: response.status, data: json };
8827
7559
  }
8828
- return {
8829
- ok: false,
8830
- status: 404,
8831
- error: `Mock API route not found: ${method} ${url.pathname}`
8832
- };
7560
+ return { request: (method, path21, body5, timeoutMs) => request2(method, path21, body5, timeoutMs), requestForm: (method, path21, body5) => request2(method, path21, body5, void 0, true) };
7561
+ }
7562
+
7563
+ // src/api.ts
7564
+ var testTransport;
7565
+ var testScreenshotDelay;
7566
+ var testScreenshotNow;
7567
+ function screenshotNow() {
7568
+ return testScreenshotNow?.() ?? Date.now();
8833
7569
  }
8834
7570
  function getUserKeyIdentity(creds) {
8835
7571
  return request(
@@ -8845,8 +7581,8 @@ function getUserKeyIdentity(creds) {
8845
7581
  function getAccountContext(creds) {
8846
7582
  return userRequest(creds, "GET", "/api/v1/me/context");
8847
7583
  }
8848
- function userRequest(creds, method, path20, body5) {
8849
- return request({ ...creds, apiKey: creds.userApiKey ?? "", baseUrl: creds.userBaseUrl ?? creds.baseUrl }, method, path20, body5);
7584
+ function userRequest(creds, method, path21, body5) {
7585
+ return request({ ...creds, apiKey: creds.userApiKey ?? "", baseUrl: creds.userBaseUrl ?? creds.baseUrl }, method, path21, body5);
8850
7586
  }
8851
7587
  function getAccountProfile(creds) {
8852
7588
  return userRequest(creds, "GET", "/api/v1/account/profile");
@@ -8863,114 +7599,30 @@ function createUserManagedKey(creds, body5) {
8863
7599
  function revokeUserManagedKey(creds, id, shopId) {
8864
7600
  return userRequest(creds, "DELETE", `/api/v1/keys/${encodeURIComponent(id)}?shop_id=${encodeURIComponent(shopId)}`);
8865
7601
  }
8866
- async function request(creds, method, path20, body5, requestTimeoutMs) {
8867
- const mock = mockResponse(method, path20, body5);
8868
- if (mock) {
8869
- const mockVersion = process.env.APEX_CLI_MOCK_API_VERSION;
8870
- if (mockVersion) warnAboutApiVersionSkew(new Headers({ [APEX_API_VERSION_HEADER]: mockVersion }));
8871
- return mock;
8872
- }
8873
- const url = `${creds.baseUrl}${path20}`;
8874
- const configuredTimeout = Number(process.env.APEX_TIMEOUT_MS ?? "30000");
8875
- const defaultTimeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 3e4;
8876
- const timeoutMs = requestTimeoutMs !== void 0 ? Math.max(1, Math.min(defaultTimeoutMs, requestTimeoutMs)) : defaultTimeoutMs;
8877
- const headers = {
8878
- Authorization: `Bearer ${creds.apiKey}`,
8879
- "Content-Type": "application/json"
8880
- };
8881
- if (creds.credentialKind === "user" || creds.apiKey.startsWith("apx_uk_")) {
8882
- if (creds.resolvedContext?.orgId) headers["X-Apex-Org"] = creds.resolvedContext.orgId;
8883
- if (creds.resolvedContext?.shopId) headers["X-Apex-Shop"] = creds.resolvedContext.shopId;
8884
- }
8885
- let res;
8886
- try {
8887
- res = await fetch(url, {
8888
- method,
8889
- headers,
8890
- body: body5 ? JSON.stringify(body5) : void 0,
8891
- signal: AbortSignal.timeout(timeoutMs)
8892
- });
8893
- } catch (error) {
8894
- const seconds = timeoutMs / 1e3;
8895
- throw new CliError(
8896
- `Could not reach ${creds.baseUrl}: check DRIP_BASE_URL / network; server did not respond within ${seconds}s.`,
8897
- EXIT_CODES.general,
8898
- { baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) }
8899
- );
8900
- }
8901
- warnAboutApiVersionSkew(res.headers);
8902
- const json = await res.json().catch(() => null);
8903
- if (!res.ok) {
8904
- return {
8905
- ok: false,
8906
- status: res.status,
8907
- data: json,
8908
- error: extractErrorMessage(json, res.status),
8909
- errorDetails: extractErrorDetails(json),
8910
- requestId: res.headers.get("x-request-id"),
8911
- rateLimit: {
8912
- limit: res.headers.get("x-ratelimit-limit"),
8913
- remaining: res.headers.get("x-ratelimit-remaining"),
8914
- reset: res.headers.get("x-ratelimit-reset")
7602
+ var sharedClients = /* @__PURE__ */ new WeakMap();
7603
+ function sharedClient(creds) {
7604
+ let client = sharedClients.get(creds);
7605
+ if (!client) {
7606
+ client = createHttpClient(creds, {
7607
+ transport: testTransport,
7608
+ testTransportVersion: () => testTransport ? process.env[["APEX", "CLI", "MOCK", "API", "VERSION"].join("_")] : void 0,
7609
+ onNetworkError: (info) => {
7610
+ throw new CliError(
7611
+ `Could not reach ${info.baseUrl}: check DRIP_BASE_URL / network; server did not respond within ${info.timeoutMs / 1e3}s.`,
7612
+ EXIT_CODES.general,
7613
+ { ...info }
7614
+ );
8915
7615
  }
8916
- };
8917
- }
8918
- return {
8919
- ok: true,
8920
- status: res.status,
8921
- // Do not unwrap `json.data` automatically: some endpoints return
8922
- // `{ data, pagination }`, and callers need the full shape.
8923
- data: json
8924
- };
8925
- }
8926
- async function requestForm(creds, method, path20, body5) {
8927
- const mock = mockResponse(method, path20, body5);
8928
- if (mock) {
8929
- const mockVersion = process.env.APEX_CLI_MOCK_API_VERSION;
8930
- if (mockVersion) warnAboutApiVersionSkew(new Headers({ [APEX_API_VERSION_HEADER]: mockVersion }));
8931
- return mock;
8932
- }
8933
- const url = `${creds.baseUrl}${path20}`;
8934
- const configuredTimeout = Number(process.env.APEX_TIMEOUT_MS ?? "120000");
8935
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 12e4;
8936
- let res;
8937
- try {
8938
- const headers = { Authorization: `Bearer ${creds.apiKey}` };
8939
- if (creds.credentialKind === "user" || creds.apiKey.startsWith("apx_uk_")) {
8940
- if (creds.resolvedContext?.orgId) headers["X-Apex-Org"] = creds.resolvedContext.orgId;
8941
- if (creds.resolvedContext?.shopId) headers["X-Apex-Shop"] = creds.resolvedContext.shopId;
8942
- }
8943
- res = await fetch(url, {
8944
- method,
8945
- headers,
8946
- body: body5,
8947
- signal: AbortSignal.timeout(timeoutMs)
8948
7616
  });
8949
- } catch (error) {
8950
- const seconds = timeoutMs / 1e3;
8951
- throw new CliError(
8952
- `Could not reach ${creds.baseUrl}: check DRIP_BASE_URL / network; server did not respond within ${seconds}s.`,
8953
- EXIT_CODES.general,
8954
- { baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) }
8955
- );
8956
- }
8957
- warnAboutApiVersionSkew(res.headers);
8958
- const json = await res.json().catch(() => null);
8959
- if (!res.ok) {
8960
- return {
8961
- ok: false,
8962
- status: res.status,
8963
- error: extractErrorMessage(json, res.status),
8964
- errorDetails: extractErrorDetails(json),
8965
- requestId: res.headers.get("x-request-id"),
8966
- rateLimit: {
8967
- limit: res.headers.get("x-ratelimit-limit"),
8968
- remaining: res.headers.get("x-ratelimit-remaining"),
8969
- reset: res.headers.get("x-ratelimit-reset")
8970
- }
8971
- };
7617
+ sharedClients.set(creds, client);
8972
7618
  }
8973
- return { ok: true, status: res.status, data: json };
7619
+ return client;
7620
+ }
7621
+ async function request(creds, method, path21, body5, requestTimeoutMs) {
7622
+ return sharedClient(creds).request(method, path21, body5, requestTimeoutMs);
7623
+ }
7624
+ async function requestForm(creds, method, path21, body5) {
7625
+ return sharedClient(creds).requestForm(method, path21, body5);
8974
7626
  }
8975
7627
  function listExperiments(creds, opts) {
8976
7628
  const params = new URLSearchParams();
@@ -9006,9 +7658,6 @@ function upsertExperimentBySlug(creds, slug, body5) {
9006
7658
  function createExperiment(creds, body5) {
9007
7659
  return request(creds, "POST", "/api/v1/experiments", body5);
9008
7660
  }
9009
- function startExperiment(creds, id) {
9010
- return request(creds, "POST", `/api/v1/experiments/${id}/start`);
9011
- }
9012
7661
  function pauseExperiment(creds, id) {
9013
7662
  return request(creds, "POST", `/api/v1/experiments/${id}/pause`);
9014
7663
  }
@@ -9043,9 +7692,7 @@ function createExperimentScreenshots(creds, id, body5) {
9043
7692
  return request(creds, "POST", `/api/v1/experiments/${id}/screenshots`, body5);
9044
7693
  }
9045
7694
  async function getScreenshotJob(creds, jobId, timeoutMs) {
9046
- if (process.env.APEX_CLI_MOCK_API === "1" && ["deadline-processing", "deadline-completed"].includes(process.env.APEX_CLI_MOCK_SCREENSHOTS ?? "")) {
9047
- await new Promise((resolve) => setTimeout(resolve, 5));
9048
- }
7695
+ await testScreenshotDelay?.(timeoutMs);
9049
7696
  return request(creds, "GET", `/api/v1/screenshots/${jobId}`, void 0, timeoutMs);
9050
7697
  }
9051
7698
  function planExperimentFilesystem(creds, id, body5) {
@@ -9259,8 +7906,8 @@ function getRoadmapItemHandoff(creds, id) {
9259
7906
  `/api/v1/roadmap/items/${encodeURIComponent(id)}/handoff`
9260
7907
  );
9261
7908
  }
9262
- function createProxyPreviewSession(creds, experimentId, variationId, targetUrl) {
9263
- return request(creds, "POST", "/api/v1/preview-sessions", { experiment_id: experimentId, ...variationId ? { variation_id: variationId } : {}, ...targetUrl ? { target_url: targetUrl } : {} });
7909
+ function createProxyPreviewSession(creds, experimentId, variationId, targetUrl, ttlHours) {
7910
+ return request(creds, "POST", "/api/v1/preview-sessions", { experiment_id: experimentId, ...variationId ? { variation_id: variationId } : {}, ...targetUrl ? { target_url: targetUrl } : {}, ...ttlHours !== void 0 ? { ttl_hours: ttlHours } : {} });
9264
7911
  }
9265
7912
  function listAuditLogs(creds, opts) {
9266
7913
  const p = new URLSearchParams();
@@ -9336,11 +7983,11 @@ function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor
9336
7983
  if (limit) params.set("limit", String(limit));
9337
7984
  if (cursor) params.set("cursor", cursor);
9338
7985
  const query = params.toString();
9339
- const path20 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
9340
- return request(creds, method, path20, body5);
7986
+ const path21 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
7987
+ return request(creds, method, path21, body5);
9341
7988
  }
9342
- function managedPath(kind5, id, action70) {
9343
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action70 ? `/${action70}` : ""}`;
7989
+ function managedPath(kind5, id, action69) {
7990
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action69 ? `/${action69}` : ""}`;
9344
7991
  }
9345
7992
  function listManagedDeliveries(creds, kind5, limit) {
9346
7993
  const q = limit ? `?limit=${limit}` : "";
@@ -9362,8 +8009,8 @@ function createManagedDelivery(creds, kind5, body5) {
9362
8009
  function updateManagedDelivery(creds, kind5, id, body5) {
9363
8010
  return request(creds, "PATCH", managedPath(kind5, id), body5);
9364
8011
  }
9365
- function transitionManagedDelivery(creds, kind5, id, action70) {
9366
- return request(creds, "POST", managedPath(kind5, id, action70));
8012
+ function transitionManagedDelivery(creds, kind5, id, action69) {
8013
+ return request(creds, "POST", managedPath(kind5, id, action69));
9367
8014
  }
9368
8015
  function listConfigResources(creds, family, opts) {
9369
8016
  const params = new URLSearchParams();
@@ -9620,6 +8267,11 @@ async function pickContext(tree) {
9620
8267
  init_source();
9621
8268
  var STATUS_COLORS = {
9622
8269
  draft: source_default.gray,
8270
+ awaiting_client_approval: source_default.magenta,
8271
+ changes_requested: source_default.red,
8272
+ client_approved: source_default.magentaBright,
8273
+ qa: source_default.cyan,
8274
+ ready_to_launch: source_default.greenBright,
9623
8275
  running: source_default.green,
9624
8276
  paused: source_default.yellow,
9625
8277
  completed: source_default.blue
@@ -9710,8 +8362,8 @@ function parseFormBody(body5) {
9710
8362
  return new URLSearchParams(body5);
9711
8363
  }
9712
8364
  async function waitForBrowserLogin(baseUrl, userMode = false) {
9713
- const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
9714
- const clientName = import_node_os4.default.hostname() || "Local machine";
8365
+ const state = import_node_crypto.default.randomBytes(24).toString("base64url");
8366
+ const clientName = import_node_os5.default.hostname() || "Local machine";
9715
8367
  return new Promise((resolve, reject) => {
9716
8368
  let settled = false;
9717
8369
  let timeout = null;
@@ -9962,9 +8614,41 @@ var useCommand = new Command("use").description("Set the current Apex organizati
9962
8614
 
9963
8615
  // src/commands/experiments.ts
9964
8616
  init_source();
9965
- var import_node_crypto3 = __toESM(require("node:crypto"));
9966
- var import_node_fs5 = __toESM(require("node:fs"));
9967
- var import_node_path3 = __toESM(require("node:path"));
8617
+ var import_node_crypto2 = __toESM(require("node:crypto"));
8618
+ var import_node_fs6 = __toESM(require("node:fs"));
8619
+ var import_node_path4 = __toESM(require("node:path"));
8620
+
8621
+ // ../api-client/src/qa-preview-url.ts
8622
+ function applyQaPreviewParams(url, params) {
8623
+ url.searchParams.set("drip_force", `${params.experimentId}:${params.variationId}`);
8624
+ url.searchParams.set("drip_experiment_id", params.experimentId);
8625
+ url.searchParams.set("drip_variation_id", params.variationId);
8626
+ url.searchParams.set("drip_qa", "1");
8627
+ if (params.debug !== false) url.searchParams.set("drip_debug", "1");
8628
+ if (params.devtools) url.searchParams.set("drip_devtools", "1");
8629
+ if (params.qaSessionId) url.searchParams.set("drip_qa_session", params.qaSessionId);
8630
+ if (params.harnessToken) url.searchParams.set("drip_harness", params.harnessToken);
8631
+ return url;
8632
+ }
8633
+ function buildQaPreviewUrl(experiment, options = {}) {
8634
+ const variation = options.variationName ? experiment.variations?.find((v) => v.name === options.variationName) : experiment.variations?.find((v) => !v.is_control) ?? experiment.variations?.[0];
8635
+ const target = options.storeUrl ?? experiment.store_url;
8636
+ if (!target || !variation) return null;
8637
+ try {
8638
+ return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, ...options });
8639
+ } catch {
8640
+ return null;
8641
+ }
8642
+ }
8643
+
8644
+ // src/lib/qa-preview-url.ts
8645
+ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
8646
+ const url = buildQaPreviewUrl({ id: experimentId, store_url: storeUrl, variations: [{ id: variationId, is_control: false }] });
8647
+ if (!url) throw new TypeError("Invalid QA preview URL");
8648
+ return url;
8649
+ }
8650
+
8651
+ // src/commands/experiments.ts
9968
8652
  function formatDate(iso) {
9969
8653
  return new Date(iso).toLocaleDateString("en-US", {
9970
8654
  month: "short",
@@ -10070,25 +8754,25 @@ var FILESYSTEM_FILE_RE = new RegExp(
10070
8754
  `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
10071
8755
  );
10072
8756
  function sha256Text(value) {
10073
- return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
8757
+ return import_node_crypto2.default.createHash("sha256").update(value).digest("hex");
10074
8758
  }
10075
8759
  function folderPath(folder) {
10076
- return import_node_path3.default.resolve(process.cwd(), folder);
8760
+ return import_node_path4.default.resolve(process.cwd(), folder);
10077
8761
  }
10078
8762
  function filesystemApexDir(folder) {
10079
- return import_node_path3.default.join(folder, ".apex");
8763
+ return import_node_path4.default.join(folder, ".apex");
10080
8764
  }
10081
8765
  function filesystemSessionPath(folder) {
10082
- return import_node_path3.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
8766
+ return import_node_path4.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
10083
8767
  }
10084
8768
  function writeFilesystemSession(folder, session) {
10085
- import_node_fs5.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
10086
- import_node_fs5.default.writeFileSync(
10087
- import_node_path3.default.join(filesystemApexDir(folder), ".gitignore"),
8769
+ import_node_fs6.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
8770
+ import_node_fs6.default.writeFileSync(
8771
+ import_node_path4.default.join(filesystemApexDir(folder), ".gitignore"),
10088
8772
  "experiment-filesystem.json\n",
10089
8773
  "utf8"
10090
8774
  );
10091
- import_node_fs5.default.writeFileSync(
8775
+ import_node_fs6.default.writeFileSync(
10092
8776
  filesystemSessionPath(folder),
10093
8777
  `${JSON.stringify(session, null, 2)}
10094
8778
  `,
@@ -10097,14 +8781,14 @@ function writeFilesystemSession(folder, session) {
10097
8781
  }
10098
8782
  function readFilesystemSession(folder) {
10099
8783
  const filePath = filesystemSessionPath(folder);
10100
- if (!import_node_fs5.default.existsSync(filePath)) {
10101
- const workspaceMarker = import_node_path3.default.join(filesystemApexDir(folder), "session.json");
10102
- if (import_node_fs5.default.existsSync(workspaceMarker)) {
8784
+ if (!import_node_fs6.default.existsSync(filePath)) {
8785
+ const workspaceMarker = import_node_path4.default.join(filesystemApexDir(folder), "session.json");
8786
+ if (import_node_fs6.default.existsSync(workspaceMarker)) {
10103
8787
  throw new Error("This folder is an operator workspace (apex workspace \u2026), not an experiment filesystem \u2014 run `apex workspace status` here, or `apex experiments pull` into a different folder.");
10104
8788
  }
10105
8789
  throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
10106
8790
  }
10107
- return JSON.parse(import_node_fs5.default.readFileSync(filePath, "utf8"));
8791
+ return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
10108
8792
  }
10109
8793
  function validateFilesystemPath(relativePath) {
10110
8794
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -10120,38 +8804,38 @@ function writeExperimentFilesystemFiles(folder, files) {
10120
8804
  }));
10121
8805
  for (const file of validatedFiles) {
10122
8806
  const relativePath = file.relativePath;
10123
- const absPath = import_node_path3.default.join(folder, relativePath);
10124
- import_node_fs5.default.mkdirSync(import_node_path3.default.dirname(absPath), { recursive: true });
10125
- import_node_fs5.default.writeFileSync(absPath, file.content, "utf8");
8807
+ const absPath = import_node_path4.default.join(folder, relativePath);
8808
+ import_node_fs6.default.mkdirSync(import_node_path4.default.dirname(absPath), { recursive: true });
8809
+ import_node_fs6.default.writeFileSync(absPath, file.content, "utf8");
10126
8810
  }
10127
8811
  }
10128
8812
  function listExperimentFilesystemFiles(folder) {
10129
8813
  const files = [];
10130
8814
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel) {
10131
- const absPath = import_node_path3.default.join(folder, fileName);
10132
- if (!import_node_fs5.default.existsSync(absPath)) continue;
10133
- const content = import_node_fs5.default.readFileSync(absPath, "utf8");
8815
+ const absPath = import_node_path4.default.join(folder, fileName);
8816
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
8817
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10134
8818
  files.push({ path: fileName, content, sha256: sha256Text(content) });
10135
8819
  }
10136
8820
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
10137
8821
  const relativePath = `experiment/${fileName}`;
10138
- const absPath = import_node_path3.default.join(folder, relativePath);
10139
- if (!import_node_fs5.default.existsSync(absPath)) continue;
10140
- const content = import_node_fs5.default.readFileSync(absPath, "utf8");
8822
+ const absPath = import_node_path4.default.join(folder, relativePath);
8823
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
8824
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10141
8825
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
10142
8826
  }
10143
- const variationsRoot = import_node_path3.default.join(folder, "variations");
10144
- if (import_node_fs5.default.existsSync(variationsRoot)) {
10145
- for (const variationDir of import_node_fs5.default.readdirSync(variationsRoot, { withFileTypes: true })) {
8827
+ const variationsRoot = import_node_path4.default.join(folder, "variations");
8828
+ if (import_node_fs6.default.existsSync(variationsRoot)) {
8829
+ for (const variationDir of import_node_fs6.default.readdirSync(variationsRoot, { withFileTypes: true })) {
10146
8830
  if (!variationDir.isDirectory()) continue;
10147
- const base = import_node_path3.default.join(variationsRoot, variationDir.name);
8831
+ const base = import_node_path4.default.join(variationsRoot, variationDir.name);
10148
8832
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation) {
10149
- const absPath = import_node_path3.default.join(base, fileName);
10150
- if (!import_node_fs5.default.existsSync(absPath)) continue;
8833
+ const absPath = import_node_path4.default.join(base, fileName);
8834
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
10151
8835
  const relativePath = validateFilesystemPath(
10152
- import_node_path3.default.relative(folder, absPath)
8836
+ import_node_path4.default.relative(folder, absPath)
10153
8837
  );
10154
- const content = import_node_fs5.default.readFileSync(absPath, "utf8");
8838
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10155
8839
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
10156
8840
  }
10157
8841
  }
@@ -10193,6 +8877,39 @@ function blockingIssueSummary(issues) {
10193
8877
  const remaining = issues.length - 1;
10194
8878
  return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
10195
8879
  }
8880
+ async function getQaPreviewUrls(creds, experimentId) {
8881
+ try {
8882
+ const res = await getExperiment(creds, experimentId);
8883
+ const storeUrl = res.data?.store_url;
8884
+ if (!res.ok || !res.data || !storeUrl) {
8885
+ return { previewUrls: [], unavailable: true };
8886
+ }
8887
+ const experiment = res.data;
8888
+ const previewUrls = (experiment.variations ?? []).filter((variation) => !variation.is_control).map((variation) => ({
8889
+ variationId: variation.id,
8890
+ variationName: variation.name,
8891
+ url: buildQaPreviewUrl2(
8892
+ storeUrl,
8893
+ experiment.id,
8894
+ variation.id
8895
+ ).toString()
8896
+ }));
8897
+ return { previewUrls, unavailable: false };
8898
+ } catch {
8899
+ return { previewUrls: [], unavailable: true };
8900
+ }
8901
+ }
8902
+ function printQaPreviewUrls(experimentId, result) {
8903
+ if (result.unavailable) {
8904
+ console.log(source_default.dim(`Preview: apex experiments get ${experimentId} / drip preview ${experimentId}`));
8905
+ return;
8906
+ }
8907
+ for (const preview of result.previewUrls) {
8908
+ console.log(
8909
+ `Preview (interactive, works while draft \u2014 no on-site editing needed): ${preview.url}`
8910
+ );
8911
+ }
8912
+ }
10196
8913
  function printQaRun(run3) {
10197
8914
  console.log();
10198
8915
  console.log(source_default.bold(`QA run ${run3.id}`));
@@ -10236,7 +8953,7 @@ async function confirmApply(message) {
10236
8953
  function parseEvidenceInput(value) {
10237
8954
  if (!value) return void 0;
10238
8955
  const source = value.startsWith("@") ? value.slice(1) : value;
10239
- const text = value.startsWith("@") || import_node_fs5.default.existsSync(source) ? import_node_fs5.default.readFileSync(import_node_path3.default.resolve(process.cwd(), source), "utf8") : value;
8956
+ const text = value.startsWith("@") || import_node_fs6.default.existsSync(source) ? import_node_fs6.default.readFileSync(import_node_path4.default.resolve(process.cwd(), source), "utf8") : value;
10240
8957
  const parsed = JSON.parse(text);
10241
8958
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10242
8959
  throw new Error("QA evidence must be a JSON object");
@@ -10260,7 +8977,7 @@ async function resolveExperimentForFilesystemOrId(value) {
10260
8977
  return {
10261
8978
  experimentId: session.experimentId,
10262
8979
  folder: candidate,
10263
- label: import_node_path3.default.basename(candidate)
8980
+ label: import_node_path4.default.basename(candidate)
10264
8981
  };
10265
8982
  }
10266
8983
  const exp = await resolveExperiment(value);
@@ -10306,7 +9023,7 @@ async function resolveExperiment(idOrSlug) {
10306
9023
  res.errorDetails
10307
9024
  );
10308
9025
  }
10309
- var list = new Command("list").alias("ls").description("List experiments (archived experiments are hidden by default)").option("-s, --status <status>", "Filter by status (draft|running|paused|completed)").option("-n, --limit <n>", "Max results", "20").option("--include-archived", "Include archived experiments").action(async function action3(opts) {
9026
+ var list = new Command("list").alias("ls").description("List experiments (archived experiments are hidden by default)").option("-s, --status <status>", "Filter by status (draft|awaiting_client_approval|changes_requested|client_approved|qa|ready_to_launch|running|paused|completed)").option("-n, --limit <n>", "Max results", "20").option("--include-archived", "Include archived experiments").action(async function action3(opts) {
10310
9027
  const creds = requireCredentials();
10311
9028
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10312
9029
  const spinner = process.stderr.isTTY ? ora2("Fetching experiments...").start() : null;
@@ -10506,25 +9223,7 @@ var create = new Command("create").description("Create a new experiment").requir
10506
9223
  spinner?.succeed("Experiment created");
10507
9224
  output(this, { experiment: res.data }, () => printExperiment(res.data));
10508
9225
  });
10509
- var start = new Command("start").description("Start an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
10510
- const creds = requireCredentials();
10511
- const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10512
- const spinner = ora2("Starting experiment...").start();
10513
- const exp = await resolveExperiment(idOrSlug);
10514
- if (!exp) {
10515
- spinner.fail(`Experiment "${idOrSlug}" not found`);
10516
- fail(this, `Experiment "${idOrSlug}" not found`);
10517
- }
10518
- const res = await startExperiment(creds, exp.id);
10519
- if (!res.ok) {
10520
- spinner.fail("Failed to start experiment");
10521
- fail(this, res.error ?? "Failed to start experiment", classifyStatus(res.status), res.errorDetails);
10522
- }
10523
- output(this, { experiment: { id: exp.id, name: exp.name, status: "running" } }, () => {
10524
- spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("running")}`);
10525
- });
10526
- });
10527
- var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
9226
+ var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
10528
9227
  const creds = requireCredentials();
10529
9228
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10530
9229
  const spinner = ora2("Pausing experiment...").start();
@@ -10542,7 +9241,7 @@ var pause = new Command("pause").description("Pause a running experiment").argum
10542
9241
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
10543
9242
  });
10544
9243
  });
10545
- var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action9(idOrSlug) {
9244
+ var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
10546
9245
  const creds = requireCredentials();
10547
9246
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10548
9247
  const spinner = ora2("Completing experiment...").start();
@@ -10560,7 +9259,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
10560
9259
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
10561
9260
  });
10562
9261
  });
10563
- var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", "Experiment ID or slug").option("-f, --force", "Skip confirmation").action(async function action10(idOrSlug, opts) {
9262
+ var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", "Experiment ID or slug").option("-f, --force", "Skip confirmation").action(async function action9(idOrSlug, opts) {
10564
9263
  const creds = requireCredentials();
10565
9264
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10566
9265
  const exp = await resolveExperiment(idOrSlug);
@@ -10602,7 +9301,7 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
10602
9301
  }
10603
9302
  output(this, { experiment: { id: exp.id, name: exp.name }, deleted: true }, () => spinner.succeed(`Experiment "${exp.name}" deleted.`));
10604
9303
  });
10605
- var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action11(idOrSlug, opts) {
9304
+ var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action10(idOrSlug, opts) {
10606
9305
  const creds = requireCredentials();
10607
9306
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10608
9307
  const spinner = ora2("Pulling experiment filesystem...").start();
@@ -10622,7 +9321,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
10622
9321
  res.data.files.map((file) => [file.path, { sha256: file.sha256 }])
10623
9322
  )
10624
9323
  });
10625
- output(this, { folder, experimentId: exp.id, files: res.data.files.map((file) => import_node_path3.default.join(folder, file.path)) }, () => {
9324
+ output(this, { folder, experimentId: exp.id, files: res.data.files.map((file) => import_node_path4.default.join(folder, file.path)) }, () => {
10626
9325
  spinner.succeed(`Pulled ${exp.name}`);
10627
9326
  console.log(source_default.cyan(folder));
10628
9327
  });
@@ -10631,7 +9330,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
10631
9330
  throw error;
10632
9331
  }
10633
9332
  });
10634
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action12(folderArg) {
9333
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action11(folderArg) {
10635
9334
  const creds = requireCredentials();
10636
9335
  try {
10637
9336
  const folder = folderPath(folderArg);
@@ -10654,7 +9353,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
10654
9353
  fail(this, error instanceof Error ? error.message : String(error));
10655
9354
  }
10656
9355
  });
10657
- var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").action(async function action13(folderArg, opts) {
9356
+ var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").action(async function action12(folderArg, opts) {
10658
9357
  const creds = requireCredentials();
10659
9358
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10660
9359
  try {
@@ -10711,15 +9410,18 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10711
9410
  folder,
10712
9411
  session.experimentId
10713
9412
  );
10714
- output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data }, () => {
9413
+ const previewResult = await getQaPreviewUrls(creds, session.experimentId);
9414
+ const { previewUrls } = previewResult;
9415
+ output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data, previewUrls }, () => {
10715
9416
  printFilesystemPlan(planRes.data);
9417
+ printQaPreviewUrls(session.experimentId, previewResult);
10716
9418
  });
10717
9419
  } catch (error) {
10718
9420
  if (error instanceof CliError) throw error;
10719
9421
  fail(this, error instanceof Error ? error.message : String(error));
10720
9422
  }
10721
9423
  });
10722
- var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--json", "Print the QA run in the standard JSON envelope").action(async function action14(target) {
9424
+ var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--json", "Print the QA run in the standard JSON envelope").action(async function action13(target) {
10723
9425
  const creds = requireCredentials();
10724
9426
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10725
9427
  const spinner = ora2("Fetching QA run...").start();
@@ -10740,7 +9442,7 @@ var qaStatus = new Command("status").description("Show or create the latest expe
10740
9442
  }
10741
9443
  });
10742
9444
  var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").requiredOption("--item <id>", "Checklist item id").requiredOption("--status <status>", "pending|pass|warning|fail|accepted|skipped").option("--notes <text>", "Checklist notes").option("--evidence <json-or-path>", "Evidence JSON object or @path/to/evidence.json").option("--json", "Print raw QA run JSON").action(
10743
- async function action15(target, opts) {
9445
+ async function action14(target, opts) {
10744
9446
  const creds = requireCredentials();
10745
9447
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10746
9448
  const spinner = ora2("Updating QA checklist...").start();
@@ -10779,7 +9481,7 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
10779
9481
  }
10780
9482
  );
10781
9483
  var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--notes <text>", "QA run notes").option("--acceptance <text>", "Acceptance criteria summary").option("--json", "Print raw QA run JSON").action(
10782
- async function action16(target, opts) {
9484
+ async function action15(target, opts) {
10783
9485
  const creds = requireCredentials();
10784
9486
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10785
9487
  const spinner = ora2("Updating QA notes...").start();
@@ -10814,7 +9516,7 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10814
9516
  }
10815
9517
  }
10816
9518
  );
10817
- var qaApprove = new Command("approve").description("Mark QA done after the checklist gates are ready").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action17(target) {
9519
+ var qaApprove = new Command("approve").description("Mark QA done after the checklist gates are ready").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action16(target) {
10818
9520
  const creds = requireCredentials();
10819
9521
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10820
9522
  const spinner = ora2("Approving QA...").start();
@@ -10833,7 +9535,7 @@ var qaApprove = new Command("approve").description("Mark QA done after the check
10833
9535
  throw error;
10834
9536
  }
10835
9537
  });
10836
- var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action18(target) {
9538
+ var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action17(target) {
10837
9539
  const creds = requireCredentials();
10838
9540
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10839
9541
  const spinner = ora2("Reopening QA...").start();
@@ -10856,7 +9558,7 @@ var qa = new Command("qa").description("Manage experiment QA runs from the CLI")
10856
9558
  var SCREENSHOT_DEVICES = ["desktop_chrome", "iphone_15_pro", "galaxy_s24", "ipad_pro", "desktop_safari"];
10857
9559
  var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
10858
9560
  var commaList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
10859
- var screenshots = new Command("screenshots").description("Force-assign every capture, including control, to its variation server-side for deterministic screenshots").argument("<experimentRef>", "Experiment ID or slug").option("--url <target>", "Target URL override (not supported by the current v1 API)").option("--devices <list>", `Comma-separated devices (default: ${SCREENSHOT_DEVICES.join(",")})`).option("--variations <list>", "Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned").option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--timeout <seconds>", "Maximum wait for completion; finished results are always downloaded", "300").action(async function action19(experimentRef, opts) {
9561
+ var screenshots = new Command("screenshots").description("Force-assign every capture, including control, to its variation server-side for deterministic screenshots").argument("<experimentRef>", "Experiment ID or slug").option("--url <target>", "Target URL override (not supported by the current v1 API)").option("--devices <list>", `Comma-separated devices (default: ${SCREENSHOT_DEVICES.join(",")})`).option("--variations <list>", "Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned").option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--timeout <seconds>", "Maximum wait for completion; finished results are always downloaded", "300").action(async function action18(experimentRef, opts) {
10860
9562
  if (opts.url) fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
10861
9563
  const timeoutSeconds = Number(opts.timeout);
10862
9564
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
@@ -10889,25 +9591,25 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10889
9591
  const created = await createExperimentScreenshots(creds, experiment.id, { devices, variation_ids: variationIds });
10890
9592
  if (!created.ok || !created.data) fail(this, created.error ?? "Failed to create screenshot job", classifyStatus(created.status), created.errorDetails);
10891
9593
  let job = created.data;
10892
- const deadline = Date.now() + timeoutSeconds * 1e3;
10893
- let delayMs = process.env.APEX_CLI_MOCK_API === "1" ? 1 : 1e3;
9594
+ const deadline = screenshotNow() + timeoutSeconds * 1e3;
9595
+ let delayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
10894
9596
  let consecutiveTransientFailures = 0;
10895
9597
  while (job.status === "queued" || job.status === "processing") {
10896
- if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10897
- await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - Date.now()))));
10898
- if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9598
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9599
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - screenshotNow()))));
9600
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10899
9601
  let polled;
10900
9602
  try {
10901
- polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - Date.now()));
9603
+ polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - screenshotNow()));
10902
9604
  } catch (error) {
10903
- if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9605
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10904
9606
  consecutiveTransientFailures += 1;
10905
9607
  if (consecutiveTransientFailures > 3) throw error;
10906
9608
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10907
9609
  continue;
10908
9610
  }
10909
9611
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
10910
- if (!polledToTerminalState && Date.now() >= deadline) {
9612
+ if (!polledToTerminalState && screenshotNow() >= deadline) {
10911
9613
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10912
9614
  }
10913
9615
  if (!polled.ok || !polled.data) {
@@ -10923,8 +9625,8 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10923
9625
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10924
9626
  }
10925
9627
  if (job.status === "failed") fail(this, `Screenshot job failed: ${job.error ?? "unknown error"}`);
10926
- const outputDir = import_node_path3.default.resolve(process.cwd(), opts.out);
10927
- import_node_fs5.default.mkdirSync(outputDir, { recursive: true });
9628
+ const outputDir = import_node_path4.default.resolve(process.cwd(), opts.out);
9629
+ import_node_fs6.default.mkdirSync(outputDir, { recursive: true });
10928
9630
  const rows = [];
10929
9631
  const errors = [];
10930
9632
  const usedFilenames = /* @__PURE__ */ new Set();
@@ -10961,7 +9663,7 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10961
9663
  try {
10962
9664
  const response = await fetch(url);
10963
9665
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
10964
- await import_node_fs5.default.promises.writeFile(import_node_path3.default.join(outputDir, filename), Buffer.from(await response.arrayBuffer()));
9666
+ await import_node_fs6.default.promises.writeFile(import_node_path4.default.join(outputDir, filename), Buffer.from(await response.arrayBuffer()));
10965
9667
  workerResults.push({ index, row: [variation.name, device, filename] });
10966
9668
  } catch (error) {
10967
9669
  workerResults.push({ index, error: `${variation.name}/${device}: ${error instanceof Error ? error.message : String(error)}` });
@@ -10982,7 +9684,7 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10982
9684
  printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
10983
9685
  });
10984
9686
  });
10985
- var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(create).addCommand(start).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText("after", `
9687
+ var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(create).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText("after", `
10986
9688
  Examples:
10987
9689
  apex experiments list --status running
10988
9690
  apex experiments get checkout-banner
@@ -10992,9 +9694,9 @@ Examples:
10992
9694
 
10993
9695
  // src/commands/push.ts
10994
9696
  init_source();
10995
- var import_node_fs6 = __toESM(require("node:fs"));
10996
- var import_node_os5 = __toESM(require("node:os"));
10997
- var import_node_path4 = __toESM(require("node:path"));
9697
+ var import_node_fs7 = __toESM(require("node:fs"));
9698
+ var import_node_os6 = __toESM(require("node:os"));
9699
+ var import_node_path5 = __toESM(require("node:path"));
10998
9700
  var import_node_url2 = require("node:url");
10999
9701
  function toApiBody(exp) {
11000
9702
  return {
@@ -11018,7 +9720,7 @@ function toApiBody(exp) {
11018
9720
  };
11019
9721
  }
11020
9722
  async function importConfigModule(absPath) {
11021
- const ext = import_node_path4.default.extname(absPath).toLowerCase();
9723
+ const ext = import_node_path5.default.extname(absPath).toLowerCase();
11022
9724
  if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
11023
9725
  return await import((0, import_node_url2.pathToFileURL)(absPath).href);
11024
9726
  }
@@ -11032,8 +9734,8 @@ async function importConfigModule(absPath) {
11032
9734
  ` + (err instanceof Error ? err.message : String(err))
11033
9735
  );
11034
9736
  }
11035
- const tmpDir = import_node_fs6.default.mkdtempSync(import_node_path4.default.join(import_node_os5.default.tmpdir(), "drip-config-"));
11036
- const outFile = import_node_path4.default.join(tmpDir, "config.mjs");
9737
+ const tmpDir = import_node_fs7.default.mkdtempSync(import_node_path5.default.join(import_node_os6.default.tmpdir(), "drip-config-"));
9738
+ const outFile = import_node_path5.default.join(tmpDir, "config.mjs");
11037
9739
  try {
11038
9740
  await esbuild.build({
11039
9741
  entryPoints: [absPath],
@@ -11049,7 +9751,7 @@ async function importConfigModule(absPath) {
11049
9751
  return await import(href);
11050
9752
  } finally {
11051
9753
  try {
11052
- import_node_fs6.default.rmSync(tmpDir, { recursive: true, force: true });
9754
+ import_node_fs7.default.rmSync(tmpDir, { recursive: true, force: true });
11053
9755
  } catch {
11054
9756
  }
11055
9757
  }
@@ -11063,7 +9765,7 @@ function isRecord(value) {
11063
9765
  }
11064
9766
  var pushCommand = new Command("push").description(
11065
9767
  "Push experiment definitions from config file to the API (idempotent by slug)"
11066
- ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action20(opts) {
9768
+ ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action19(opts) {
11067
9769
  const creds = requireCredentials();
11068
9770
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
11069
9771
  const configPath = opts.config ?? findConfigPath();
@@ -11077,7 +9779,7 @@ var pushCommand = new Command("push").description(
11077
9779
  const spinner = process.stderr.isTTY ? ora2(`Loading config from ${source_default.cyan(configPath)}...`).start() : null;
11078
9780
  let experiments;
11079
9781
  try {
11080
- const absPath = import_node_path4.default.resolve(configPath);
9782
+ const absPath = import_node_path5.default.resolve(configPath);
11081
9783
  const mod = await importConfigModule(absPath);
11082
9784
  const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
11083
9785
  if (Array.isArray(exported)) {
@@ -11181,17 +9883,22 @@ var pushCommand = new Command("push").description(
11181
9883
 
11182
9884
  // src/commands/preview.ts
11183
9885
  init_source();
11184
- var previewCommand = new Command("preview").description("Open QA preview URL for an experiment variation").argument("<id-or-slug>", "Experiment ID or slug").argument("[variation-id]", "Variation ID (defaults to first non-control)").option("--store-url <url>", "Override experiment store_url (target website URL)").option("--proxy", "Create an artifact-injecting proxy preview").option("--no-open", "Print URL instead of opening browser").action(async (idOrSlug, variationId, opts) => {
9886
+ var previewCommand = new Command("preview").description("Open QA preview URL for an experiment variation").argument("<id-or-slug>", "Experiment ID or slug").argument("[variation-id]", "Variation ID (defaults to first non-control)").option("--store-url <url>", "Override experiment store_url (target website URL)").option("--proxy", "Create an artifact-injecting proxy preview").option("--ttl <hours>", "Proxy preview lifetime in hours (1-168)").option("--no-open", "Print URL instead of opening browser").action(async (idOrSlug, variationId, opts) => {
11185
9887
  const creds = requireCredentials();
11186
9888
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
11187
9889
  if (opts?.proxy) {
9890
+ const ttlHours = opts.ttl === void 0 ? void 0 : Number(opts.ttl);
9891
+ if (ttlHours !== void 0 && (!Number.isInteger(ttlHours) || ttlHours < 1 || ttlHours > 168)) {
9892
+ fail(previewCommand, "--ttl must be an integer between 1 and 168 hours");
9893
+ }
11188
9894
  const identifier = idOrSlug.startsWith("slug:") ? idOrSlug.slice(5) : idOrSlug;
11189
9895
  const proxySpinner = ora2("Creating proxy preview session...").start();
11190
9896
  const created = await createProxyPreviewSession(
11191
9897
  creds,
11192
9898
  identifier,
11193
9899
  variationId,
11194
- opts.storeUrl
9900
+ opts.storeUrl,
9901
+ ttlHours
11195
9902
  );
11196
9903
  proxySpinner.stop();
11197
9904
  if (!created.ok || !created.data) {
@@ -11202,7 +9909,15 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11202
9909
  );
11203
9910
  }
11204
9911
  const data = created.data;
11205
- output(previewCommand, data, () => console.log(source_default.cyan(data.preview_url)));
9912
+ output(previewCommand, data, () => {
9913
+ console.log(source_default.cyan(data.preview_url));
9914
+ const expiresAt = new Date(data.expires_at);
9915
+ const remainingMs = expiresAt.getTime() - Date.now();
9916
+ const value = remainingMs < 864e5 ? Math.max(1, Math.round(remainingMs / 36e5)) : Math.round(remainingMs / 864e5);
9917
+ const unit = remainingMs < 864e5 ? "hour" : "day";
9918
+ const duration = `${value} ${unit}${value === 1 ? "" : "s"}`;
9919
+ console.log(`Valid until ${expiresAt.toISOString()} (${duration}). Re-run this command for a fresh link.`);
9920
+ });
11206
9921
  if (opts.open !== false && !shouldUseJson(previewCommand)) {
11207
9922
  try {
11208
9923
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
@@ -11250,21 +9965,16 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11250
9965
  variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
11251
9966
  }
11252
9967
  spinner.stop();
11253
- const storeUrl = exp.store_url ?? opts?.storeUrl;
9968
+ const storeUrl = opts?.storeUrl ?? exp.store_url;
11254
9969
  if (!storeUrl) {
11255
9970
  fail(previewCommand, 'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.');
11256
9971
  }
11257
9972
  let previewUrl;
11258
9973
  try {
11259
- previewUrl = new URL(storeUrl);
9974
+ previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id);
11260
9975
  } catch {
11261
9976
  fail(previewCommand, 'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".');
11262
9977
  }
11263
- previewUrl.searchParams.set("drip_force", `${exp.id}:${variation.id}`);
11264
- previewUrl.searchParams.set("drip_experiment_id", exp.id);
11265
- previewUrl.searchParams.set("drip_variation_id", variation.id);
11266
- previewUrl.searchParams.set("drip_qa", "1");
11267
- previewUrl.searchParams.set("drip_debug", "1");
11268
9978
  output(previewCommand, { experimentId: exp.id, variationId: variation.id, previewUrl: previewUrl.toString() }, () => {
11269
9979
  console.log();
11270
9980
  console.log(source_default.bold(`Preview: ${exp.name}`));
@@ -11286,25 +9996,25 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11286
9996
 
11287
9997
  // src/commands/harness.ts
11288
9998
  init_source();
11289
- var import_node_fs7 = __toESM(require("node:fs"));
11290
- var import_node_path5 = __toESM(require("node:path"));
9999
+ var import_node_fs8 = __toESM(require("node:fs"));
10000
+ var import_node_path6 = __toESM(require("node:path"));
11291
10001
  var import_node_child_process6 = require("node:child_process");
11292
10002
  function slugify(value, fallback2 = "variation") {
11293
10003
  const slug = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
11294
10004
  return slug || fallback2;
11295
10005
  }
11296
10006
  function readJson(filePath) {
11297
- if (!import_node_fs7.default.existsSync(filePath)) return null;
11298
- return JSON.parse(import_node_fs7.default.readFileSync(filePath, "utf8"));
10007
+ if (!import_node_fs8.default.existsSync(filePath)) return null;
10008
+ return JSON.parse(import_node_fs8.default.readFileSync(filePath, "utf8"));
11299
10009
  }
11300
10010
  function resolveFolder(folder) {
11301
- return import_node_path5.default.resolve(process.cwd(), folder);
10011
+ return import_node_path6.default.resolve(process.cwd(), folder);
11302
10012
  }
11303
10013
  function experimentConfigPath(folder) {
11304
- return import_node_path5.default.join(folder, "experiment.config.json");
10014
+ return import_node_path6.default.join(folder, "experiment.config.json");
11305
10015
  }
11306
10016
  function apexConfigPath(folder) {
11307
- return import_node_path5.default.join(folder, "apex.harness.json");
10017
+ return import_node_path6.default.join(folder, "apex.harness.json");
11308
10018
  }
11309
10019
  function loadHarnessConfig(folder) {
11310
10020
  const experiment = readJson(experimentConfigPath(folder)) ?? {};
@@ -11313,8 +10023,8 @@ function loadHarnessConfig(folder) {
11313
10023
  const targetOrigin = experiment.targetOrigin?.replace(/\/+$/, "") ?? "";
11314
10024
  const defaultPath = experiment.defaultPath ?? "/";
11315
10025
  const targetUrl = targetOrigin ? new URL(defaultPath, targetOrigin).toString() : "";
11316
- const id = experiment.id ?? import_node_path5.default.basename(import_node_path5.default.dirname(folder));
11317
- const variation = import_node_path5.default.basename(folder);
10026
+ const id = experiment.id ?? import_node_path6.default.basename(import_node_path6.default.dirname(folder));
10027
+ const variation = import_node_path6.default.basename(folder);
11318
10028
  return {
11319
10029
  experiment,
11320
10030
  apex: {
@@ -11346,35 +10056,35 @@ function runAbHarness(args, cwd) {
11346
10056
  function artifactPaths(folder, experiment) {
11347
10057
  const variant = experiment.variant;
11348
10058
  if (variant?.endsWith(".bundle.js")) {
11349
- const jsPath = import_node_path5.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
11350
- const cssPath = import_node_path5.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
11351
- return { jsPath, cssPath: import_node_fs7.default.existsSync(cssPath) ? cssPath : null };
10059
+ const jsPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
10060
+ const cssPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
10061
+ return { jsPath, cssPath: import_node_fs8.default.existsSync(cssPath) ? cssPath : null };
11352
10062
  }
11353
- const buildDir = import_node_path5.default.join(folder, "build");
11354
- const files = import_node_fs7.default.existsSync(buildDir) ? import_node_fs7.default.readdirSync(buildDir) : [];
10063
+ const buildDir = import_node_path6.default.join(folder, "build");
10064
+ const files = import_node_fs8.default.existsSync(buildDir) ? import_node_fs8.default.readdirSync(buildDir) : [];
11355
10065
  const js = files.find((file) => file.endsWith(".js") && !file.endsWith(".bundle.js") && !file.endsWith(".min.js"));
11356
10066
  const css = files.find((file) => file.endsWith(".css") && !file.endsWith(".min.css"));
11357
10067
  if (!js) throw new Error("Could not find harness JS artifact in build/");
11358
10068
  return {
11359
- jsPath: import_node_path5.default.join(buildDir, js),
11360
- cssPath: css ? import_node_path5.default.join(buildDir, css) : null
10069
+ jsPath: import_node_path6.default.join(buildDir, js),
10070
+ cssPath: css ? import_node_path6.default.join(buildDir, css) : null
11361
10071
  };
11362
10072
  }
11363
10073
  async function uploadVersion(params) {
11364
10074
  const creds = requireCredentials();
11365
10075
  const { jsPath, cssPath } = artifactPaths(params.folder, params.experiment);
11366
- if (!import_node_fs7.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
10076
+ if (!import_node_fs8.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
11367
10077
  const form = new FormData();
11368
10078
  form.append(
11369
10079
  "js",
11370
- new Blob([import_node_fs7.default.readFileSync(jsPath)], { type: "application/javascript" }),
11371
- import_node_path5.default.basename(jsPath)
10080
+ new Blob([import_node_fs8.default.readFileSync(jsPath)], { type: "application/javascript" }),
10081
+ import_node_path6.default.basename(jsPath)
11372
10082
  );
11373
- if (cssPath && import_node_fs7.default.existsSync(cssPath) && import_node_fs7.default.statSync(cssPath).size > 0) {
10083
+ if (cssPath && import_node_fs8.default.existsSync(cssPath) && import_node_fs8.default.statSync(cssPath).size > 0) {
11374
10084
  form.append(
11375
10085
  "css",
11376
- new Blob([import_node_fs7.default.readFileSync(cssPath)], { type: "text/css" }),
11377
- import_node_path5.default.basename(cssPath)
10086
+ new Blob([import_node_fs8.default.readFileSync(cssPath)], { type: "text/css" }),
10087
+ import_node_path6.default.basename(cssPath)
11378
10088
  );
11379
10089
  }
11380
10090
  form.append(
@@ -11383,8 +10093,8 @@ async function uploadVersion(params) {
11383
10093
  ...params.apex,
11384
10094
  resetJs: cleanupResetJs(params.experiment),
11385
10095
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
11386
- jsPath: import_node_path5.default.relative(params.folder, jsPath),
11387
- cssPath: cssPath ? import_node_path5.default.relative(params.folder, cssPath) : null
10096
+ jsPath: import_node_path6.default.relative(params.folder, jsPath),
10097
+ cssPath: cssPath ? import_node_path6.default.relative(params.folder, cssPath) : null
11388
10098
  })
11389
10099
  );
11390
10100
  const res = await uploadHarnessVersion(creds, params.session.sessionId, form);
@@ -11410,18 +10120,18 @@ async function createSession(folder) {
11410
10120
  return { session: res.data, apex, experiment };
11411
10121
  }
11412
10122
  var harnessCommand = new Command("harness").description("Develop AB Test Harness projects through Apex live QA sessions");
11413
- harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action21(folderArg, opts) {
10123
+ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action20(folderArg, opts) {
11414
10124
  const folder = resolveFolder(folderArg);
11415
10125
  const targetPath = apexConfigPath(folder);
11416
- if (import_node_fs7.default.existsSync(targetPath) && !opts.force) {
10126
+ if (import_node_fs8.default.existsSync(targetPath) && !opts.force) {
11417
10127
  fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
11418
10128
  }
11419
10129
  const { apex } = loadHarnessConfig(folder);
11420
- import_node_fs7.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
10130
+ import_node_fs8.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
11421
10131
  `, "utf8");
11422
10132
  output(this, { folder, configPath: targetPath, experiment: apex.experimentSlug, variation: apex.variationSlug }, () => printSuccess(`Created ${targetPath}`));
11423
10133
  });
11424
- harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action22(folderArg, opts) {
10134
+ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action21(folderArg, opts) {
11425
10135
  const folder = resolveFolder(folderArg);
11426
10136
  const created = await createSession(folder);
11427
10137
  await runAbHarness(["build", folder], folder);
@@ -11463,7 +10173,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11463
10173
  }
11464
10174
  };
11465
10175
  let debounce = null;
11466
- const watcher = import_node_fs7.default.watch(folder, { recursive: true }, (_event, filename) => {
10176
+ const watcher = import_node_fs8.default.watch(folder, { recursive: true }, (_event, filename) => {
11467
10177
  const name = String(filename ?? "");
11468
10178
  if (!name || name.startsWith("build/") || name.includes("/build/")) return;
11469
10179
  if (debounce) clearTimeout(debounce);
@@ -11477,7 +10187,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11477
10187
  process.exitCode = 0;
11478
10188
  });
11479
10189
  });
11480
- harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
10190
+ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action22(folderArg) {
11481
10191
  const folder = resolveFolder(folderArg);
11482
10192
  const created = await createSession(folder);
11483
10193
  await runAbHarness(["verify", folder], folder);
@@ -11490,9 +10200,9 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
11490
10200
 
11491
10201
  // src/commands/operator.ts
11492
10202
  init_source();
11493
- var import_node_crypto4 = __toESM(require("node:crypto"));
11494
- var import_node_fs8 = __toESM(require("node:fs"));
11495
- var import_node_path6 = __toESM(require("node:path"));
10203
+ var import_node_crypto3 = __toESM(require("node:crypto"));
10204
+ var import_node_fs9 = __toESM(require("node:fs"));
10205
+ var import_node_path7 = __toESM(require("node:path"));
11496
10206
  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)$/;
11497
10207
  function workspaceIssueFile(field, evidence) {
11498
10208
  if (field === "custom_js" || field === "reset_js") return "src/index.js";
@@ -11536,7 +10246,7 @@ function formatWorkspaceBlockingIssues(value) {
11536
10246
  }).join("\n");
11537
10247
  }
11538
10248
  function sha256Text2(value) {
11539
- return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
10249
+ return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
11540
10250
  }
11541
10251
  function byteLength(value) {
11542
10252
  return Buffer.byteLength(value, "utf8");
@@ -11546,28 +10256,28 @@ function slugify2(value, fallback2 = "operator") {
11546
10256
  return slug || fallback2;
11547
10257
  }
11548
10258
  function folderPath2(folder) {
11549
- return import_node_path6.default.resolve(process.cwd(), folder);
10259
+ return import_node_path7.default.resolve(process.cwd(), folder);
11550
10260
  }
11551
10261
  function apexDir(folder) {
11552
- return import_node_path6.default.join(folder, ".apex");
10262
+ return import_node_path7.default.join(folder, ".apex");
11553
10263
  }
11554
10264
  function sessionPath(folder) {
11555
- return import_node_path6.default.join(apexDir(folder), "session.json");
10265
+ return import_node_path7.default.join(apexDir(folder), "session.json");
11556
10266
  }
11557
10267
  function readJsonFile(filePath) {
11558
- if (!import_node_fs8.default.existsSync(filePath)) return null;
11559
- return JSON.parse(import_node_fs8.default.readFileSync(filePath, "utf8"));
10268
+ if (!import_node_fs9.default.existsSync(filePath)) return null;
10269
+ return JSON.parse(import_node_fs9.default.readFileSync(filePath, "utf8"));
11560
10270
  }
11561
10271
  function writeJsonFile(filePath, value) {
11562
- import_node_fs8.default.mkdirSync(import_node_path6.default.dirname(filePath), { recursive: true });
11563
- import_node_fs8.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
10272
+ import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(filePath), { recursive: true });
10273
+ import_node_fs9.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
11564
10274
  `, "utf8");
11565
10275
  }
11566
10276
  function readSession(folder) {
11567
10277
  const session = readJsonFile(sessionPath(folder));
11568
10278
  if (!session) {
11569
- const filesystemMarker = import_node_path6.default.join(apexDir(folder), "experiment-filesystem.json");
11570
- if (import_node_fs8.default.existsSync(filesystemMarker)) {
10279
+ const filesystemMarker = import_node_path7.default.join(apexDir(folder), "experiment-filesystem.json");
10280
+ if (import_node_fs9.default.existsSync(filesystemMarker)) {
11571
10281
  throw new Error("This folder is an experiment filesystem (apex experiments \u2026), not an operator workspace \u2014 run `apex experiments plan` here, or `apex workspace checkout` into a different folder.");
11572
10282
  }
11573
10283
  throw new Error(`Missing ${sessionPath(folder)}. Run apex workspace checkout first.`);
@@ -11575,9 +10285,9 @@ function readSession(folder) {
11575
10285
  return session;
11576
10286
  }
11577
10287
  function writeSession(folder, session) {
11578
- import_node_fs8.default.mkdirSync(apexDir(folder), { recursive: true });
11579
- import_node_fs8.default.writeFileSync(
11580
- import_node_path6.default.join(apexDir(folder), ".gitignore"),
10288
+ import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10289
+ import_node_fs9.default.writeFileSync(
10290
+ import_node_path7.default.join(apexDir(folder), ".gitignore"),
11581
10291
  "session.json\nharness-session.json\n",
11582
10292
  "utf8"
11583
10293
  );
@@ -11595,15 +10305,15 @@ function listLocalWorkspaceFiles(folder) {
11595
10305
  const roots = ["src", "goals", "segments", "network"];
11596
10306
  const topLevel = ["experiment.config.json", "metrics.json", "targeting.json", "dev-doc.md", "brand.md"];
11597
10307
  function visit(dir) {
11598
- if (!import_node_fs8.default.existsSync(dir)) return;
11599
- for (const entry of import_node_fs8.default.readdirSync(dir, { withFileTypes: true })) {
11600
- const absPath = import_node_path6.default.join(dir, entry.name);
10308
+ if (!import_node_fs9.default.existsSync(dir)) return;
10309
+ for (const entry of import_node_fs9.default.readdirSync(dir, { withFileTypes: true })) {
10310
+ const absPath = import_node_path7.default.join(dir, entry.name);
11601
10311
  if (entry.isDirectory()) {
11602
10312
  visit(absPath);
11603
10313
  continue;
11604
10314
  }
11605
- const relativePath = validateCanonicalPath(import_node_path6.default.relative(folder, absPath));
11606
- const content = import_node_fs8.default.readFileSync(absPath, "utf8");
10315
+ const relativePath = validateCanonicalPath(import_node_path7.default.relative(folder, absPath));
10316
+ const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11607
10317
  files.push({
11608
10318
  path: relativePath,
11609
10319
  absPath,
@@ -11613,11 +10323,11 @@ function listLocalWorkspaceFiles(folder) {
11613
10323
  });
11614
10324
  }
11615
10325
  }
11616
- for (const root of roots) visit(import_node_path6.default.join(folder, root));
10326
+ for (const root of roots) visit(import_node_path7.default.join(folder, root));
11617
10327
  for (const file of topLevel) {
11618
- const absPath = import_node_path6.default.join(folder, file);
11619
- if (!import_node_fs8.default.existsSync(absPath)) continue;
11620
- const content = import_node_fs8.default.readFileSync(absPath, "utf8");
10328
+ const absPath = import_node_path7.default.join(folder, file);
10329
+ if (!import_node_fs9.default.existsSync(absPath)) continue;
10330
+ const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11621
10331
  files.push({
11622
10332
  path: file,
11623
10333
  absPath,
@@ -11641,7 +10351,7 @@ function validateJsonFiles(files) {
11641
10351
  }
11642
10352
  }
11643
10353
  function readExperimentConfig(folder) {
11644
- return readJsonFile(import_node_path6.default.join(folder, "experiment.config.json")) ?? {};
10354
+ return readJsonFile(import_node_path7.default.join(folder, "experiment.config.json")) ?? {};
11645
10355
  }
11646
10356
  function resolveVariation(exp, variationArg) {
11647
10357
  const variations = exp.variations ?? [];
@@ -11658,9 +10368,9 @@ function resolveVariation(exp, variationArg) {
11658
10368
  function writeWorkspaceFiles(folder, workspace) {
11659
10369
  for (const file of workspace.files) {
11660
10370
  const relativePath = validateCanonicalPath(file.path);
11661
- const absPath = import_node_path6.default.join(folder, relativePath);
11662
- import_node_fs8.default.mkdirSync(import_node_path6.default.dirname(absPath), { recursive: true });
11663
- import_node_fs8.default.writeFileSync(absPath, file.content, "utf8");
10371
+ const absPath = import_node_path7.default.join(folder, relativePath);
10372
+ import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(absPath), { recursive: true });
10373
+ import_node_fs9.default.writeFileSync(absPath, file.content, "utf8");
11664
10374
  }
11665
10375
  }
11666
10376
  function sessionFromWorkspace(input) {
@@ -11724,10 +10434,10 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
11724
10434
  form.append(
11725
10435
  "js",
11726
10436
  new Blob([js.content], { type: "application/javascript" }),
11727
- import_node_path6.default.basename(js.path)
10437
+ import_node_path7.default.basename(js.path)
11728
10438
  );
11729
10439
  if (css && css.content.trim()) {
11730
- form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path6.default.basename(css.path));
10440
+ form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path7.default.basename(css.path));
11731
10441
  }
11732
10442
  form.append("manifest", JSON.stringify(manifest));
11733
10443
  const res = await uploadHarnessVersion(creds, sessionId, form);
@@ -11809,7 +10519,7 @@ Examples:
11809
10519
  apex workspace dev ./workspace --no-open
11810
10520
  apex workspace publish ./workspace
11811
10521
  `);
11812
- operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action24(experimentId, opts) {
10522
+ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").addOption(new Option("--dir <folder>", "Legacy alias for --out").hideHelp()).action(async function action23(experimentId, opts) {
11813
10523
  try {
11814
10524
  const creds = requireCredentials();
11815
10525
  const expRes = await getExperiment(creds, experimentId);
@@ -11828,7 +10538,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11828
10538
  const folder = folderPath2(opts.out ?? opts.dir ?? slugify2(`${experiment.name}-${variation.name}`, "apex-operator"));
11829
10539
  const workspace = await getOperatorWorkspace(creds, experiment.id, variation.id);
11830
10540
  if (!workspace.ok || !workspace.data) throw new CliError(workspace.error ?? "Could not load workspace", classifyStatus(workspace.status), workspace.errorDetails);
11831
- import_node_fs8.default.mkdirSync(folder, { recursive: true });
10541
+ import_node_fs9.default.mkdirSync(folder, { recursive: true });
11832
10542
  writeWorkspaceFiles(folder, workspace.data);
11833
10543
  writeSession(folder, sessionFromWorkspace({
11834
10544
  credsBaseUrl: creds.baseUrl,
@@ -11844,7 +10554,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11844
10554
  throw error;
11845
10555
  }
11846
10556
  });
11847
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
10557
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action24(folderArg) {
11848
10558
  try {
11849
10559
  const folder = folderPath2(folderArg);
11850
10560
  const session = readSession(folder);
@@ -11872,14 +10582,14 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
11872
10582
  fail(this, error instanceof Error ? error.message : String(error));
11873
10583
  }
11874
10584
  });
11875
- operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function action26(folderArg, opts) {
10585
+ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder", ".").option("--no-open", "Print preview URL instead of opening it").option("--once", "Upload one preview version and exit").action(async function action25(folderArg, opts) {
11876
10586
  try {
11877
10587
  const folder = folderPath2(folderArg);
11878
10588
  const session = readSession(folder);
11879
10589
  const harnessSession = await createExactHarnessSession(folder, session);
11880
10590
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
11881
- import_node_fs8.default.mkdirSync(apexDir(folder), { recursive: true });
11882
- writeJsonFile(import_node_path6.default.join(apexDir(folder), "harness-session.json"), {
10591
+ import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10592
+ writeJsonFile(import_node_path7.default.join(apexDir(folder), "harness-session.json"), {
11883
10593
  sessionId: harnessSession.sessionId,
11884
10594
  experimentId: harnessSession.experimentId,
11885
10595
  variationId: harnessSession.variationId,
@@ -11924,7 +10634,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11924
10634
  }
11925
10635
  };
11926
10636
  let timer = null;
11927
- const watcher = import_node_fs8.default.watch(folder, { recursive: true }, (_event, filename) => {
10637
+ const watcher = import_node_fs9.default.watch(folder, { recursive: true }, (_event, filename) => {
11928
10638
  const name = String(filename ?? "");
11929
10639
  if (!name || name.startsWith(".apex/") || name.includes("/.apex/")) return;
11930
10640
  if (timer) clearTimeout(timer);
@@ -11941,7 +10651,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11941
10651
  throw error;
11942
10652
  }
11943
10653
  });
11944
- operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function action27(folderArg, opts) {
10654
+ operatorCommand.command("publish").argument("[folder]", "Operator workspace folder", ".").option("--allow-goal-writes", "Allow metrics.json to create or update shared shop-level goals").action(async function action26(folderArg, opts) {
11945
10655
  try {
11946
10656
  const result = await runPublish(folderArg, opts);
11947
10657
  const build = result.build;
@@ -11959,8 +10669,8 @@ operatorCommand.command("publish").argument("[folder]", "Operator workspace fold
11959
10669
 
11960
10670
  // src/commands/roadmap.ts
11961
10671
  init_source();
11962
- var import_node_fs9 = __toESM(require("node:fs"));
11963
- var import_node_path7 = __toESM(require("node:path"));
10672
+ var import_node_fs10 = __toESM(require("node:fs"));
10673
+ var import_node_path8 = __toESM(require("node:path"));
11964
10674
  function truncate(value, max) {
11965
10675
  const text = typeof value === "string" ? value : value == null ? "" : String(value);
11966
10676
  if (text.length <= max) return text;
@@ -11984,7 +10694,7 @@ function briefLine(item) {
11984
10694
  ].filter(Boolean);
11985
10695
  return parts.join("\n");
11986
10696
  }
11987
- var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function action28(opts) {
10697
+ var approved = new Command("approved").description("List approved roadmap ideas ready to build in Apex").option("-n, --limit <n>", "Max results", "20").option("--cursor <id>", "Pagination cursor").option("-q, --query <text>", "Search title, hypothesis, brief, and notes").option("--unbuilt", "Only show approved ideas without a linked experiment").option("--built", "Only show approved ideas with a linked experiment").option("--json", "Print raw JSON").option("--brief", "Print each item with hypothesis and target URL").action(async function action27(opts) {
11988
10698
  const creds = requireCredentials();
11989
10699
  const limit = parseInt(opts.limit, 10);
11990
10700
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -12028,7 +10738,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
12028
10738
  }
12029
10739
  });
12030
10740
  });
12031
- var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function action29(id, opts) {
10741
+ var handoff = new Command("handoff").description("Get the full build brief for an approved roadmap item").argument("<id>", "Roadmap/backlog item ID").option("--out <path>", "Write handoff JSON to a file").addOption(new Option("-o, --output <path>", "Legacy alias for --out").hideHelp()).option("--instructions", "Print only the build instructions").action(async function action28(id, opts) {
12032
10742
  const creds = requireCredentials();
12033
10743
  const res = await getRoadmapItemHandoff(creds, id);
12034
10744
  if (!res.ok) fail(this, res.error ?? "Failed to fetch roadmap handoff", classifyStatus(res.status), res.errorDetails);
@@ -12037,9 +10747,9 @@ var handoff = new Command("handoff").description("Get the full build brief for a
12037
10747
  `;
12038
10748
  const outputData = opts.instructions ? { roadmapItemId: id, instructions: String(payload.instructions ?? "") } : payload;
12039
10749
  if (opts.out || opts.output) {
12040
- const target = import_node_path7.default.resolve(process.cwd(), opts.out ?? opts.output);
12041
- import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(target), { recursive: true });
12042
- import_node_fs9.default.writeFileSync(target, rendered, "utf8");
10750
+ const target = import_node_path8.default.resolve(process.cwd(), opts.out ?? opts.output);
10751
+ import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(target), { recursive: true });
10752
+ import_node_fs10.default.writeFileSync(target, rendered, "utf8");
12043
10753
  output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
12044
10754
  return;
12045
10755
  }
@@ -12051,8 +10761,8 @@ roadmapCommand.addCommand(handoff);
12051
10761
 
12052
10762
  // src/commands/tracking.ts
12053
10763
  init_source();
12054
- var import_node_fs10 = __toESM(require("node:fs"));
12055
- var import_node_path8 = __toESM(require("node:path"));
10764
+ var import_node_fs11 = __toESM(require("node:fs"));
10765
+ var import_node_path9 = __toESM(require("node:path"));
12056
10766
  function normalizeUrl(input) {
12057
10767
  const value = input.trim();
12058
10768
  if (!value) throw new Error("URL cannot be empty");
@@ -12098,17 +10808,17 @@ function commonCookieDomain(hostnames) {
12098
10808
  return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
12099
10809
  }
12100
10810
  function defaultManifestPath() {
12101
- return import_node_path8.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
10811
+ return import_node_path9.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
12102
10812
  }
12103
10813
  function readManifest(filePath) {
12104
- if (!import_node_fs10.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
12105
- const parsed = JSON.parse(import_node_fs10.default.readFileSync(filePath, "utf8"));
10814
+ if (!import_node_fs11.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
10815
+ const parsed = JSON.parse(import_node_fs11.default.readFileSync(filePath, "utf8"));
12106
10816
  if (parsed.schemaVersion !== 1) throw new Error("Unsupported tracking manifest schema");
12107
10817
  return parsed;
12108
10818
  }
12109
10819
  function writeManifest(filePath, manifest) {
12110
- import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(filePath), { recursive: true });
12111
- import_node_fs10.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
10820
+ import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(filePath), { recursive: true });
10821
+ import_node_fs11.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
12112
10822
  `, "utf8");
12113
10823
  }
12114
10824
  function buildSnippet(shopId, endpoint) {
@@ -12175,7 +10885,7 @@ function buildManifest(urlArgs, opts) {
12175
10885
  };
12176
10886
  }
12177
10887
  function manifestPath(input) {
12178
- return import_node_path8.default.resolve(process.cwd(), input ?? defaultManifestPath());
10888
+ return import_node_path9.default.resolve(process.cwd(), input ?? defaultManifestPath());
12179
10889
  }
12180
10890
  function printManifestPlan(manifest) {
12181
10891
  console.log(source_default.bold("Apex tracking ingestion plan"));
@@ -12223,7 +10933,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
12223
10933
  console.log(`${index + 1}. ${check}`);
12224
10934
  });
12225
10935
  });
12226
- trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function action30(opts) {
10936
+ trackingCommand.command("verify").description("Verify live tracking events for the current shop").option("--minutes <n>", "Lookback window in minutes", "120").option("--limit <n>", "Recent events to include", "20").option("--experiment <id>", "Filter to one experiment ID").option("--event-type <type>", "Filter to one event type; repeat with comma-separated values").action(async function action29(opts) {
12227
10937
  const creds = requireCredentials();
12228
10938
  const res = await verifyTracking(creds, {
12229
10939
  minutes: Number.parseInt(opts.minutes, 10),
@@ -12261,10 +10971,10 @@ trackingCommand.command("verify").description("Verify live tracking events for t
12261
10971
 
12262
10972
  // src/commands/goals.ts
12263
10973
  init_source();
12264
- var import_node_fs11 = __toESM(require("node:fs"));
12265
- var import_node_path9 = __toESM(require("node:path"));
10974
+ var import_node_fs12 = __toESM(require("node:fs"));
10975
+ var import_node_path10 = __toESM(require("node:path"));
12266
10976
 
12267
- // src/lib/sdk-url-match.ts
10977
+ // ../sdk/src/safe-regex.ts
12268
10978
  var MAX_PATTERN_LENGTH = 1024;
12269
10979
  var MAX_CACHE_ENTRIES = 256;
12270
10980
  var regexCache = /* @__PURE__ */ new Map();
@@ -12291,10 +11001,16 @@ function safeRegexTest(pattern, input) {
12291
11001
  }
12292
11002
  }
12293
11003
 
11004
+ // ../sdk/src/goal-eligibility.ts
11005
+ function goalIsEligible(goal, pageActiveIds, href) {
11006
+ if (goal.pageId) return pageActiveIds.has(goal.pageId);
11007
+ return !goal.urlPattern || safeRegexTest(goal.urlPattern, href);
11008
+ }
11009
+
12294
11010
  // src/lib/sdk-goal-url-match.ts
11011
+ var NO_ACTIVE_PAGES = /* @__PURE__ */ new Set();
12295
11012
  function sdkPageviewGoalMatch(urlPattern, href) {
12296
- if (!urlPattern) return true;
12297
- return safeRegexTest(urlPattern, href);
11013
+ return goalIsEligible({ urlPattern: urlPattern ?? null }, NO_ACTIVE_PAGES, href);
12298
11014
  }
12299
11015
 
12300
11016
  // src/commands/goals.ts
@@ -12411,7 +11127,7 @@ function printSuggestions(data) {
12411
11127
  function parseCapture(command, value) {
12412
11128
  if (!value) return void 0;
12413
11129
  const source = value.startsWith("@") ? value.slice(1) : value;
12414
- const text = value.startsWith("@") || import_node_fs11.default.existsSync(source) ? import_node_fs11.default.readFileSync(import_node_path9.default.resolve(process.cwd(), source), "utf8") : value;
11130
+ const text = value.startsWith("@") || import_node_fs12.default.existsSync(source) ? import_node_fs12.default.readFileSync(import_node_path10.default.resolve(process.cwd(), source), "utf8") : value;
12415
11131
  try {
12416
11132
  return JSON.parse(text);
12417
11133
  } catch (error) {
@@ -12430,6 +11146,8 @@ function printGoal(goal) {
12430
11146
  console.log(` ID: ${source_default.dim(goal.id)}`);
12431
11147
  console.log(` Type: ${goal.type}`);
12432
11148
  console.log(` Measurement: ${goal.measurement === "value" ? "value" : "conversion"}`);
11149
+ if (goal.value_property) console.log(` Value source: ${goal.value_property}`);
11150
+ if (goal.value_unit) console.log(` Value unit: ${goal.value_unit}`);
12433
11151
  if (goal.selector) console.log(` Selector: ${goal.selector}`);
12434
11152
  if (goal.text_match) console.log(` Text match: ${goal.text_match}`);
12435
11153
  if (goal.url_pattern) console.log(` URL: ${goal.url_pattern}`);
@@ -12437,7 +11155,7 @@ function printGoal(goal) {
12437
11155
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
12438
11156
  console.log();
12439
11157
  }
12440
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
11158
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action30(opts) {
12441
11159
  const creds = requireCredentials();
12442
11160
  const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
12443
11161
  if (!res.ok || !res.data) {
@@ -12459,12 +11177,14 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
12459
11177
  );
12460
11178
  });
12461
11179
  });
12462
- 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("--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 action32(opts) {
11180
+ 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 action31(opts) {
12463
11181
  const creds = requireCredentials();
12464
11182
  const body5 = cleanBody({
12465
11183
  name: opts.name,
12466
11184
  type: opts.type,
12467
11185
  measurement: opts.measurement,
11186
+ value_property: opts.valueProperty,
11187
+ value_unit: opts.valueUnit,
12468
11188
  selector: opts.selector,
12469
11189
  text_match: opts.textMatch,
12470
11190
  url_pattern: opts.urlPattern,
@@ -12478,12 +11198,14 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
12478
11198
  }
12479
11199
  output(this, { goal: res.data }, () => printGoal(res.data));
12480
11200
  });
12481
- var update = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").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 action33(id, opts) {
11201
+ var update = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").option("--value-property <property>", "Numeric event property for value goals; pass null to clear").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 action32(id, opts) {
12482
11202
  const creds = requireCredentials();
12483
11203
  const body5 = cleanBody({
12484
11204
  name: opts.name,
12485
11205
  type: opts.type,
12486
11206
  measurement: opts.measurement,
11207
+ value_property: opts.valueProperty === "null" ? null : opts.valueProperty,
11208
+ value_unit: opts.valueUnit === "null" ? null : opts.valueUnit,
12487
11209
  selector: opts.selector,
12488
11210
  text_match: opts.textMatch,
12489
11211
  url_pattern: opts.urlPattern,
@@ -12500,7 +11222,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
12500
11222
  }
12501
11223
  output(this, { goal: res.data }, () => printGoal(res.data));
12502
11224
  });
12503
- var test = new Command("test").description("Dry run a goal URL pattern against traffic").option("--pattern <regex>", "URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)").option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action34(opts) {
11225
+ var test = new Command("test").description("Dry run a goal URL pattern against traffic").option("--pattern <regex>", "URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)").option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action33(opts) {
12504
11226
  if (!opts.pattern && !opts.goal) fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
12505
11227
  if (opts.pattern && opts.goal) fail(this, "Use only one of --pattern or --goal.", EXIT_CODES.validation);
12506
11228
  const creds = requireCredentials();
@@ -12545,7 +11267,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
12545
11267
  printTable(["URL", "PAGEVIEWS", "VISITORS"], matches.map((item) => [`${item.host}${item.path}`.slice(0, 64), String(item.pageviews), String(item.visitors)]), [64, 10, 10]);
12546
11268
  });
12547
11269
  });
12548
- var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function action35(opts) {
11270
+ var suggest = new Command("suggest").description("Suggest goals and funnels from analytics").option("--create", "Create selected goals after confirmation").option("--yes", "Skip prompts when used with create").action(async function action34(opts) {
12549
11271
  const creds = requireCredentials();
12550
11272
  const res = await suggestGoals(creds);
12551
11273
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to suggest goals", classifyStatus(res.status), res.errorDetails);
@@ -12574,12 +11296,12 @@ Examples:
12574
11296
 
12575
11297
  // src/commands/funnels.ts
12576
11298
  init_source();
12577
- var import_node_fs12 = __toESM(require("node:fs"));
12578
- var import_node_path10 = __toESM(require("node:path"));
11299
+ var import_node_fs13 = __toESM(require("node:fs"));
11300
+ var import_node_path11 = __toESM(require("node:path"));
12579
11301
  function readSteps(command, file) {
12580
11302
  let parsed;
12581
11303
  try {
12582
- parsed = JSON.parse(import_node_fs12.default.readFileSync(import_node_path10.default.resolve(process.cwd(), file), "utf8"));
11304
+ parsed = JSON.parse(import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), file), "utf8"));
12583
11305
  } catch (error) {
12584
11306
  fail(command, `Invalid --steps-file JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.validation);
12585
11307
  }
@@ -12598,27 +11320,27 @@ function printFunnel(funnel) {
12598
11320
  for (const step of [...funnel.steps].sort((a, b) => a.order - b.order)) console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
12599
11321
  console.log();
12600
11322
  }
12601
- var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
11323
+ var list3 = new Command("list").alias("ls").description("List funnels").action(async function action35() {
12602
11324
  const res = await listFunnels(requireCredentials());
12603
11325
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list funnels", classifyStatus(res.status), res.errorDetails);
12604
11326
  output(this, { funnels: res.data.data }, () => printTable(["NAME", "ID", "STEPS"], res.data.data.map((f) => [f.name.slice(0, 36), f.id, String(f.steps.length)]), [36, 36, 5]));
12605
11327
  });
12606
- var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action37(opts) {
11328
+ var create3 = new Command("create").description("Create a funnel").requiredOption("--name <name>", "Funnel name").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action36(opts) {
12607
11329
  const res = await createFunnel(requireCredentials(), { name: opts.name, steps: readSteps(this, opts.stepsFile) });
12608
11330
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to create funnel", classifyStatus(res.status), res.errorDetails);
12609
11331
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12610
11332
  });
12611
- var update2 = new Command("update").description("Replace a funnel's steps").argument("<id>", "Funnel ID").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action38(id, opts) {
11333
+ var update2 = new Command("update").description("Replace a funnel's steps").argument("<id>", "Funnel ID").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(async function action37(id, opts) {
12612
11334
  const res = await updateFunnel(requireCredentials(), id, { steps: readSteps(this, opts.stepsFile) });
12613
11335
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update funnel", classifyStatus(res.status), res.errorDetails);
12614
11336
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12615
11337
  });
12616
- var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
11338
+ var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action38(id) {
12617
11339
  const res = await deleteFunnel(requireCredentials(), id);
12618
11340
  if (!res.ok) fail(this, res.error ?? "Failed to delete funnel", classifyStatus(res.status), res.errorDetails);
12619
11341
  output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
12620
11342
  });
12621
- var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action40(id, opts) {
11343
+ var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action39(id, opts) {
12622
11344
  if (!/^[1-9]\d*$/.test(opts.days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
12623
11345
  const days = Number.parseInt(opts.days, 10);
12624
11346
  if (!Number.isSafeInteger(days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
@@ -12626,7 +11348,7 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
12626
11348
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to run funnel", classifyStatus(res.status), res.errorDetails);
12627
11349
  output(this, { run: res.data }, () => printTable(["STEP", "LABEL", "VISITORS", "DROP-OFF"], res.data.steps.map((s) => [String(s.step), s.label, String(s.visitors), `${s.drop_off_rate}%`]), [5, 36, 8, 8]));
12628
11350
  });
12629
- var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action41(opts) {
11351
+ var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action40(opts) {
12630
11352
  const steps = readSteps(this, opts.stepsFile);
12631
11353
  output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
12632
11354
  });
@@ -12683,11 +11405,11 @@ async function load2(command) {
12683
11405
  }
12684
11406
  return result.data;
12685
11407
  }
12686
- var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
11408
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action41() {
12687
11409
  const { environments } = await load2(this);
12688
11410
  output(this, { environments }, () => printEnvironments(environments.items));
12689
11411
  });
12690
- var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function action43(domain, opts) {
11412
+ var setDomain = new Command("set-domain").description("Map a domain to a runtime environment").argument("<domain>", "Domain to map").requiredOption("--environment <env>", "Environment key").action(async function action42(domain, opts) {
12691
11413
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12692
11414
  if (!normalizedDomain) {
12693
11415
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12730,7 +11452,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
12730
11452
  () => printEnvironments(result.data.environments.items)
12731
11453
  );
12732
11454
  });
12733
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
11455
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action43(domain) {
12734
11456
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12735
11457
  if (!normalizedDomain) {
12736
11458
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12784,11 +11506,11 @@ function printConsent(data) {
12784
11506
  console.log(`Getter: ${data.consentMode.getter || "-"}`);
12785
11507
  console.log(`Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`);
12786
11508
  }
12787
- var get2 = new Command("get").description("Show consent settings").action(async function action45() {
11509
+ var get2 = new Command("get").description("Show consent settings").action(async function action44() {
12788
11510
  const data = await load3(this);
12789
11511
  output(this, data, () => printConsent(data));
12790
11512
  });
12791
- var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function action46(opts) {
11513
+ var set = new Command("set").description("Enable and configure consent gating").requiredOption("--provider <provider>", "Consent provider").requiredOption("--group <group>", "Required consent group").option("--getter <path>", "Custom consent getter path").action(async function action45(opts) {
12792
11514
  const consentMode = { enabled: true, provider: opts.provider, requiredGroup: opts.group };
12793
11515
  if (opts.getter !== void 0) consentMode.getter = opts.getter;
12794
11516
  const result = await updateConsent(requireCredentials(), { consentMode, applyCanonicalSplit: true });
@@ -12796,7 +11518,7 @@ var set = new Command("set").description("Enable and configure consent gating").
12796
11518
  warn(this, result.data.precompile);
12797
11519
  output(this, result.data, () => printConsent(result.data));
12798
11520
  });
12799
- var off = new Command("off").description("Disable consent gating").action(async function action47() {
11521
+ var off = new Command("off").description("Disable consent gating").action(async function action46() {
12800
11522
  const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
12801
11523
  if (!result.ok || !result.data) fail(this, result.error ?? "Failed to disable consent settings", classifyStatus(result.status), result.errorDetails);
12802
11524
  warn(this, result.data.precompile);
@@ -12831,21 +11553,21 @@ async function patchWithRetry(command, build) {
12831
11553
  warn2(command, result.data.precompile);
12832
11554
  return result.data;
12833
11555
  }
12834
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
11556
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action47() {
12835
11557
  const data = await load4(this);
12836
11558
  output(this, data, () => printDomains(data));
12837
11559
  });
12838
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
11560
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action48(domain) {
12839
11561
  const added = domain.map(normalizeDomain);
12840
11562
  const data = await patchWithRetry(this, (current) => Array.from(/* @__PURE__ */ new Set([...current.additionalDomains, ...added])));
12841
11563
  output(this, data, () => printDomains(data));
12842
11564
  });
12843
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
11565
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action49(domain) {
12844
11566
  const removeSet = new Set(domain.map(normalizeDomain));
12845
11567
  const data = await patchWithRetry(this, (current) => current.additionalDomains.filter((d) => !removeSet.has(normalizeDomain(d))));
12846
11568
  output(this, data, () => printDomains(data));
12847
11569
  });
12848
- var set2 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
11570
+ var set2 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action50(domain) {
12849
11571
  const replacement = domain.map(normalizeDomain);
12850
11572
  const data = await patchWithRetry(this, () => replacement);
12851
11573
  output(this, data, () => printDomains(data));
@@ -12864,7 +11586,11 @@ function normalizeDomain2(value) {
12864
11586
  return "";
12865
11587
  }
12866
11588
  }
12867
- var QA_FETCH_TIMEOUT_MS = 12e3;
11589
+ var QA_FETCH_TIMEOUT_MS = 15e3;
11590
+ function qaFetchTimeoutMs() {
11591
+ const testOverride = Number(cliTestSetting("QA_FETCH_TIMEOUT_MS"));
11592
+ return Number.isFinite(testOverride) && testOverride > 0 ? testOverride : QA_FETCH_TIMEOUT_MS;
11593
+ }
12868
11594
  var NAMED_HTML_ENTITIES = {
12869
11595
  amp: "&",
12870
11596
  lt: "<",
@@ -12902,29 +11628,29 @@ async function patchWithRetry2(command, build) {
12902
11628
  if (!shouldUseJson(command) && result.data.precompile?.status && result.data.precompile.status !== "ok") console.error(source_default.yellow(`Warning (${result.data.precompile.status}): ${result.data.precompile.summary}. Edge config may be stale until republish succeeds.`));
12903
11629
  return result.data;
12904
11630
  }
12905
- var get3 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
11631
+ var get3 = new Command("get").description("Show cross-domain handover settings").action(async function action51() {
12906
11632
  const data = await load5(this);
12907
11633
  output(this, data, () => printSettings(data.externalTracking));
12908
11634
  });
12909
- var set3 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option("--signed", "Require signed handover tokens (the default; unsigning is a platform-support action)").action(async function action53(opts) {
11635
+ var set3 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option("--signed", "Require signed handover tokens (the default; unsigning is a platform-support action)").action(async function action52(opts) {
12910
11636
  const domains = opts.domain.map(normalizeDomain2);
12911
11637
  if (domains.some((domain) => !domain)) fail(this, "Every --domain value must be a valid hostname containing a dot", EXIT_CODES.validation);
12912
11638
  if (opts.param && !/^[a-z_][a-z0-9_]{0,31}$/.test(opts.param)) fail(this, "--param must match ^[a-z_][a-z0-9_]{0,31}$", EXIT_CODES.validation);
12913
11639
  const data = await patchWithRetry2(this, (current) => ({ externalTracking: { domains: Array.from(new Set(domains)), handoverQueryParam: opts.param ?? current.externalTracking.handoverQueryParam, signed: true }, ifRevision: current.revision }));
12914
11640
  output(this, data, () => printSettings(data.externalTracking));
12915
11641
  });
12916
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
11642
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action53() {
12917
11643
  const data = await patchWithRetry2(this, (current) => ({ externalTracking: { domains: [], signed: true }, ifRevision: current.revision }));
12918
11644
  output(this, data, () => printSettings(data.externalTracking));
12919
11645
  });
12920
- var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action55(opts) {
11646
+ var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action54(opts) {
12921
11647
  const config = await load5(this);
12922
11648
  const target = normalizeDomain2(opts.toDomain);
12923
11649
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
12924
11650
  let response;
12925
11651
  let html;
12926
11652
  try {
12927
- response = await fetch(opts.from, { signal: AbortSignal.timeout(QA_FETCH_TIMEOUT_MS), redirect: "follow" });
11653
+ response = await fetch(opts.from, { signal: AbortSignal.timeout(qaFetchTimeoutMs()), redirect: "follow" });
12928
11654
  if (!response.ok) fail(this, `Fetching ${opts.from} returned HTTP ${response.status}`, classifyStatus(response.status));
12929
11655
  html = await response.text();
12930
11656
  } catch (error) {
@@ -12934,7 +11660,10 @@ var qa2 = new Command("qa").description("Check static link/form handover paramet
12934
11660
  }
12935
11661
  const param = config.externalTracking.handoverQueryParam;
12936
11662
  const baseUrl = response.url || opts.from;
12937
- const hrefs = [...html.matchAll(/<(a|form)\b[^>]*(?:href|action)\s*=\s*["']([^"']+)["'][^>]*>/gi)].map((m) => decodeHtmlEntities(m[2]));
11663
+ const hrefs = [
11664
+ ...html.matchAll(/<(?:a|form)\b[^>]*\s(?:href|action)\s*=\s*["']([^"']+)["'][^>]*>/gi),
11665
+ ...html.matchAll(/<[a-z][^>]*\s(?:data-href|data-url)\s*=\s*["']([^"']+)["'][^>]*>/gi)
11666
+ ].map((m) => decodeHtmlEntities(m[1]));
12938
11667
  const targetUrls = hrefs.map((raw) => {
12939
11668
  try {
12940
11669
  return new URL(raw, baseUrl);
@@ -13033,8 +11762,8 @@ function goalMatches(goal, rows) {
13033
11762
  var CHALLENGE_STATUSES = [401, 403, 429, 503];
13034
11763
  var PAGE_FETCH_TIMEOUT_MS = 12e3;
13035
11764
  async function fetchPrimaryHtml(host) {
13036
- if (process.env.APEX_CLI_MOCK_API === "1") {
13037
- const state = process.env.APEX_CLI_MOCK_DOCTOR_STATE;
11765
+ if (cliTestSetting("API") === "1") {
11766
+ const state = cliTestSetting("DOCTOR_STATE");
13038
11767
  if (state === "protected") return { protected: true };
13039
11768
  if (state === "page-error") return { error: "http 404" };
13040
11769
  if (state === "page-timeout") return { error: "The operation was aborted due to timeout" };
@@ -13092,8 +11821,8 @@ function servedConfigUrl(workerBase, shopId) {
13092
11821
  return `${workerBase}/config?shopId=${encodeURIComponent(shopId)}&env=${PRODUCTION_ENVIRONMENT_KEY}`;
13093
11822
  }
13094
11823
  async function fetchServedConfig(workerBase, shopId) {
13095
- if (process.env.APEX_CLI_MOCK_API === "1") {
13096
- const state = process.env.APEX_CLI_MOCK_DOCTOR_SERVED_CONFIG ?? "agree";
11824
+ if (cliTestSetting("API") === "1") {
11825
+ const state = cliTestSetting("DOCTOR_SERVED_CONFIG") ?? "agree";
13097
11826
  if (state === "unreachable") return { error: "mock worker is unreachable" };
13098
11827
  if (state === "diverge") return { config: { storeOrigins: ["https://example.com"], runtime: { consentMode: { enabled: false, provider: "custom" } } } };
13099
11828
  return { config: { storeOrigins: ["https://example.com", "https://www.example.com", "https://shop.example.com"], runtime: { consentMode: { enabled: true, provider: "cookiescript" } } } };
@@ -13190,7 +11919,7 @@ async function runDoctor(options = {}) {
13190
11919
  checks.push(unbound.length ? { checkId: "unbound_hosts", status: "FAIL", evidence: [...unbound.map((h) => `${h} sent pageviews but is not bound`), ...inventoryCaveat], fix: `apex domains add ${unbound[0]}` } : { checkId: "unbound_hosts", status: "PASS", evidence: ["All event hosts are bound to shop or environment domains.", ...inventoryCaveat] });
13191
11920
  checks.push(withCaveat(devPollutionCheck(eventHosts, envsRes.data), inventoryCaveat));
13192
11921
  const primary = hostOnly(domainsRes.data.domain) ?? eventHosts[0];
13193
- const shopId = resolveShopId(options);
11922
+ const shopId = resolveShopId(options) ?? domainsRes.data.shop_id;
13194
11923
  const workerBase = resolveWorkerBase(options);
13195
11924
  const [page, servedResult] = await Promise.all([
13196
11925
  primary ? fetchPrimaryHtml(primary) : Promise.resolve({}),
@@ -13200,10 +11929,11 @@ async function runDoctor(options = {}) {
13200
11929
  if (!primary) checks.push({ checkId: "cmp_mismatch", status: "SKIP", evidence: ["No storefront host is configured for this shop yet; add a domain to run the CMP check."] });
13201
11930
  else if (page.protected) checks.push({ checkId: "cmp_mismatch", status: "SKIP", evidence: [`Primary storefront ${primary} appears protected or bot-challenged; verify in browser.`] });
13202
11931
  else if (page.error !== void 0 || page.html === void 0) checks.push({ checkId: "cmp_mismatch", status: "SKIP", evidence: [`Could not read https://${primary}: ${page.error ?? "no response body"}`, "No CMP verdict was formed; an error page carries no consent markers."] });
11932
+ else if (consent.enabled && !consent.provider) checks.push({ checkId: "cmp_mismatch", status: "WARN", evidence: ["Consent mode is on, but no consent provider is configured", "Configure a consent provider before relying on consent gating"], fix: "apex consent set" });
13203
11933
  else {
13204
11934
  const cmpMarker = matchDetectionMarkers(page.html, "cmp")[0];
13205
11935
  if (!cmpMarker) {
13206
- checks.push(consent.enabled && consent.provider ? { checkId: "cmp_mismatch", status: "WARN", evidence: [`Consent mode is on with provider ${consent.provider}, but the served page carries no CMP marker`, `Expected CMP ${consent.provider} not found in served page`], fix: "apex consent set" } : { checkId: "cmp_mismatch", status: "PASS", evidence: ["No CMP marker detected."] });
11936
+ checks.push(consent.enabled ? { checkId: "cmp_mismatch", status: "WARN", evidence: [`Consent mode is on with provider ${consent.provider}, but the served page carries no CMP marker`, `Expected CMP ${consent.provider} not found in served page`], fix: "apex consent set" } : { checkId: "cmp_mismatch", status: "PASS", evidence: ["No CMP marker detected."] });
13207
11937
  } else {
13208
11938
  const vendor = cmpMarker.verdict;
13209
11939
  const mismatch = !consent.enabled || consent.provider && consent.provider.toLowerCase() !== vendor.toLowerCase();
@@ -13257,7 +11987,7 @@ function consentRateCheck(items) {
13257
11987
  const estimate = estimateConsentRate(items);
13258
11988
  return { checkId: "consent_rate", status: "PASS", evidence: [`estimatedAcceptRate=${estimate.estimatedAcceptRate ?? "unknown"}`, `Baseline ${describeWindow(estimate.baselineWindow)}; gated ${describeWindow(estimate.gatedWindow)}`, CAVEAT] };
13259
11989
  }
13260
- var doctorCommand = new Command("doctor").description("Check shop configuration against observed analytics").option("--shop-id <uuid>", "Shop ID used to read the Worker-served config; defaults to DRIP_SHOP_ID").option("--worker <url>", `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`).action(async function action56(options) {
11990
+ var doctorCommand = new Command("doctor").description("Check shop configuration against observed analytics").option("--shop-id <uuid>", "Override the shop ID discovered from the API (DRIP_SHOP_ID also overrides it)").option("--worker <url>", `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`).action(async function action55(options) {
13261
11991
  const data = await runDoctor(options);
13262
11992
  output(this, data, () => printDoctorReport(data.checks));
13263
11993
  if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.general;
@@ -13271,7 +12001,7 @@ function printDoctorReport(checks) {
13271
12001
  if (check.fix) console.log(` fix: ${check.fix}`);
13272
12002
  }
13273
12003
  }
13274
- var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option("--since <date>", "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows").action(async function action57(opts) {
12004
+ var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option("--since <date>", "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows").action(async function action56(opts) {
13275
12005
  if (opts.since !== void 0 && parseDay(opts.since) === null) fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
13276
12006
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
13277
12007
  if (!result.ok || !result.data) fail(this, result.error ?? "Failed to load visitor series", classifyStatus(result.status), result.errorDetails);
@@ -13310,14 +12040,14 @@ function analyticsSubcommand(name, description, defaultDays) {
13310
12040
  command.option("--planning-days <n>", "Planning window in days", "180");
13311
12041
  command.option("--limit <n>", "Maximum page rows", "50");
13312
12042
  }
13313
- return command.action(async function action70(opts) {
12043
+ return command.action(async function action69(opts) {
13314
12044
  const result = await getAnalytics(requireCredentials(), name, { days: positiveInteger(this, opts.days, "--days"), planningDays: positiveInteger(this, opts.planningDays, "--planning-days"), limit: positiveInteger(this, opts.limit, "--limit") });
13315
12045
  if (!result.ok || !result.data) fail(this, result.error ?? `Failed to load ${name} analytics`, classifyStatus(result.status), result.errorDetails);
13316
12046
  output(this, { data: result.data.data }, () => printAnalytics(result.data.data));
13317
12047
  });
13318
12048
  }
13319
12049
  function urlsCommand() {
13320
- return new Command("urls").description("Show storefront URL inventory from pageview analytics").option("-d, --days <n>", "Analytics window in days", "14").option("--host <host>", "Filter inventory to one host").option("--contains <substring>", "Server-side path substring filter").option("--match <regex>", "Client-side regex filter; exhausts inventory pages before filtering").option("--limit <n>", "Maximum rows to print", "50").action(async function action70(opts) {
12050
+ return new Command("urls").description("Show storefront URL inventory from pageview analytics").option("-d, --days <n>", "Analytics window in days", "14").option("--host <host>", "Filter inventory to one host").option("--contains <substring>", "Server-side path substring filter").option("--match <regex>", "Client-side regex filter; exhausts inventory pages before filtering").option("--limit <n>", "Maximum rows to print", "50").action(async function action69(opts) {
13321
12051
  const creds = requireCredentials();
13322
12052
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
13323
12053
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -13348,7 +12078,7 @@ function urlsCommand() {
13348
12078
  });
13349
12079
  });
13350
12080
  }
13351
- var analyticsCommand = new Command("analytics").description("Read shop analytics").addCommand(analyticsSubcommand("stats", "Show dashboard analytics stats", 7)).addCommand(analyticsSubcommand("timeseries", "Show analytics timeseries", 30)).addCommand(analyticsSubcommand("traffic", "Show traffic sources", 30)).addCommand(analyticsSubcommand("pages", "Show page analytics", 30)).addCommand(analyticsSubcommand("geo", "Show geography and device analytics", 30)).addCommand(analyticsSubcommand("sessions", "Show session analytics", 30)).addCommand(analyticsSubcommand("web-vitals", "Show web vitals analytics", 7)).addCommand(analyticsSubcommand("realtime", "Show realtime analytics")).addCommand(analyticsSubcommand("entry-exit", "Show entry and exit page analytics", 30)).addCommand(consentRateCommand).addCommand(urlsCommand()).addHelpText("after", `
12081
+ var analyticsCommand = new Command("analytics").description("Read shop analytics").addCommand(analyticsSubcommand("stats", "Show dashboard analytics stats", 7)).addCommand(analyticsSubcommand("timeseries", "Show analytics timeseries", 30)).addCommand(analyticsSubcommand("traffic", "Show traffic sources", 30)).addCommand(analyticsSubcommand("pages", "Show page analytics", 30)).addCommand(analyticsSubcommand("geo", "Show geography and device analytics", 30)).addCommand(analyticsSubcommand("sessions", "Show session analytics", 30)).addCommand(analyticsSubcommand("realtime", "Show realtime analytics")).addCommand(analyticsSubcommand("entry-exit", "Show entry and exit page analytics", 30)).addCommand(consentRateCommand).addCommand(urlsCommand()).addHelpText("after", `
13352
12082
  Examples:
13353
12083
  apex analytics stats --days 7
13354
12084
  apex analytics pages --planning-days 180 --limit 25
@@ -13394,18 +12124,10 @@ function privateIp(rawHost) {
13394
12124
  const [a, b] = host.split(".").map(Number);
13395
12125
  return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
13396
12126
  }
13397
- function mockResolve(hostname) {
13398
- if (process.env.APEX_CLI_MOCK_API !== "1") return null;
13399
- const mode = process.env.APEX_CLI_MOCK_DNS ?? "public";
13400
- if (mode === "fail") throw new Error(`getaddrinfo EAI_AGAIN ${hostname}`);
13401
- if (mode === "empty") return [];
13402
- if (mode === "mapped-loopback") return [{ address: "::ffff:127.0.0.1", family: 6 }];
13403
- if (mode === "mapped-private-hex") return [{ address: "::ffff:0a00:0001", family: 6 }];
13404
- if (mode === "unspecified-v6") return [{ address: "::", family: 6 }];
13405
- return [{ address: "93.184.216.34", family: 4 }];
13406
- }
12127
+ var testResolver;
12128
+ var testFetch;
13407
12129
  async function resolveHost(hostname) {
13408
- const mocked = mockResolve(hostname);
12130
+ const mocked = testResolver?.(hostname) ?? null;
13409
12131
  const addresses = mocked ?? await (0, import_promises3.lookup)(hostname, { all: true });
13410
12132
  return addresses.map((entry) => entry.address);
13411
12133
  }
@@ -13430,44 +12152,6 @@ async function validateUrl(raw, allowedHosts) {
13430
12152
  if (privateAddress) throw new UrlPolicyError(`Refusing ${url.hostname}; DNS resolves to a private address (${privateAddress})`);
13431
12153
  return { url, addresses };
13432
12154
  }
13433
- function mockFetch(url) {
13434
- if (process.env.APEX_CLI_MOCK_API !== "1") return null;
13435
- const oversizedFiller = "x".repeat(BODY_LIMIT_BYTES + 1024);
13436
- if (url.pathname === "/config") {
13437
- const mode = process.env.APEX_CLI_MOCK_CONFIG ?? "ok";
13438
- if (mode === "unreachable") throw new TypeError("fetch failed");
13439
- if (mode === "oversized") return new Response(JSON.stringify({ publicationRevision: 7, experiments: [], filler: oversizedFiller }), { status: 200, headers: { "content-type": "application/json" } });
13440
- if (mode === "gzipped") return new Response((0, import_node_zlib.gzipSync)(Buffer.from(JSON.stringify({ publicationRevision: 7, publication_revision: 7, experiments: [{ id: "experiment_1" }], runtime: { consentMode: { provider: "usercentrics", requiredGroup: "marketing" } }, delivery: { revision: 7, scriptUrl: `${url.origin}/s/shop_1.js?v=7`, deliveryState: "no_visual_tests" } }))), { status: 200, headers: { "content-type": "application/json", "content-encoding": "gzip" } });
13441
- if (mode === "unknown-encoding") return new Response("compressed-with-something-else", { status: 200, headers: { "content-type": "application/json", "content-encoding": "zstd" } });
13442
- if (mode === "http-error") return new Response(JSON.stringify({ error: "Failed to fetch config" }), { status: 502, headers: { "content-type": "application/json" } });
13443
- if (mode === "unrecognized") return new Response(JSON.stringify({ unexpected: true }), { status: 200, headers: { "content-type": "application/json" } });
13444
- const shopId = url.searchParams.get("shopId") ?? "shop_1";
13445
- return new Response(JSON.stringify({
13446
- publication_revision: 7,
13447
- publicationRevision: 7,
13448
- generated_at: "2026-08-05T00:00:00.000Z",
13449
- experiments: [{ id: "experiment_1" }],
13450
- runtime: { consentMode: { enabled: true, provider: "usercentrics", getter: "", requiredGroup: "marketing" } },
13451
- delivery: { version: 1, revision: 7, scriptUrl: `${url.origin}/s/${shopId}.js?v=7`, deliveryState: "no_visual_tests" },
13452
- geo: { country: "DE" }
13453
- }), { status: 200, headers: { "content-type": "application/json" } });
13454
- }
13455
- const page = process.env.APEX_CLI_MOCK_PAGE ?? "installed";
13456
- const installedHtml = `<html><head><script src="${PRODUCTION_WORKER_BASE_URL2}/s/shop_1.js?v=7"></script></head><body>ok</body></html>`;
13457
- if (page === "not-found") return new Response(`<html><body>Not found</body>${installedHtml}</html>`, { status: 404, headers: { "content-type": "text/html" } });
13458
- if (page === "forbidden") return new Response("<html><body>Forbidden</body></html>", { status: 403, headers: { "content-type": "text/html" } });
13459
- if (page === "challenge") return new Response('<html><title>Just a moment...</title><script src="/cdn-cgi/challenge-platform/h/b/orchestrate/chl_page/v1"></script></html>', { status: 403, headers: { "content-type": "text/html" } });
13460
- if (page === "oversized") return new Response(`${installedHtml}<!--${oversizedFiller}-->`, { status: 200, headers: { "content-type": "text/html" } });
13461
- if (page === "gzipped") return new Response((0, import_node_zlib.gzipSync)(Buffer.from(installedHtml)), { status: 200, headers: { "content-type": "text/html", "content-encoding": "gzip" } });
13462
- if (page === "corrupt-gzip-trailer") {
13463
- const archive2 = (0, import_node_zlib.gzipSync)(Buffer.from(`${installedHtml}<!--${"x".repeat(64 * 1024)}-->`));
13464
- archive2[archive2.length - 8] ^= 255;
13465
- return new Response(archive2, { status: 200, headers: { "content-type": "text/html", "content-encoding": "gzip" } });
13466
- }
13467
- if (page === "unknown-encoding") return new Response("compressed-with-something-else", { status: 200, headers: { "content-type": "text/html", "content-encoding": "zstd" } });
13468
- if (page === "prose-shopid") return new Response("<html><body><p>Set your shopId to shop_1 in the docs.</p></body></html>", { status: 200, headers: { "content-type": "text/html" } });
13469
- return new Response(installedHtml, { status: 200, headers: { "content-type": "text/html" } });
13470
- }
13471
12155
  var REQUEST_TIMEOUT_MS = 1e4;
13472
12156
  var RESPONSE_DEADLINE_MS = 15e3;
13473
12157
  var BODY_LIMIT_BYTES = 2 * 1024 * 1024;
@@ -13496,10 +12180,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
13496
12180
  const chunks = [];
13497
12181
  let bytes = 0;
13498
12182
  let settled = false;
13499
- const settle = (action70) => {
12183
+ const settle = (action69) => {
13500
12184
  if (settled) return;
13501
12185
  settled = true;
13502
- action70();
12186
+ action69();
13503
12187
  };
13504
12188
  stream.on("data", (chunk) => {
13505
12189
  if (settled) return;
@@ -13550,11 +12234,11 @@ function pinnedFetch(url, addresses, limits) {
13550
12234
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
13551
12235
  };
13552
12236
  let settled = false;
13553
- const settle = (action70) => {
12237
+ const settle = (action69) => {
13554
12238
  if (settled) return;
13555
12239
  settled = true;
13556
12240
  clearTimeout(deadline);
13557
- action70();
12241
+ action69();
13558
12242
  };
13559
12243
  const req = send2(url, {
13560
12244
  method: "GET",
@@ -13631,7 +12315,7 @@ async function enforceBodyLimit(url, response, limits) {
13631
12315
  async function fetchSafe(raw, allowedHosts, limits) {
13632
12316
  let target = await validateUrl(raw, allowedHosts);
13633
12317
  for (let hop = 0; hop <= 5; hop++) {
13634
- const mocked = mockFetch(target.url);
12318
+ const mocked = testFetch?.(target.url) ?? null;
13635
12319
  const result = mocked ? await enforceBodyLimit(target.url, mocked, limits) : await pinnedFetch(target.url, target.addresses, limits);
13636
12320
  if (![301, 302, 303, 307, 308].includes(result.response.status)) return result;
13637
12321
  const location = result.response.headers.get("location");
@@ -13743,13 +12427,14 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
13743
12427
  message: "Worker served a recognized config document for this shop."
13744
12428
  };
13745
12429
  }
13746
- var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description("Verify snippet, served config, and recent tracking for the current shop").option("--url <url>", "Check one explicit bound storefront URL").option("--shop-id <id>", "Apex shop ID to verify (falls back to DRIP_SHOP_ID)").option("--worker-url <url>", "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)").option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
12430
+ var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description("Verify snippet, served config, and recent tracking for the current shop").option("--url <url>", "Check one explicit bound storefront URL").option("--shop-id <id>", "Apex shop ID to verify (falls back to DRIP_SHOP_ID)").option("--worker-url <url>", "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)").option("--environment <key>", "Environment key to request from the worker", "production").action(async function action57(opts) {
13747
12431
  const creds = requireCredentials();
13748
12432
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
13749
12433
  if (!shopId) fail(this, "Shop ID is required: pass --shop-id <id> or set DRIP_SHOP_ID", EXIT_CODES.validation, { field: "shop-id" });
13750
12434
  const domains = await getInstallDomains(creds);
13751
12435
  if (!domains.ok || !domains.data) fail(this, domains.error ?? "Failed to load install domains", classifyStatus(domains.status), domains.errorDetails);
13752
- const storeOrigins = process.env.APEX_CLI_MOCK_API === "1" && process.env.APEX_CLI_MOCK_STORE_ORIGINS ? process.env.APEX_CLI_MOCK_STORE_ORIGINS.split(",") : domains.data.storeOrigins;
12436
+ const mockStoreOrigins = cliTestSetting("STORE_ORIGINS");
12437
+ const storeOrigins = cliTestSetting("API") === "1" && mockStoreOrigins ? mockStoreOrigins.split(",") : domains.data.storeOrigins;
13753
12438
  const origins = opts.url ? [opts.url] : storeOrigins;
13754
12439
  const allowedHosts = new Set(storeOrigins.map((origin) => normalizeHostname(new URL(origin).hostname)));
13755
12440
  const pages = [];
@@ -13808,11 +12493,11 @@ var installCommand = new Command("install").description("Check Apex storefront i
13808
12493
 
13809
12494
  // src/commands/flags.ts
13810
12495
  init_source();
13811
- var import_node_fs13 = __toESM(require("node:fs"));
13812
- var import_node_path11 = __toESM(require("node:path"));
12496
+ var import_node_fs14 = __toESM(require("node:fs"));
12497
+ var import_node_path12 = __toESM(require("node:path"));
13813
12498
 
13814
12499
  // src/edge-status.ts
13815
- function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 420) {
12500
+ function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
13816
12501
  return deliveryType === "api" ? { edge_delivery: "not_applicable", message: `API-delivered flag \u2014 edge delivery is not applicable. A previously cached visual payload may remain at some PoPs for up to ~${expectedWorstCaseSeconds}s; a single-vantage status cannot prove global propagation.` } : { edge_delivery: "applicable", message: null };
13817
12502
  }
13818
12503
  function parsePublicationMetadata(value) {
@@ -13837,7 +12522,7 @@ function compareEdgePublication(currentValue, edgeValue, expectedWorstCaseSecond
13837
12522
  return { ...base, verdict: "live", relationship: edge.publication_revision > current.publication_revision ? "edge_ahead" : "equal" };
13838
12523
  }
13839
12524
  function formatEdgePropagationStatus(status2) {
13840
- const caveat = "This checks one CDN vantage point; other PoPs may lag by up to max-age + SWR.";
12525
+ const caveat = "This checks one CDN vantage point; other PoPs may lag by up to max-age.";
13841
12526
  if (status2.verdict === "pending_publication") return `Pending publication \u2014 flag updated ${status2.flag_updated_at}, after the latest publication generated ${status2.current.generated_at ?? "never"}; the latest change is not live. After publication, allow ~${status2.expected_worst_case_seconds}s for propagation. ${caveat}`;
13842
12527
  if (status2.verdict === "unknown") return `Unknown \u2014 edge payload predates publication metadata. ${caveat}`;
13843
12528
  if (status2.verdict === "propagating") return `Propagating \u2014 edge revision ${status2.edge.publication_revision} (generated ${status2.edge.generated_at}) vs current revision ${status2.current.publication_revision} (generated ${status2.current.generated_at}); expected within ~${status2.expected_worst_case_seconds}s after generation. ${caveat}`;
@@ -13851,7 +12536,7 @@ function body(command, value, opts) {
13851
12536
  let parsed = {};
13852
12537
  if (value) {
13853
12538
  const source = value.startsWith("@") ? value.slice(1) : value;
13854
- const text = value.startsWith("@") || import_node_fs13.default.existsSync(source) ? import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), source), "utf8") : value;
12539
+ const text = value.startsWith("@") || import_node_fs14.default.existsSync(source) ? import_node_fs14.default.readFileSync(import_node_path12.default.resolve(process.cwd(), source), "utf8") : value;
13855
12540
  try {
13856
12541
  parsed = JSON.parse(text);
13857
12542
  } catch (e) {
@@ -13979,18 +12664,18 @@ function write(name) {
13979
12664
  });
13980
12665
  return c;
13981
12666
  }
13982
- function lifecycle(action70) {
13983
- return new Command(action70).description(`${action70[0].toUpperCase()}${action70.slice(1)} a flag`).argument("<id>").action(async function(id) {
12667
+ function lifecycle(action69) {
12668
+ return new Command(action69).description(`${action69[0].toUpperCase()}${action69.slice(1)} a flag`).argument("<id>").action(async function(id) {
13984
12669
  const r = await transitionManagedDelivery(
13985
12670
  requireCredentials(),
13986
12671
  kind,
13987
12672
  id,
13988
- action70
12673
+ action69
13989
12674
  );
13990
12675
  if (!r.ok || !r.data)
13991
12676
  fail(
13992
12677
  this,
13993
- r.error ?? `Failed to ${action70}`,
12678
+ r.error ?? `Failed to ${action69}`,
13994
12679
  classifyStatus(r.status),
13995
12680
  r.errorDetails
13996
12681
  );
@@ -14006,8 +12691,8 @@ Examples:
14006
12691
  `);
14007
12692
 
14008
12693
  // src/commands/backlog.ts
14009
- var import_node_fs14 = __toESM(require("node:fs"));
14010
- var import_node_path12 = __toESM(require("node:path"));
12694
+ var import_node_fs15 = __toESM(require("node:fs"));
12695
+ var import_node_path13 = __toESM(require("node:path"));
14011
12696
  init_source();
14012
12697
  var kind2 = "backlog";
14013
12698
  var singular = "backlog item";
@@ -14015,7 +12700,7 @@ function parse(command, value) {
14015
12700
  if (!value) return {};
14016
12701
  try {
14017
12702
  const source = value.startsWith("@") ? value.slice(1) : value;
14018
- const text = value.startsWith("@") || import_node_fs14.default.existsSync(source) ? import_node_fs14.default.readFileSync(import_node_path12.default.resolve(process.cwd(), source), "utf8") : value;
12703
+ const text = value.startsWith("@") || import_node_fs15.default.existsSync(source) ? import_node_fs15.default.readFileSync(import_node_path13.default.resolve(process.cwd(), source), "utf8") : value;
14019
12704
  const body5 = JSON.parse(text);
14020
12705
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
14021
12706
  return body5;
@@ -14040,18 +12725,18 @@ var get5 = new Command("get").description("Get a backlog item").argument("<id>",
14040
12725
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
14041
12726
  output(this, { backlog_item: r.data }, () => detail2(r.data));
14042
12727
  });
14043
- function write2(action70) {
14044
- const c = new Command(action70).description(`${action70 === "create" ? "Create" : "Update"} a ${singular}`);
14045
- if (action70 === "update") c.argument("<id>", `${singular} ID`);
12728
+ function write2(action69) {
12729
+ const c = new Command(action69).description(`${action69 === "create" ? "Create" : "Update"} a ${singular}`);
12730
+ if (action69 === "update") c.argument("<id>", `${singular} ID`);
14046
12731
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
14047
12732
  c.action(async function(...args) {
14048
12733
  const o = this.opts();
14049
- const id = action70 === "update" ? String(args[0]) : void 0;
14050
- const raw = o.file ? `@${o.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
12734
+ const id = action69 === "update" ? String(args[0]) : void 0;
12735
+ const raw = o.file ? `@${o.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14051
12736
  const body5 = parse(this, raw);
14052
12737
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
14053
- const r = action70 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
14054
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
12738
+ const r = action69 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
12739
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action69}`, classifyStatus(r.status), r.errorDetails);
14055
12740
  output(this, { backlog_item: r.data }, () => detail2(r.data));
14056
12741
  });
14057
12742
  return c;
@@ -14086,8 +12771,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
14086
12771
  var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get5).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
14087
12772
 
14088
12773
  // src/commands/signals.ts
14089
- var import_node_fs15 = __toESM(require("node:fs"));
14090
- var import_node_path13 = __toESM(require("node:path"));
12774
+ var import_node_fs16 = __toESM(require("node:fs"));
12775
+ var import_node_path14 = __toESM(require("node:path"));
14091
12776
  init_source();
14092
12777
  var kind3 = "signals";
14093
12778
  var singular2 = "signal";
@@ -14095,7 +12780,7 @@ function parse2(command, value) {
14095
12780
  if (!value) return {};
14096
12781
  try {
14097
12782
  const source = value.startsWith("@") ? value.slice(1) : value;
14098
- const text = value.startsWith("@") || import_node_fs15.default.existsSync(source) ? import_node_fs15.default.readFileSync(import_node_path13.default.resolve(process.cwd(), source), "utf8") : value;
12783
+ const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
14099
12784
  const body5 = JSON.parse(text);
14100
12785
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
14101
12786
  return body5;
@@ -14120,18 +12805,18 @@ var get6 = new Command("get").description("Get a signal").argument("<id>", "sign
14120
12805
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
14121
12806
  output(this, { signal: r.data }, () => detail3(r.data));
14122
12807
  });
14123
- function write3(action70) {
14124
- const c = new Command(action70).description(`${action70 === "create" ? "Create" : "Update"} a ${singular2}`);
14125
- if (action70 === "update") c.argument("<id>", `${singular2} ID`);
12808
+ function write3(action69) {
12809
+ const c = new Command(action69).description(`${action69 === "create" ? "Create" : "Update"} a ${singular2}`);
12810
+ if (action69 === "update") c.argument("<id>", `${singular2} ID`);
14126
12811
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
14127
12812
  c.action(async function(...args) {
14128
12813
  const o = this.opts();
14129
- const id = action70 === "update" ? String(args[0]) : void 0;
14130
- const raw = o.file ? `@${o.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
12814
+ const id = action69 === "update" ? String(args[0]) : void 0;
12815
+ const raw = o.file ? `@${o.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14131
12816
  const body5 = parse2(this, raw);
14132
12817
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
14133
- const r = action70 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
14134
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
12818
+ const r = action69 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
12819
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action69}`, classifyStatus(r.status), r.errorDetails);
14135
12820
  output(this, { signal: r.data }, () => detail3(r.data));
14136
12821
  });
14137
12822
  return c;
@@ -14145,14 +12830,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
14145
12830
 
14146
12831
  // src/commands/personalizations.ts
14147
12832
  init_source();
14148
- var import_node_fs16 = __toESM(require("node:fs"));
14149
- var import_node_path14 = __toESM(require("node:path"));
12833
+ var import_node_fs17 = __toESM(require("node:fs"));
12834
+ var import_node_path15 = __toESM(require("node:path"));
14150
12835
  var kind4 = "personalizations";
14151
12836
  function body2(command, value, opts) {
14152
12837
  let parsed = {};
14153
12838
  if (value) {
14154
12839
  const source = value.startsWith("@") ? value.slice(1) : value;
14155
- const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
12840
+ const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
14156
12841
  try {
14157
12842
  parsed = JSON.parse(text);
14158
12843
  } catch (e) {
@@ -14249,20 +12934,20 @@ function write4(name) {
14249
12934
  });
14250
12935
  return c;
14251
12936
  }
14252
- function lifecycle2(action70) {
14253
- return new Command(action70).description(
14254
- `${action70[0].toUpperCase()}${action70.slice(1)} a personalization`
12937
+ function lifecycle2(action69) {
12938
+ return new Command(action69).description(
12939
+ `${action69[0].toUpperCase()}${action69.slice(1)} a personalization`
14255
12940
  ).argument("<id>").action(async function(id) {
14256
12941
  const r = await transitionManagedDelivery(
14257
12942
  requireCredentials(),
14258
12943
  kind4,
14259
12944
  id,
14260
- action70
12945
+ action69
14261
12946
  );
14262
12947
  if (!r.ok || !r.data)
14263
12948
  fail(
14264
12949
  this,
14265
- r.error ?? `Failed to ${action70}`,
12950
+ r.error ?? `Failed to ${action69}`,
14266
12951
  classifyStatus(r.status),
14267
12952
  r.errorDetails
14268
12953
  );
@@ -14273,13 +12958,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
14273
12958
 
14274
12959
  // src/commands/targeting-resources.ts
14275
12960
  init_source();
14276
- var import_node_fs17 = __toESM(require("node:fs"));
14277
- var import_node_path15 = __toESM(require("node:path"));
12961
+ var import_node_fs18 = __toESM(require("node:fs"));
12962
+ var import_node_path16 = __toESM(require("node:path"));
14278
12963
  function body3(command, value) {
14279
12964
  if (!value) return {};
14280
12965
  const source = value.startsWith("@") ? value.slice(1) : value;
14281
12966
  try {
14282
- const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
12967
+ const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
14283
12968
  return JSON.parse(text);
14284
12969
  } catch (error) {
14285
12970
  fail(
@@ -14358,17 +13043,17 @@ function targetingResourceCommand(kind5) {
14358
13043
  );
14359
13044
  })
14360
13045
  );
14361
- for (const action70 of ["create", "update"]) {
14362
- const command = new Command(action70).description(
14363
- `${action70 === "create" ? "Create" : "Update"} a ${singular3}`
13046
+ for (const action69 of ["create", "update"]) {
13047
+ const command = new Command(action69).description(
13048
+ `${action69 === "create" ? "Create" : "Update"} a ${singular3}`
14364
13049
  );
14365
- if (action70 === "update") command.argument("<id>");
13050
+ if (action69 === "update") command.argument("<id>");
14366
13051
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
14367
13052
  const opts = this.opts();
14368
- const id = action70 === "update" ? String(args[0]) : void 0;
14369
- const value = opts.file ? `@${opts.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
13053
+ const id = action69 === "update" ? String(args[0]) : void 0;
13054
+ const value = opts.file ? `@${opts.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14370
13055
  const requestBody = body3(this, value);
14371
- if (action70 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
13056
+ if (action69 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
14372
13057
  fail(
14373
13058
  this,
14374
13059
  `Updating a ${singular3} requires at least one field.`,
@@ -14376,7 +13061,7 @@ function targetingResourceCommand(kind5) {
14376
13061
  );
14377
13062
  const response = await requestTargetingResource(
14378
13063
  requireCredentials(),
14379
- action70 === "create" ? "POST" : "PATCH",
13064
+ action69 === "create" ? "POST" : "PATCH",
14380
13065
  kind5,
14381
13066
  id,
14382
13067
  requestBody
@@ -14384,7 +13069,7 @@ function targetingResourceCommand(kind5) {
14384
13069
  if (!response.ok || !response.data)
14385
13070
  fail(
14386
13071
  this,
14387
- response.error ?? `Failed to ${action70}`,
13072
+ response.error ?? `Failed to ${action69}`,
14388
13073
  classifyStatus(response.status),
14389
13074
  response.errorDetails
14390
13075
  );
@@ -14393,7 +13078,7 @@ function targetingResourceCommand(kind5) {
14393
13078
  {
14394
13079
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
14395
13080
  },
14396
- () => console.log(source_default.green(`${action70}d ${singular3}`))
13081
+ () => console.log(source_default.green(`${action69}d ${singular3}`))
14397
13082
  );
14398
13083
  });
14399
13084
  root.addCommand(command);
@@ -14480,7 +13165,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
14480
13165
  // src/commands/brand.ts
14481
13166
  init_source();
14482
13167
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
14483
- var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
13168
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action58() {
14484
13169
  const creds = requireCredentials();
14485
13170
  const res = await getBrandContext(creds);
14486
13171
  if (res.status === 404) {
@@ -14513,7 +13198,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
14513
13198
 
14514
13199
  // src/commands/targeting.ts
14515
13200
  init_source();
14516
- var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action60(url, opts) {
13201
+ var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action59(url, opts) {
14517
13202
  const creds = requireCredentials();
14518
13203
  const res = await testTargeting(creds, url, { status: opts.status });
14519
13204
  if (!res.ok || !res.data) {
@@ -14553,13 +13238,13 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
14553
13238
 
14554
13239
  // src/commands/sos.ts
14555
13240
  init_source();
14556
- var import_node_fs18 = __toESM(require("node:fs"));
14557
- var import_node_path16 = __toESM(require("node:path"));
13241
+ var import_node_fs19 = __toESM(require("node:fs"));
13242
+ var import_node_path17 = __toESM(require("node:path"));
14558
13243
  var SOS_TYPES = /* @__PURE__ */ new Set(["full_handover", "design", "development", "qa", "config", "bug"]);
14559
13244
  function readText(value) {
14560
13245
  if (!value) return void 0;
14561
13246
  if (!value.startsWith("@")) return value;
14562
- return import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), value.slice(1)), "utf8");
13247
+ return import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), value.slice(1)), "utf8");
14563
13248
  }
14564
13249
  function printTicket(ticket, reused) {
14565
13250
  console.log();
@@ -14570,7 +13255,7 @@ function printTicket(ticket, reused) {
14570
13255
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
14571
13256
  console.log();
14572
13257
  }
14573
- var create4 = new Command("create").description("Create an Apex SOS ticket").option("--type <type>", "full_handover|design|development|qa|config|bug", "qa").option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action61(descriptionArg, opts) {
13258
+ var create4 = new Command("create").description("Create an Apex SOS ticket").option("--type <type>", "full_handover|design|development|qa|config|bug", "qa").option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action60(descriptionArg, opts) {
14574
13259
  if (!SOS_TYPES.has(opts.type)) {
14575
13260
  fail(this, "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.", 3);
14576
13261
  }
@@ -14588,7 +13273,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
14588
13273
  }
14589
13274
  output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
14590
13275
  });
14591
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
13276
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action61() {
14592
13277
  const creds = requireCredentials();
14593
13278
  const res = await listMySos(creds);
14594
13279
  if (!res.ok || !res.data) {
@@ -14641,13 +13326,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
14641
13326
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send).addCommand(markRead);
14642
13327
 
14643
13328
  // src/commands/shops.ts
14644
- var import_node_fs19 = __toESM(require("node:fs"));
14645
- var import_node_path17 = __toESM(require("node:path"));
13329
+ var import_node_fs20 = __toESM(require("node:fs"));
13330
+ var import_node_path18 = __toESM(require("node:path"));
14646
13331
  function body4(command, raw) {
14647
13332
  if (!raw) return {};
14648
13333
  try {
14649
13334
  const source = raw.startsWith("@") ? raw.slice(1) : raw;
14650
- const text = raw.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(source), "utf8") : raw;
13335
+ const text = raw.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(source), "utf8") : raw;
14651
13336
  const value = JSON.parse(text);
14652
13337
  if (!value || typeof value !== "object" || Array.isArray(value))
14653
13338
  throw new Error("body must be an object");
@@ -14909,7 +13594,7 @@ function commandSchema(command) {
14909
13594
  };
14910
13595
  }
14911
13596
  function createSchemaCommand(program3) {
14912
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action70() {
13597
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action69() {
14913
13598
  const schema = commandSchema(program3);
14914
13599
  output(this, schema, () => {
14915
13600
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -14919,16 +13604,16 @@ function createSchemaCommand(program3) {
14919
13604
  }
14920
13605
 
14921
13606
  // src/commands/skills.ts
14922
- var import_node_fs20 = __toESM(require("node:fs"));
14923
- var import_node_path18 = __toESM(require("node:path"));
13607
+ var import_node_fs21 = __toESM(require("node:fs"));
13608
+ var import_node_path19 = __toESM(require("node:path"));
14924
13609
  function packageRoot() {
14925
13610
  const candidates = [
14926
- import_node_path18.default.resolve(__dirname, ".."),
14927
- import_node_path18.default.resolve(import_node_path18.default.dirname(process.argv[1] ?? process.cwd()), ".."),
14928
- import_node_path18.default.resolve(process.cwd(), "packages/cli")
13611
+ import_node_path19.default.resolve(__dirname, ".."),
13612
+ import_node_path19.default.resolve(import_node_path19.default.dirname(process.argv[1] ?? process.cwd()), ".."),
13613
+ import_node_path19.default.resolve(process.cwd(), "packages/cli")
14929
13614
  ];
14930
13615
  for (const candidate of candidates) {
14931
- if (import_node_fs20.default.existsSync(import_node_path18.default.join(candidate, "AGENTS.md"))) return candidate;
13616
+ if (import_node_fs21.default.existsSync(import_node_path19.default.join(candidate, "AGENTS.md"))) return candidate;
14932
13617
  }
14933
13618
  throw new Error(
14934
13619
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -14937,24 +13622,24 @@ function packageRoot() {
14937
13622
  function skillPaths() {
14938
13623
  const root = packageRoot();
14939
13624
  return {
14940
- agents: import_node_path18.default.join(root, "AGENTS.md"),
14941
- skill: import_node_path18.default.join(root, "skills", "apex-cli", "SKILL.md")
13625
+ agents: import_node_path19.default.join(root, "AGENTS.md"),
13626
+ skill: import_node_path19.default.join(root, "skills", "apex-cli", "SKILL.md")
14942
13627
  };
14943
13628
  }
14944
13629
  function installSkill(paths, targetRoot) {
14945
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs20.default.existsSync(filePath));
13630
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs21.default.existsSync(filePath));
14946
13631
  if (missing.length > 0) {
14947
13632
  throw new Error(
14948
13633
  `Apex CLI package files not found. Missing: ${missing.join(", ")}`
14949
13634
  );
14950
13635
  }
14951
- const target = import_node_path18.default.resolve(targetRoot, "apex-cli");
14952
- import_node_fs20.default.mkdirSync(target, { recursive: true });
14953
- import_node_fs20.default.copyFileSync(paths.skill, import_node_path18.default.join(target, "SKILL.md"));
14954
- import_node_fs20.default.copyFileSync(paths.agents, import_node_path18.default.join(target, "AGENTS.md"));
13636
+ const target = import_node_path19.default.resolve(targetRoot, "apex-cli");
13637
+ import_node_fs21.default.mkdirSync(target, { recursive: true });
13638
+ import_node_fs21.default.copyFileSync(paths.skill, import_node_path19.default.join(target, "SKILL.md"));
13639
+ import_node_fs21.default.copyFileSync(paths.agents, import_node_path19.default.join(target, "AGENTS.md"));
14955
13640
  return target;
14956
13641
  }
14957
- var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action63(opts) {
13642
+ var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action62(opts) {
14958
13643
  let paths;
14959
13644
  try {
14960
13645
  paths = skillPaths();
@@ -15169,8 +13854,8 @@ async function fetchText(url) {
15169
13854
  const res = await fetch(url, { headers: { "user-agent": DESKTOP_UA2, accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8" }, redirect: "follow", signal: AbortSignal.timeout(12e3) });
15170
13855
  return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
15171
13856
  }
15172
- function abs(base, path20) {
15173
- return new URL(path20, base).toString();
13857
+ function abs(base, path21) {
13858
+ return new URL(path21, base).toString();
15174
13859
  }
15175
13860
  function looksLikeShopifyCart(text) {
15176
13861
  try {
@@ -15190,9 +13875,9 @@ async function collectSignals(url) {
15190
13875
  const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
15191
13876
  const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
15192
13877
  if (!protectedHit) {
15193
- for (const path20 of ["/cart.js"]) {
13878
+ for (const path21 of ["/cart.js"]) {
15194
13879
  try {
15195
- const secondary = await fetchText(abs(primary.finalUrl || url, path20));
13880
+ const secondary = await fetchText(abs(primary.finalUrl || url, path21));
15196
13881
  if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
15197
13882
  chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
15198
13883
  }
@@ -15267,7 +13952,7 @@ var prospectCommand = new Command("prospect").description("Pre-sales storefront
15267
13952
 
15268
13953
  // src/commands/webhooks.ts
15269
13954
  init_source();
15270
- var SIGNATURE_DOC = "https://app.drip-apex.com/docs/api/webhooks#signatures";
13955
+ var SIGNATURE_DOC = "https://app.drip-apex.com/docs/developers/webhooks#signatures";
15271
13956
  function requireShopSecretKey(_command) {
15272
13957
  requireCredentials();
15273
13958
  }
@@ -15325,11 +14010,11 @@ function printDeliveries(deliveries2, webhook) {
15325
14010
  function selectedEvents(opts) {
15326
14011
  return [...new Set((opts.event ?? []).map((event) => event.trim()).filter(Boolean))];
15327
14012
  }
15328
- var list14 = new Command("list").alias("ls").description("List webhooks. JSON includes availableEvents; secrets are never returned after create.").action(async function action64() {
14013
+ var list14 = new Command("list").alias("ls").description("List webhooks. JSON includes availableEvents; secrets are never returned after create.").action(async function action63() {
15329
14014
  const data = await loadWebhookIndex(this);
15330
14015
  output(this, data, () => printWebhooks(data.data));
15331
14016
  });
15332
- var create7 = new Command("create").description(`Create a webhook. Prints the signing secret once. Signature docs: ${SIGNATURE_DOC}`).requiredOption("--url <url>", "HTTPS callback URL").option("--event <event>", "Webhook event to subscribe to; repeat for multiple events", (value, previous = []) => [...previous, value], []).action(async function action65(opts) {
14017
+ var create7 = new Command("create").description(`Create a webhook. Prints the signing secret once. Signature docs: ${SIGNATURE_DOC}`).requiredOption("--url <url>", "HTTPS callback URL").option("--event <event>", "Webhook event to subscribe to; repeat for multiple events", (value, previous = []) => [...previous, value], []).action(async function action64(opts) {
15333
14018
  const events = selectedEvents(opts);
15334
14019
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
15335
14020
  const index = await loadWebhookIndex(this);
@@ -15346,19 +14031,19 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
15346
14031
  console.log(`Signature verification: ${SIGNATURE_DOC}`);
15347
14032
  });
15348
14033
  });
15349
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action66(id) {
14034
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action65(id) {
15350
14035
  requireShopSecretKey(this);
15351
14036
  const result = handleResult(this, await deleteWebhook(requireCredentials(), id), "Failed to delete webhook");
15352
14037
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
15353
14038
  });
15354
- var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action67(id, opts) {
14039
+ var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action66(id, opts) {
15355
14040
  const limit = Number.parseInt(opts.limit, 10);
15356
14041
  if (!Number.isInteger(limit) || limit < 1) fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
15357
14042
  requireShopSecretKey(this);
15358
14043
  const result = handleResult(this, await getWebhookDeliveries(requireCredentials(), id, limit), "Failed to load deliveries");
15359
14044
  output(this, result, () => printDeliveries(result.data, result.webhook));
15360
14045
  });
15361
- var update4 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option("--event <event>", "Replacement event; repeat for multiple events", (value, previous = []) => [...previous, value], []).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action68(id, opts) {
14046
+ var update4 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option("--event <event>", "Replacement event; repeat for multiple events", (value, previous = []) => [...previous, value], []).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action67(id, opts) {
15362
14047
  if (opts.active && opts.inactive) fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
15363
14048
  const events = selectedEvents(opts);
15364
14049
  const body5 = {};
@@ -15378,7 +14063,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
15378
14063
  const result = handleResult(this, await updateWebhook(requireCredentials(), id, body5), "Failed to update webhook");
15379
14064
  output(this, { webhook: result }, () => printWebhooks([result]));
15380
14065
  });
15381
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action69(id) {
14066
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action68(id) {
15382
14067
  requireShopSecretKey(this);
15383
14068
  const result = handleResult(this, await testWebhook(requireCredentials(), id), "Failed to send test webhook");
15384
14069
  output(this, result, () => console.log(source_default.green(`Sent test delivery ${result.delivery.id}: ${result.delivery.status}`)));
@@ -15386,8 +14071,8 @@ var test3 = new Command("test").description("Send a signed test event to a webho
15386
14071
  var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update4).addCommand(remove4).addCommand(deliveries).addCommand(test3);
15387
14072
 
15388
14073
  // src/commands/config-resources.ts
15389
- var import_node_fs21 = __toESM(require("node:fs"));
15390
- var import_node_path19 = __toESM(require("node:path"));
14074
+ var import_node_fs22 = __toESM(require("node:fs"));
14075
+ var import_node_path20 = __toESM(require("node:path"));
15391
14076
  function show(resource) {
15392
14077
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
15393
14078
  `);
@@ -15472,14 +14157,14 @@ function readFamily(family, label) {
15472
14157
  }
15473
14158
  var assetReads = readFamily("assets", "assets");
15474
14159
  var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
15475
- const absolute = import_node_path19.default.resolve(process.cwd(), file);
15476
- if (!import_node_fs21.default.existsSync(absolute))
14160
+ const absolute = import_node_path20.default.resolve(process.cwd(), file);
14161
+ if (!import_node_fs22.default.existsSync(absolute))
15477
14162
  fail(this, `File not found: ${file}`, 3);
15478
- const fileStat = import_node_fs21.default.statSync(absolute);
14163
+ const fileStat = import_node_fs22.default.statSync(absolute);
15479
14164
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
15480
14165
  if (fileStat.size > MAX_ASSET_SIZE_BYTES)
15481
14166
  fail(this, `${file} is larger than 10 MB`, 3);
15482
- const mimeType = ASSET_MIME_TYPES[import_node_path19.default.extname(absolute).toLowerCase()];
14167
+ const mimeType = ASSET_MIME_TYPES[import_node_path20.default.extname(absolute).toLowerCase()];
15483
14168
  if (!mimeType)
15484
14169
  fail(
15485
14170
  this,
@@ -15488,8 +14173,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
15488
14173
  );
15489
14174
  const r = await uploadAssets(requireCredentials(), [
15490
14175
  {
15491
- name: import_node_path19.default.basename(absolute),
15492
- bytes: import_node_fs21.default.readFileSync(absolute),
14176
+ name: import_node_path20.default.basename(absolute),
14177
+ bytes: import_node_fs22.default.readFileSync(absolute),
15493
14178
  type: mimeType
15494
14179
  }
15495
14180
  ]);
@@ -15543,6 +14228,19 @@ var snippet = new Command("snippet").description("Generate an external purchase
15543
14228
  var externalPixelsCommand = new Command("external-pixels").description("Manage external pixels").addCommand(snippet);
15544
14229
 
15545
14230
  // src/cli.ts
14231
+ var mockApiEnvironmentKey = ["APEX", "CLI", "MOCK", "API"].join("_");
14232
+ if (process.env[mockApiEnvironmentKey] === "1" && !cliTestHarnessActive()) {
14233
+ const message = "mock harness is not part of the published CLI; use the test artifact";
14234
+ const guardTerminatorIndex = process.argv.indexOf("--");
14235
+ const guardArgv = guardTerminatorIndex === -1 ? process.argv : process.argv.slice(0, guardTerminatorIndex);
14236
+ if (guardArgv.includes("--json") || !process.stdout.isTTY) {
14237
+ writeErrorEnvelope(new CliError(message, EXIT_CODES.general));
14238
+ } else {
14239
+ process.stderr.write(`${message}
14240
+ `);
14241
+ }
14242
+ process.exit(EXIT_CODES.general);
14243
+ }
15546
14244
  function addJsonOption(command) {
15547
14245
  if (!command.options.some((option) => option.long === "--json")) {
15548
14246
  command.option("--json", "Print JSON output");