@drip-apex/apex-cli 0.4.0 → 0.6.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 +691 -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.6.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,42 @@ 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.screenshot) url.searchParams.set("drip_screenshot", "1");
8630
+ if (params.qaSessionId) url.searchParams.set("drip_qa_session", params.qaSessionId);
8631
+ if (params.harnessToken) url.searchParams.set("drip_harness", params.harnessToken);
8632
+ return url;
8633
+ }
8634
+ function buildQaPreviewUrl(experiment, options = {}) {
8635
+ const variation = options.variationName ? experiment.variations?.find((v) => v.name === options.variationName) : experiment.variations?.find((v) => !v.is_control) ?? experiment.variations?.[0];
8636
+ const target = options.storeUrl ?? experiment.store_url;
8637
+ if (!target || !variation) return null;
8638
+ try {
8639
+ return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, ...options });
8640
+ } catch {
8641
+ return null;
8642
+ }
8643
+ }
8644
+
8645
+ // src/lib/qa-preview-url.ts
8646
+ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
8647
+ const url = buildQaPreviewUrl({ id: experimentId, store_url: storeUrl, variations: [{ id: variationId, is_control: false }] });
8648
+ if (!url) throw new TypeError("Invalid QA preview URL");
8649
+ return url;
8650
+ }
8651
+
8652
+ // src/commands/experiments.ts
9968
8653
  function formatDate(iso) {
9969
8654
  return new Date(iso).toLocaleDateString("en-US", {
9970
8655
  month: "short",
@@ -10070,25 +8755,25 @@ var FILESYSTEM_FILE_RE = new RegExp(
10070
8755
  `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
10071
8756
  );
10072
8757
  function sha256Text(value) {
10073
- return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
8758
+ return import_node_crypto2.default.createHash("sha256").update(value).digest("hex");
10074
8759
  }
10075
8760
  function folderPath(folder) {
10076
- return import_node_path3.default.resolve(process.cwd(), folder);
8761
+ return import_node_path4.default.resolve(process.cwd(), folder);
10077
8762
  }
10078
8763
  function filesystemApexDir(folder) {
10079
- return import_node_path3.default.join(folder, ".apex");
8764
+ return import_node_path4.default.join(folder, ".apex");
10080
8765
  }
10081
8766
  function filesystemSessionPath(folder) {
10082
- return import_node_path3.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
8767
+ return import_node_path4.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
10083
8768
  }
10084
8769
  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"),
8770
+ import_node_fs6.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
8771
+ import_node_fs6.default.writeFileSync(
8772
+ import_node_path4.default.join(filesystemApexDir(folder), ".gitignore"),
10088
8773
  "experiment-filesystem.json\n",
10089
8774
  "utf8"
10090
8775
  );
10091
- import_node_fs5.default.writeFileSync(
8776
+ import_node_fs6.default.writeFileSync(
10092
8777
  filesystemSessionPath(folder),
10093
8778
  `${JSON.stringify(session, null, 2)}
10094
8779
  `,
@@ -10097,14 +8782,14 @@ function writeFilesystemSession(folder, session) {
10097
8782
  }
10098
8783
  function readFilesystemSession(folder) {
10099
8784
  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)) {
8785
+ if (!import_node_fs6.default.existsSync(filePath)) {
8786
+ const workspaceMarker = import_node_path4.default.join(filesystemApexDir(folder), "session.json");
8787
+ if (import_node_fs6.default.existsSync(workspaceMarker)) {
10103
8788
  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
8789
  }
10105
8790
  throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
10106
8791
  }
10107
- return JSON.parse(import_node_fs5.default.readFileSync(filePath, "utf8"));
8792
+ return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
10108
8793
  }
10109
8794
  function validateFilesystemPath(relativePath) {
10110
8795
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -10120,38 +8805,38 @@ function writeExperimentFilesystemFiles(folder, files) {
10120
8805
  }));
10121
8806
  for (const file of validatedFiles) {
10122
8807
  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");
8808
+ const absPath = import_node_path4.default.join(folder, relativePath);
8809
+ import_node_fs6.default.mkdirSync(import_node_path4.default.dirname(absPath), { recursive: true });
8810
+ import_node_fs6.default.writeFileSync(absPath, file.content, "utf8");
10126
8811
  }
10127
8812
  }
10128
8813
  function listExperimentFilesystemFiles(folder) {
10129
8814
  const files = [];
10130
8815
  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");
8816
+ const absPath = import_node_path4.default.join(folder, fileName);
8817
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
8818
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10134
8819
  files.push({ path: fileName, content, sha256: sha256Text(content) });
10135
8820
  }
10136
8821
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
10137
8822
  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");
8823
+ const absPath = import_node_path4.default.join(folder, relativePath);
8824
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
8825
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10141
8826
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
10142
8827
  }
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 })) {
8828
+ const variationsRoot = import_node_path4.default.join(folder, "variations");
8829
+ if (import_node_fs6.default.existsSync(variationsRoot)) {
8830
+ for (const variationDir of import_node_fs6.default.readdirSync(variationsRoot, { withFileTypes: true })) {
10146
8831
  if (!variationDir.isDirectory()) continue;
10147
- const base = import_node_path3.default.join(variationsRoot, variationDir.name);
8832
+ const base = import_node_path4.default.join(variationsRoot, variationDir.name);
10148
8833
  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;
8834
+ const absPath = import_node_path4.default.join(base, fileName);
8835
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
10151
8836
  const relativePath = validateFilesystemPath(
10152
- import_node_path3.default.relative(folder, absPath)
8837
+ import_node_path4.default.relative(folder, absPath)
10153
8838
  );
10154
- const content = import_node_fs5.default.readFileSync(absPath, "utf8");
8839
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
10155
8840
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
10156
8841
  }
10157
8842
  }
@@ -10193,6 +8878,39 @@ function blockingIssueSummary(issues) {
10193
8878
  const remaining = issues.length - 1;
10194
8879
  return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
10195
8880
  }
8881
+ async function getQaPreviewUrls(creds, experimentId) {
8882
+ try {
8883
+ const res = await getExperiment(creds, experimentId);
8884
+ const storeUrl = res.data?.store_url;
8885
+ if (!res.ok || !res.data || !storeUrl) {
8886
+ return { previewUrls: [], unavailable: true };
8887
+ }
8888
+ const experiment = res.data;
8889
+ const previewUrls = (experiment.variations ?? []).filter((variation) => !variation.is_control).map((variation) => ({
8890
+ variationId: variation.id,
8891
+ variationName: variation.name,
8892
+ url: buildQaPreviewUrl2(
8893
+ storeUrl,
8894
+ experiment.id,
8895
+ variation.id
8896
+ ).toString()
8897
+ }));
8898
+ return { previewUrls, unavailable: false };
8899
+ } catch {
8900
+ return { previewUrls: [], unavailable: true };
8901
+ }
8902
+ }
8903
+ function printQaPreviewUrls(experimentId, result) {
8904
+ if (result.unavailable) {
8905
+ console.log(source_default.dim(`Preview: apex experiments get ${experimentId} / drip preview ${experimentId}`));
8906
+ return;
8907
+ }
8908
+ for (const preview of result.previewUrls) {
8909
+ console.log(
8910
+ `Preview (interactive, works while draft \u2014 no on-site editing needed): ${preview.url}`
8911
+ );
8912
+ }
8913
+ }
10196
8914
  function printQaRun(run3) {
10197
8915
  console.log();
10198
8916
  console.log(source_default.bold(`QA run ${run3.id}`));
@@ -10236,7 +8954,7 @@ async function confirmApply(message) {
10236
8954
  function parseEvidenceInput(value) {
10237
8955
  if (!value) return void 0;
10238
8956
  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;
8957
+ 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
8958
  const parsed = JSON.parse(text);
10241
8959
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10242
8960
  throw new Error("QA evidence must be a JSON object");
@@ -10260,7 +8978,7 @@ async function resolveExperimentForFilesystemOrId(value) {
10260
8978
  return {
10261
8979
  experimentId: session.experimentId,
10262
8980
  folder: candidate,
10263
- label: import_node_path3.default.basename(candidate)
8981
+ label: import_node_path4.default.basename(candidate)
10264
8982
  };
10265
8983
  }
10266
8984
  const exp = await resolveExperiment(value);
@@ -10306,7 +9024,7 @@ async function resolveExperiment(idOrSlug) {
10306
9024
  res.errorDetails
10307
9025
  );
10308
9026
  }
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) {
9027
+ 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
9028
  const creds = requireCredentials();
10311
9029
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10312
9030
  const spinner = process.stderr.isTTY ? ora2("Fetching experiments...").start() : null;
@@ -10506,25 +9224,7 @@ var create = new Command("create").description("Create a new experiment").requir
10506
9224
  spinner?.succeed("Experiment created");
10507
9225
  output(this, { experiment: res.data }, () => printExperiment(res.data));
10508
9226
  });
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) {
9227
+ var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
10528
9228
  const creds = requireCredentials();
10529
9229
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10530
9230
  const spinner = ora2("Pausing experiment...").start();
@@ -10542,7 +9242,7 @@ var pause = new Command("pause").description("Pause a running experiment").argum
10542
9242
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
10543
9243
  });
10544
9244
  });
10545
- var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action9(idOrSlug) {
9245
+ var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
10546
9246
  const creds = requireCredentials();
10547
9247
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10548
9248
  const spinner = ora2("Completing experiment...").start();
@@ -10560,7 +9260,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
10560
9260
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
10561
9261
  });
10562
9262
  });
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) {
9263
+ 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
9264
  const creds = requireCredentials();
10565
9265
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10566
9266
  const exp = await resolveExperiment(idOrSlug);
@@ -10602,7 +9302,7 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
10602
9302
  }
10603
9303
  output(this, { experiment: { id: exp.id, name: exp.name }, deleted: true }, () => spinner.succeed(`Experiment "${exp.name}" deleted.`));
10604
9304
  });
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) {
9305
+ 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
9306
  const creds = requireCredentials();
10607
9307
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10608
9308
  const spinner = ora2("Pulling experiment filesystem...").start();
@@ -10622,7 +9322,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
10622
9322
  res.data.files.map((file) => [file.path, { sha256: file.sha256 }])
10623
9323
  )
10624
9324
  });
10625
- output(this, { folder, experimentId: exp.id, files: res.data.files.map((file) => import_node_path3.default.join(folder, file.path)) }, () => {
9325
+ output(this, { folder, experimentId: exp.id, files: res.data.files.map((file) => import_node_path4.default.join(folder, file.path)) }, () => {
10626
9326
  spinner.succeed(`Pulled ${exp.name}`);
10627
9327
  console.log(source_default.cyan(folder));
10628
9328
  });
@@ -10631,7 +9331,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
10631
9331
  throw error;
10632
9332
  }
10633
9333
  });
10634
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action12(folderArg) {
9334
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action11(folderArg) {
10635
9335
  const creds = requireCredentials();
10636
9336
  try {
10637
9337
  const folder = folderPath(folderArg);
@@ -10654,7 +9354,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
10654
9354
  fail(this, error instanceof Error ? error.message : String(error));
10655
9355
  }
10656
9356
  });
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) {
9357
+ 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
9358
  const creds = requireCredentials();
10659
9359
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10660
9360
  try {
@@ -10711,15 +9411,18 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10711
9411
  folder,
10712
9412
  session.experimentId
10713
9413
  );
10714
- output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data }, () => {
9414
+ const previewResult = await getQaPreviewUrls(creds, session.experimentId);
9415
+ const { previewUrls } = previewResult;
9416
+ output(this, { folder, experimentId: session.experimentId, applied: true, plan: planRes.data, result: res.data, previewUrls }, () => {
10715
9417
  printFilesystemPlan(planRes.data);
9418
+ printQaPreviewUrls(session.experimentId, previewResult);
10716
9419
  });
10717
9420
  } catch (error) {
10718
9421
  if (error instanceof CliError) throw error;
10719
9422
  fail(this, error instanceof Error ? error.message : String(error));
10720
9423
  }
10721
9424
  });
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) {
9425
+ 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
9426
  const creds = requireCredentials();
10724
9427
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10725
9428
  const spinner = ora2("Fetching QA run...").start();
@@ -10740,7 +9443,7 @@ var qaStatus = new Command("status").description("Show or create the latest expe
10740
9443
  }
10741
9444
  });
10742
9445
  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) {
9446
+ async function action14(target, opts) {
10744
9447
  const creds = requireCredentials();
10745
9448
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10746
9449
  const spinner = ora2("Updating QA checklist...").start();
@@ -10779,7 +9482,7 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
10779
9482
  }
10780
9483
  );
10781
9484
  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) {
9485
+ async function action15(target, opts) {
10783
9486
  const creds = requireCredentials();
10784
9487
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10785
9488
  const spinner = ora2("Updating QA notes...").start();
@@ -10814,7 +9517,7 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10814
9517
  }
10815
9518
  }
10816
9519
  );
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) {
9520
+ 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
9521
  const creds = requireCredentials();
10819
9522
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10820
9523
  const spinner = ora2("Approving QA...").start();
@@ -10833,7 +9536,7 @@ var qaApprove = new Command("approve").description("Mark QA done after the check
10833
9536
  throw error;
10834
9537
  }
10835
9538
  });
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) {
9539
+ 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
9540
  const creds = requireCredentials();
10838
9541
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
10839
9542
  const spinner = ora2("Reopening QA...").start();
@@ -10856,7 +9559,7 @@ var qa = new Command("qa").description("Manage experiment QA runs from the CLI")
10856
9559
  var SCREENSHOT_DEVICES = ["desktop_chrome", "iphone_15_pro", "galaxy_s24", "ipad_pro", "desktop_safari"];
10857
9560
  var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
10858
9561
  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) {
9562
+ 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
9563
  if (opts.url) fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
10861
9564
  const timeoutSeconds = Number(opts.timeout);
10862
9565
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
@@ -10889,25 +9592,25 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10889
9592
  const created = await createExperimentScreenshots(creds, experiment.id, { devices, variation_ids: variationIds });
10890
9593
  if (!created.ok || !created.data) fail(this, created.error ?? "Failed to create screenshot job", classifyStatus(created.status), created.errorDetails);
10891
9594
  let job = created.data;
10892
- const deadline = Date.now() + timeoutSeconds * 1e3;
10893
- let delayMs = process.env.APEX_CLI_MOCK_API === "1" ? 1 : 1e3;
9595
+ const deadline = screenshotNow() + timeoutSeconds * 1e3;
9596
+ let delayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
10894
9597
  let consecutiveTransientFailures = 0;
10895
9598
  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.`);
9599
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9600
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - screenshotNow()))));
9601
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10899
9602
  let polled;
10900
9603
  try {
10901
- polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - Date.now()));
9604
+ polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - screenshotNow()));
10902
9605
  } catch (error) {
10903
- if (Date.now() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9606
+ if (screenshotNow() >= deadline) fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10904
9607
  consecutiveTransientFailures += 1;
10905
9608
  if (consecutiveTransientFailures > 3) throw error;
10906
9609
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10907
9610
  continue;
10908
9611
  }
10909
9612
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
10910
- if (!polledToTerminalState && Date.now() >= deadline) {
9613
+ if (!polledToTerminalState && screenshotNow() >= deadline) {
10911
9614
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10912
9615
  }
10913
9616
  if (!polled.ok || !polled.data) {
@@ -10923,8 +9626,8 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10923
9626
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10924
9627
  }
10925
9628
  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 });
9629
+ const outputDir = import_node_path4.default.resolve(process.cwd(), opts.out);
9630
+ import_node_fs6.default.mkdirSync(outputDir, { recursive: true });
10928
9631
  const rows = [];
10929
9632
  const errors = [];
10930
9633
  const usedFilenames = /* @__PURE__ */ new Set();
@@ -10961,7 +9664,7 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10961
9664
  try {
10962
9665
  const response = await fetch(url);
10963
9666
  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()));
9667
+ await import_node_fs6.default.promises.writeFile(import_node_path4.default.join(outputDir, filename), Buffer.from(await response.arrayBuffer()));
10965
9668
  workerResults.push({ index, row: [variation.name, device, filename] });
10966
9669
  } catch (error) {
10967
9670
  workerResults.push({ index, error: `${variation.name}/${device}: ${error instanceof Error ? error.message : String(error)}` });
@@ -10982,7 +9685,7 @@ var screenshots = new Command("screenshots").description("Force-assign every cap
10982
9685
  printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
10983
9686
  });
10984
9687
  });
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", `
9688
+ 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
9689
  Examples:
10987
9690
  apex experiments list --status running
10988
9691
  apex experiments get checkout-banner
@@ -10992,9 +9695,9 @@ Examples:
10992
9695
 
10993
9696
  // src/commands/push.ts
10994
9697
  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"));
9698
+ var import_node_fs7 = __toESM(require("node:fs"));
9699
+ var import_node_os6 = __toESM(require("node:os"));
9700
+ var import_node_path5 = __toESM(require("node:path"));
10998
9701
  var import_node_url2 = require("node:url");
10999
9702
  function toApiBody(exp) {
11000
9703
  return {
@@ -11018,7 +9721,7 @@ function toApiBody(exp) {
11018
9721
  };
11019
9722
  }
11020
9723
  async function importConfigModule(absPath) {
11021
- const ext = import_node_path4.default.extname(absPath).toLowerCase();
9724
+ const ext = import_node_path5.default.extname(absPath).toLowerCase();
11022
9725
  if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
11023
9726
  return await import((0, import_node_url2.pathToFileURL)(absPath).href);
11024
9727
  }
@@ -11032,8 +9735,8 @@ async function importConfigModule(absPath) {
11032
9735
  ` + (err instanceof Error ? err.message : String(err))
11033
9736
  );
11034
9737
  }
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");
9738
+ const tmpDir = import_node_fs7.default.mkdtempSync(import_node_path5.default.join(import_node_os6.default.tmpdir(), "drip-config-"));
9739
+ const outFile = import_node_path5.default.join(tmpDir, "config.mjs");
11037
9740
  try {
11038
9741
  await esbuild.build({
11039
9742
  entryPoints: [absPath],
@@ -11049,7 +9752,7 @@ async function importConfigModule(absPath) {
11049
9752
  return await import(href);
11050
9753
  } finally {
11051
9754
  try {
11052
- import_node_fs6.default.rmSync(tmpDir, { recursive: true, force: true });
9755
+ import_node_fs7.default.rmSync(tmpDir, { recursive: true, force: true });
11053
9756
  } catch {
11054
9757
  }
11055
9758
  }
@@ -11063,7 +9766,7 @@ function isRecord(value) {
11063
9766
  }
11064
9767
  var pushCommand = new Command("push").description(
11065
9768
  "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) {
9769
+ ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action19(opts) {
11067
9770
  const creds = requireCredentials();
11068
9771
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
11069
9772
  const configPath = opts.config ?? findConfigPath();
@@ -11077,7 +9780,7 @@ var pushCommand = new Command("push").description(
11077
9780
  const spinner = process.stderr.isTTY ? ora2(`Loading config from ${source_default.cyan(configPath)}...`).start() : null;
11078
9781
  let experiments;
11079
9782
  try {
11080
- const absPath = import_node_path4.default.resolve(configPath);
9783
+ const absPath = import_node_path5.default.resolve(configPath);
11081
9784
  const mod = await importConfigModule(absPath);
11082
9785
  const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
11083
9786
  if (Array.isArray(exported)) {
@@ -11181,17 +9884,22 @@ var pushCommand = new Command("push").description(
11181
9884
 
11182
9885
  // src/commands/preview.ts
11183
9886
  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) => {
9887
+ 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
9888
  const creds = requireCredentials();
11186
9889
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
11187
9890
  if (opts?.proxy) {
9891
+ const ttlHours = opts.ttl === void 0 ? void 0 : Number(opts.ttl);
9892
+ if (ttlHours !== void 0 && (!Number.isInteger(ttlHours) || ttlHours < 1 || ttlHours > 168)) {
9893
+ fail(previewCommand, "--ttl must be an integer between 1 and 168 hours");
9894
+ }
11188
9895
  const identifier = idOrSlug.startsWith("slug:") ? idOrSlug.slice(5) : idOrSlug;
11189
9896
  const proxySpinner = ora2("Creating proxy preview session...").start();
11190
9897
  const created = await createProxyPreviewSession(
11191
9898
  creds,
11192
9899
  identifier,
11193
9900
  variationId,
11194
- opts.storeUrl
9901
+ opts.storeUrl,
9902
+ ttlHours
11195
9903
  );
11196
9904
  proxySpinner.stop();
11197
9905
  if (!created.ok || !created.data) {
@@ -11202,7 +9910,15 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11202
9910
  );
11203
9911
  }
11204
9912
  const data = created.data;
11205
- output(previewCommand, data, () => console.log(source_default.cyan(data.preview_url)));
9913
+ output(previewCommand, data, () => {
9914
+ console.log(source_default.cyan(data.preview_url));
9915
+ const expiresAt = new Date(data.expires_at);
9916
+ const remainingMs = expiresAt.getTime() - Date.now();
9917
+ const value = remainingMs < 864e5 ? Math.max(1, Math.round(remainingMs / 36e5)) : Math.round(remainingMs / 864e5);
9918
+ const unit = remainingMs < 864e5 ? "hour" : "day";
9919
+ const duration = `${value} ${unit}${value === 1 ? "" : "s"}`;
9920
+ console.log(`Valid until ${expiresAt.toISOString()} (${duration}). Re-run this command for a fresh link.`);
9921
+ });
11206
9922
  if (opts.open !== false && !shouldUseJson(previewCommand)) {
11207
9923
  try {
11208
9924
  const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
@@ -11250,21 +9966,16 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11250
9966
  variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
11251
9967
  }
11252
9968
  spinner.stop();
11253
- const storeUrl = exp.store_url ?? opts?.storeUrl;
9969
+ const storeUrl = opts?.storeUrl ?? exp.store_url;
11254
9970
  if (!storeUrl) {
11255
9971
  fail(previewCommand, 'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.');
11256
9972
  }
11257
9973
  let previewUrl;
11258
9974
  try {
11259
- previewUrl = new URL(storeUrl);
9975
+ previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id);
11260
9976
  } catch {
11261
9977
  fail(previewCommand, 'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".');
11262
9978
  }
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
9979
  output(previewCommand, { experimentId: exp.id, variationId: variation.id, previewUrl: previewUrl.toString() }, () => {
11269
9980
  console.log();
11270
9981
  console.log(source_default.bold(`Preview: ${exp.name}`));
@@ -11286,25 +9997,25 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
11286
9997
 
11287
9998
  // src/commands/harness.ts
11288
9999
  init_source();
11289
- var import_node_fs7 = __toESM(require("node:fs"));
11290
- var import_node_path5 = __toESM(require("node:path"));
10000
+ var import_node_fs8 = __toESM(require("node:fs"));
10001
+ var import_node_path6 = __toESM(require("node:path"));
11291
10002
  var import_node_child_process6 = require("node:child_process");
11292
10003
  function slugify(value, fallback2 = "variation") {
11293
10004
  const slug = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
11294
10005
  return slug || fallback2;
11295
10006
  }
11296
10007
  function readJson(filePath) {
11297
- if (!import_node_fs7.default.existsSync(filePath)) return null;
11298
- return JSON.parse(import_node_fs7.default.readFileSync(filePath, "utf8"));
10008
+ if (!import_node_fs8.default.existsSync(filePath)) return null;
10009
+ return JSON.parse(import_node_fs8.default.readFileSync(filePath, "utf8"));
11299
10010
  }
11300
10011
  function resolveFolder(folder) {
11301
- return import_node_path5.default.resolve(process.cwd(), folder);
10012
+ return import_node_path6.default.resolve(process.cwd(), folder);
11302
10013
  }
11303
10014
  function experimentConfigPath(folder) {
11304
- return import_node_path5.default.join(folder, "experiment.config.json");
10015
+ return import_node_path6.default.join(folder, "experiment.config.json");
11305
10016
  }
11306
10017
  function apexConfigPath(folder) {
11307
- return import_node_path5.default.join(folder, "apex.harness.json");
10018
+ return import_node_path6.default.join(folder, "apex.harness.json");
11308
10019
  }
11309
10020
  function loadHarnessConfig(folder) {
11310
10021
  const experiment = readJson(experimentConfigPath(folder)) ?? {};
@@ -11313,8 +10024,8 @@ function loadHarnessConfig(folder) {
11313
10024
  const targetOrigin = experiment.targetOrigin?.replace(/\/+$/, "") ?? "";
11314
10025
  const defaultPath = experiment.defaultPath ?? "/";
11315
10026
  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);
10027
+ const id = experiment.id ?? import_node_path6.default.basename(import_node_path6.default.dirname(folder));
10028
+ const variation = import_node_path6.default.basename(folder);
11318
10029
  return {
11319
10030
  experiment,
11320
10031
  apex: {
@@ -11346,35 +10057,35 @@ function runAbHarness(args, cwd) {
11346
10057
  function artifactPaths(folder, experiment) {
11347
10058
  const variant = experiment.variant;
11348
10059
  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 };
10060
+ const jsPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
10061
+ const cssPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
10062
+ return { jsPath, cssPath: import_node_fs8.default.existsSync(cssPath) ? cssPath : null };
11352
10063
  }
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) : [];
10064
+ const buildDir = import_node_path6.default.join(folder, "build");
10065
+ const files = import_node_fs8.default.existsSync(buildDir) ? import_node_fs8.default.readdirSync(buildDir) : [];
11355
10066
  const js = files.find((file) => file.endsWith(".js") && !file.endsWith(".bundle.js") && !file.endsWith(".min.js"));
11356
10067
  const css = files.find((file) => file.endsWith(".css") && !file.endsWith(".min.css"));
11357
10068
  if (!js) throw new Error("Could not find harness JS artifact in build/");
11358
10069
  return {
11359
- jsPath: import_node_path5.default.join(buildDir, js),
11360
- cssPath: css ? import_node_path5.default.join(buildDir, css) : null
10070
+ jsPath: import_node_path6.default.join(buildDir, js),
10071
+ cssPath: css ? import_node_path6.default.join(buildDir, css) : null
11361
10072
  };
11362
10073
  }
11363
10074
  async function uploadVersion(params) {
11364
10075
  const creds = requireCredentials();
11365
10076
  const { jsPath, cssPath } = artifactPaths(params.folder, params.experiment);
11366
- if (!import_node_fs7.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
10077
+ if (!import_node_fs8.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
11367
10078
  const form = new FormData();
11368
10079
  form.append(
11369
10080
  "js",
11370
- new Blob([import_node_fs7.default.readFileSync(jsPath)], { type: "application/javascript" }),
11371
- import_node_path5.default.basename(jsPath)
10081
+ new Blob([import_node_fs8.default.readFileSync(jsPath)], { type: "application/javascript" }),
10082
+ import_node_path6.default.basename(jsPath)
11372
10083
  );
11373
- if (cssPath && import_node_fs7.default.existsSync(cssPath) && import_node_fs7.default.statSync(cssPath).size > 0) {
10084
+ if (cssPath && import_node_fs8.default.existsSync(cssPath) && import_node_fs8.default.statSync(cssPath).size > 0) {
11374
10085
  form.append(
11375
10086
  "css",
11376
- new Blob([import_node_fs7.default.readFileSync(cssPath)], { type: "text/css" }),
11377
- import_node_path5.default.basename(cssPath)
10087
+ new Blob([import_node_fs8.default.readFileSync(cssPath)], { type: "text/css" }),
10088
+ import_node_path6.default.basename(cssPath)
11378
10089
  );
11379
10090
  }
11380
10091
  form.append(
@@ -11383,8 +10094,8 @@ async function uploadVersion(params) {
11383
10094
  ...params.apex,
11384
10095
  resetJs: cleanupResetJs(params.experiment),
11385
10096
  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
10097
+ jsPath: import_node_path6.default.relative(params.folder, jsPath),
10098
+ cssPath: cssPath ? import_node_path6.default.relative(params.folder, cssPath) : null
11388
10099
  })
11389
10100
  );
11390
10101
  const res = await uploadHarnessVersion(creds, params.session.sessionId, form);
@@ -11410,18 +10121,18 @@ async function createSession(folder) {
11410
10121
  return { session: res.data, apex, experiment };
11411
10122
  }
11412
10123
  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) {
10124
+ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action20(folderArg, opts) {
11414
10125
  const folder = resolveFolder(folderArg);
11415
10126
  const targetPath = apexConfigPath(folder);
11416
- if (import_node_fs7.default.existsSync(targetPath) && !opts.force) {
10127
+ if (import_node_fs8.default.existsSync(targetPath) && !opts.force) {
11417
10128
  fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
11418
10129
  }
11419
10130
  const { apex } = loadHarnessConfig(folder);
11420
- import_node_fs7.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
10131
+ import_node_fs8.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
11421
10132
  `, "utf8");
11422
10133
  output(this, { folder, configPath: targetPath, experiment: apex.experimentSlug, variation: apex.variationSlug }, () => printSuccess(`Created ${targetPath}`));
11423
10134
  });
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) {
10135
+ 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
10136
  const folder = resolveFolder(folderArg);
11426
10137
  const created = await createSession(folder);
11427
10138
  await runAbHarness(["build", folder], folder);
@@ -11463,7 +10174,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11463
10174
  }
11464
10175
  };
11465
10176
  let debounce = null;
11466
- const watcher = import_node_fs7.default.watch(folder, { recursive: true }, (_event, filename) => {
10177
+ const watcher = import_node_fs8.default.watch(folder, { recursive: true }, (_event, filename) => {
11467
10178
  const name = String(filename ?? "");
11468
10179
  if (!name || name.startsWith("build/") || name.includes("/build/")) return;
11469
10180
  if (debounce) clearTimeout(debounce);
@@ -11477,7 +10188,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11477
10188
  process.exitCode = 0;
11478
10189
  });
11479
10190
  });
11480
- harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
10191
+ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action22(folderArg) {
11481
10192
  const folder = resolveFolder(folderArg);
11482
10193
  const created = await createSession(folder);
11483
10194
  await runAbHarness(["verify", folder], folder);
@@ -11490,9 +10201,9 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
11490
10201
 
11491
10202
  // src/commands/operator.ts
11492
10203
  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"));
10204
+ var import_node_crypto3 = __toESM(require("node:crypto"));
10205
+ var import_node_fs9 = __toESM(require("node:fs"));
10206
+ var import_node_path7 = __toESM(require("node:path"));
11496
10207
  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
10208
  function workspaceIssueFile(field, evidence) {
11498
10209
  if (field === "custom_js" || field === "reset_js") return "src/index.js";
@@ -11536,7 +10247,7 @@ function formatWorkspaceBlockingIssues(value) {
11536
10247
  }).join("\n");
11537
10248
  }
11538
10249
  function sha256Text2(value) {
11539
- return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
10250
+ return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
11540
10251
  }
11541
10252
  function byteLength(value) {
11542
10253
  return Buffer.byteLength(value, "utf8");
@@ -11546,28 +10257,28 @@ function slugify2(value, fallback2 = "operator") {
11546
10257
  return slug || fallback2;
11547
10258
  }
11548
10259
  function folderPath2(folder) {
11549
- return import_node_path6.default.resolve(process.cwd(), folder);
10260
+ return import_node_path7.default.resolve(process.cwd(), folder);
11550
10261
  }
11551
10262
  function apexDir(folder) {
11552
- return import_node_path6.default.join(folder, ".apex");
10263
+ return import_node_path7.default.join(folder, ".apex");
11553
10264
  }
11554
10265
  function sessionPath(folder) {
11555
- return import_node_path6.default.join(apexDir(folder), "session.json");
10266
+ return import_node_path7.default.join(apexDir(folder), "session.json");
11556
10267
  }
11557
10268
  function readJsonFile(filePath) {
11558
- if (!import_node_fs8.default.existsSync(filePath)) return null;
11559
- return JSON.parse(import_node_fs8.default.readFileSync(filePath, "utf8"));
10269
+ if (!import_node_fs9.default.existsSync(filePath)) return null;
10270
+ return JSON.parse(import_node_fs9.default.readFileSync(filePath, "utf8"));
11560
10271
  }
11561
10272
  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)}
10273
+ import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(filePath), { recursive: true });
10274
+ import_node_fs9.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
11564
10275
  `, "utf8");
11565
10276
  }
11566
10277
  function readSession(folder) {
11567
10278
  const session = readJsonFile(sessionPath(folder));
11568
10279
  if (!session) {
11569
- const filesystemMarker = import_node_path6.default.join(apexDir(folder), "experiment-filesystem.json");
11570
- if (import_node_fs8.default.existsSync(filesystemMarker)) {
10280
+ const filesystemMarker = import_node_path7.default.join(apexDir(folder), "experiment-filesystem.json");
10281
+ if (import_node_fs9.default.existsSync(filesystemMarker)) {
11571
10282
  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
10283
  }
11573
10284
  throw new Error(`Missing ${sessionPath(folder)}. Run apex workspace checkout first.`);
@@ -11575,9 +10286,9 @@ function readSession(folder) {
11575
10286
  return session;
11576
10287
  }
11577
10288
  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"),
10289
+ import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10290
+ import_node_fs9.default.writeFileSync(
10291
+ import_node_path7.default.join(apexDir(folder), ".gitignore"),
11581
10292
  "session.json\nharness-session.json\n",
11582
10293
  "utf8"
11583
10294
  );
@@ -11595,15 +10306,15 @@ function listLocalWorkspaceFiles(folder) {
11595
10306
  const roots = ["src", "goals", "segments", "network"];
11596
10307
  const topLevel = ["experiment.config.json", "metrics.json", "targeting.json", "dev-doc.md", "brand.md"];
11597
10308
  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);
10309
+ if (!import_node_fs9.default.existsSync(dir)) return;
10310
+ for (const entry of import_node_fs9.default.readdirSync(dir, { withFileTypes: true })) {
10311
+ const absPath = import_node_path7.default.join(dir, entry.name);
11601
10312
  if (entry.isDirectory()) {
11602
10313
  visit(absPath);
11603
10314
  continue;
11604
10315
  }
11605
- const relativePath = validateCanonicalPath(import_node_path6.default.relative(folder, absPath));
11606
- const content = import_node_fs8.default.readFileSync(absPath, "utf8");
10316
+ const relativePath = validateCanonicalPath(import_node_path7.default.relative(folder, absPath));
10317
+ const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11607
10318
  files.push({
11608
10319
  path: relativePath,
11609
10320
  absPath,
@@ -11613,11 +10324,11 @@ function listLocalWorkspaceFiles(folder) {
11613
10324
  });
11614
10325
  }
11615
10326
  }
11616
- for (const root of roots) visit(import_node_path6.default.join(folder, root));
10327
+ for (const root of roots) visit(import_node_path7.default.join(folder, root));
11617
10328
  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");
10329
+ const absPath = import_node_path7.default.join(folder, file);
10330
+ if (!import_node_fs9.default.existsSync(absPath)) continue;
10331
+ const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11621
10332
  files.push({
11622
10333
  path: file,
11623
10334
  absPath,
@@ -11641,7 +10352,7 @@ function validateJsonFiles(files) {
11641
10352
  }
11642
10353
  }
11643
10354
  function readExperimentConfig(folder) {
11644
- return readJsonFile(import_node_path6.default.join(folder, "experiment.config.json")) ?? {};
10355
+ return readJsonFile(import_node_path7.default.join(folder, "experiment.config.json")) ?? {};
11645
10356
  }
11646
10357
  function resolveVariation(exp, variationArg) {
11647
10358
  const variations = exp.variations ?? [];
@@ -11658,9 +10369,9 @@ function resolveVariation(exp, variationArg) {
11658
10369
  function writeWorkspaceFiles(folder, workspace) {
11659
10370
  for (const file of workspace.files) {
11660
10371
  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");
10372
+ const absPath = import_node_path7.default.join(folder, relativePath);
10373
+ import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(absPath), { recursive: true });
10374
+ import_node_fs9.default.writeFileSync(absPath, file.content, "utf8");
11664
10375
  }
11665
10376
  }
11666
10377
  function sessionFromWorkspace(input) {
@@ -11724,10 +10435,10 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
11724
10435
  form.append(
11725
10436
  "js",
11726
10437
  new Blob([js.content], { type: "application/javascript" }),
11727
- import_node_path6.default.basename(js.path)
10438
+ import_node_path7.default.basename(js.path)
11728
10439
  );
11729
10440
  if (css && css.content.trim()) {
11730
- form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path6.default.basename(css.path));
10441
+ form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path7.default.basename(css.path));
11731
10442
  }
11732
10443
  form.append("manifest", JSON.stringify(manifest));
11733
10444
  const res = await uploadHarnessVersion(creds, sessionId, form);
@@ -11809,7 +10520,7 @@ Examples:
11809
10520
  apex workspace dev ./workspace --no-open
11810
10521
  apex workspace publish ./workspace
11811
10522
  `);
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) {
10523
+ 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
10524
  try {
11814
10525
  const creds = requireCredentials();
11815
10526
  const expRes = await getExperiment(creds, experimentId);
@@ -11828,7 +10539,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11828
10539
  const folder = folderPath2(opts.out ?? opts.dir ?? slugify2(`${experiment.name}-${variation.name}`, "apex-operator"));
11829
10540
  const workspace = await getOperatorWorkspace(creds, experiment.id, variation.id);
11830
10541
  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 });
10542
+ import_node_fs9.default.mkdirSync(folder, { recursive: true });
11832
10543
  writeWorkspaceFiles(folder, workspace.data);
11833
10544
  writeSession(folder, sessionFromWorkspace({
11834
10545
  credsBaseUrl: creds.baseUrl,
@@ -11844,7 +10555,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11844
10555
  throw error;
11845
10556
  }
11846
10557
  });
11847
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
10558
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action24(folderArg) {
11848
10559
  try {
11849
10560
  const folder = folderPath2(folderArg);
11850
10561
  const session = readSession(folder);
@@ -11872,14 +10583,14 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
11872
10583
  fail(this, error instanceof Error ? error.message : String(error));
11873
10584
  }
11874
10585
  });
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) {
10586
+ 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
10587
  try {
11877
10588
  const folder = folderPath2(folderArg);
11878
10589
  const session = readSession(folder);
11879
10590
  const harnessSession = await createExactHarnessSession(folder, session);
11880
10591
  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"), {
10592
+ import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10593
+ writeJsonFile(import_node_path7.default.join(apexDir(folder), "harness-session.json"), {
11883
10594
  sessionId: harnessSession.sessionId,
11884
10595
  experimentId: harnessSession.experimentId,
11885
10596
  variationId: harnessSession.variationId,
@@ -11924,7 +10635,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11924
10635
  }
11925
10636
  };
11926
10637
  let timer = null;
11927
- const watcher = import_node_fs8.default.watch(folder, { recursive: true }, (_event, filename) => {
10638
+ const watcher = import_node_fs9.default.watch(folder, { recursive: true }, (_event, filename) => {
11928
10639
  const name = String(filename ?? "");
11929
10640
  if (!name || name.startsWith(".apex/") || name.includes("/.apex/")) return;
11930
10641
  if (timer) clearTimeout(timer);
@@ -11941,7 +10652,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11941
10652
  throw error;
11942
10653
  }
11943
10654
  });
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) {
10655
+ 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
10656
  try {
11946
10657
  const result = await runPublish(folderArg, opts);
11947
10658
  const build = result.build;
@@ -11959,8 +10670,8 @@ operatorCommand.command("publish").argument("[folder]", "Operator workspace fold
11959
10670
 
11960
10671
  // src/commands/roadmap.ts
11961
10672
  init_source();
11962
- var import_node_fs9 = __toESM(require("node:fs"));
11963
- var import_node_path7 = __toESM(require("node:path"));
10673
+ var import_node_fs10 = __toESM(require("node:fs"));
10674
+ var import_node_path8 = __toESM(require("node:path"));
11964
10675
  function truncate(value, max) {
11965
10676
  const text = typeof value === "string" ? value : value == null ? "" : String(value);
11966
10677
  if (text.length <= max) return text;
@@ -11984,7 +10695,7 @@ function briefLine(item) {
11984
10695
  ].filter(Boolean);
11985
10696
  return parts.join("\n");
11986
10697
  }
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) {
10698
+ 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
10699
  const creds = requireCredentials();
11989
10700
  const limit = parseInt(opts.limit, 10);
11990
10701
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -12028,7 +10739,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
12028
10739
  }
12029
10740
  });
12030
10741
  });
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) {
10742
+ 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
10743
  const creds = requireCredentials();
12033
10744
  const res = await getRoadmapItemHandoff(creds, id);
12034
10745
  if (!res.ok) fail(this, res.error ?? "Failed to fetch roadmap handoff", classifyStatus(res.status), res.errorDetails);
@@ -12037,9 +10748,9 @@ var handoff = new Command("handoff").description("Get the full build brief for a
12037
10748
  `;
12038
10749
  const outputData = opts.instructions ? { roadmapItemId: id, instructions: String(payload.instructions ?? "") } : payload;
12039
10750
  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");
10751
+ const target = import_node_path8.default.resolve(process.cwd(), opts.out ?? opts.output);
10752
+ import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(target), { recursive: true });
10753
+ import_node_fs10.default.writeFileSync(target, rendered, "utf8");
12043
10754
  output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
12044
10755
  return;
12045
10756
  }
@@ -12051,8 +10762,8 @@ roadmapCommand.addCommand(handoff);
12051
10762
 
12052
10763
  // src/commands/tracking.ts
12053
10764
  init_source();
12054
- var import_node_fs10 = __toESM(require("node:fs"));
12055
- var import_node_path8 = __toESM(require("node:path"));
10765
+ var import_node_fs11 = __toESM(require("node:fs"));
10766
+ var import_node_path9 = __toESM(require("node:path"));
12056
10767
  function normalizeUrl(input) {
12057
10768
  const value = input.trim();
12058
10769
  if (!value) throw new Error("URL cannot be empty");
@@ -12098,17 +10809,17 @@ function commonCookieDomain(hostnames) {
12098
10809
  return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
12099
10810
  }
12100
10811
  function defaultManifestPath() {
12101
- return import_node_path8.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
10812
+ return import_node_path9.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
12102
10813
  }
12103
10814
  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"));
10815
+ if (!import_node_fs11.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
10816
+ const parsed = JSON.parse(import_node_fs11.default.readFileSync(filePath, "utf8"));
12106
10817
  if (parsed.schemaVersion !== 1) throw new Error("Unsupported tracking manifest schema");
12107
10818
  return parsed;
12108
10819
  }
12109
10820
  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)}
10821
+ import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(filePath), { recursive: true });
10822
+ import_node_fs11.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
12112
10823
  `, "utf8");
12113
10824
  }
12114
10825
  function buildSnippet(shopId, endpoint) {
@@ -12175,7 +10886,7 @@ function buildManifest(urlArgs, opts) {
12175
10886
  };
12176
10887
  }
12177
10888
  function manifestPath(input) {
12178
- return import_node_path8.default.resolve(process.cwd(), input ?? defaultManifestPath());
10889
+ return import_node_path9.default.resolve(process.cwd(), input ?? defaultManifestPath());
12179
10890
  }
12180
10891
  function printManifestPlan(manifest) {
12181
10892
  console.log(source_default.bold("Apex tracking ingestion plan"));
@@ -12223,7 +10934,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
12223
10934
  console.log(`${index + 1}. ${check}`);
12224
10935
  });
12225
10936
  });
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) {
10937
+ 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
10938
  const creds = requireCredentials();
12228
10939
  const res = await verifyTracking(creds, {
12229
10940
  minutes: Number.parseInt(opts.minutes, 10),
@@ -12261,10 +10972,10 @@ trackingCommand.command("verify").description("Verify live tracking events for t
12261
10972
 
12262
10973
  // src/commands/goals.ts
12263
10974
  init_source();
12264
- var import_node_fs11 = __toESM(require("node:fs"));
12265
- var import_node_path9 = __toESM(require("node:path"));
10975
+ var import_node_fs12 = __toESM(require("node:fs"));
10976
+ var import_node_path10 = __toESM(require("node:path"));
12266
10977
 
12267
- // src/lib/sdk-url-match.ts
10978
+ // ../sdk/src/safe-regex.ts
12268
10979
  var MAX_PATTERN_LENGTH = 1024;
12269
10980
  var MAX_CACHE_ENTRIES = 256;
12270
10981
  var regexCache = /* @__PURE__ */ new Map();
@@ -12291,10 +11002,16 @@ function safeRegexTest(pattern, input) {
12291
11002
  }
12292
11003
  }
12293
11004
 
11005
+ // ../sdk/src/goal-eligibility.ts
11006
+ function goalIsEligible(goal, pageActiveIds, href) {
11007
+ if (goal.pageId) return pageActiveIds.has(goal.pageId);
11008
+ return !goal.urlPattern || safeRegexTest(goal.urlPattern, href);
11009
+ }
11010
+
12294
11011
  // src/lib/sdk-goal-url-match.ts
11012
+ var NO_ACTIVE_PAGES = /* @__PURE__ */ new Set();
12295
11013
  function sdkPageviewGoalMatch(urlPattern, href) {
12296
- if (!urlPattern) return true;
12297
- return safeRegexTest(urlPattern, href);
11014
+ return goalIsEligible({ urlPattern: urlPattern ?? null }, NO_ACTIVE_PAGES, href);
12298
11015
  }
12299
11016
 
12300
11017
  // src/commands/goals.ts
@@ -12411,7 +11128,7 @@ function printSuggestions(data) {
12411
11128
  function parseCapture(command, value) {
12412
11129
  if (!value) return void 0;
12413
11130
  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;
11131
+ 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
11132
  try {
12416
11133
  return JSON.parse(text);
12417
11134
  } catch (error) {
@@ -12430,6 +11147,8 @@ function printGoal(goal) {
12430
11147
  console.log(` ID: ${source_default.dim(goal.id)}`);
12431
11148
  console.log(` Type: ${goal.type}`);
12432
11149
  console.log(` Measurement: ${goal.measurement === "value" ? "value" : "conversion"}`);
11150
+ if (goal.value_property) console.log(` Value source: ${goal.value_property}`);
11151
+ if (goal.value_unit) console.log(` Value unit: ${goal.value_unit}`);
12433
11152
  if (goal.selector) console.log(` Selector: ${goal.selector}`);
12434
11153
  if (goal.text_match) console.log(` Text match: ${goal.text_match}`);
12435
11154
  if (goal.url_pattern) console.log(` URL: ${goal.url_pattern}`);
@@ -12437,7 +11156,7 @@ function printGoal(goal) {
12437
11156
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
12438
11157
  console.log();
12439
11158
  }
12440
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
11159
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action30(opts) {
12441
11160
  const creds = requireCredentials();
12442
11161
  const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
12443
11162
  if (!res.ok || !res.data) {
@@ -12459,12 +11178,14 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
12459
11178
  );
12460
11179
  });
12461
11180
  });
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) {
11181
+ 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
11182
  const creds = requireCredentials();
12464
11183
  const body5 = cleanBody({
12465
11184
  name: opts.name,
12466
11185
  type: opts.type,
12467
11186
  measurement: opts.measurement,
11187
+ value_property: opts.valueProperty,
11188
+ value_unit: opts.valueUnit,
12468
11189
  selector: opts.selector,
12469
11190
  text_match: opts.textMatch,
12470
11191
  url_pattern: opts.urlPattern,
@@ -12478,12 +11199,14 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
12478
11199
  }
12479
11200
  output(this, { goal: res.data }, () => printGoal(res.data));
12480
11201
  });
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) {
11202
+ 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
11203
  const creds = requireCredentials();
12483
11204
  const body5 = cleanBody({
12484
11205
  name: opts.name,
12485
11206
  type: opts.type,
12486
11207
  measurement: opts.measurement,
11208
+ value_property: opts.valueProperty === "null" ? null : opts.valueProperty,
11209
+ value_unit: opts.valueUnit === "null" ? null : opts.valueUnit,
12487
11210
  selector: opts.selector,
12488
11211
  text_match: opts.textMatch,
12489
11212
  url_pattern: opts.urlPattern,
@@ -12500,7 +11223,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
12500
11223
  }
12501
11224
  output(this, { goal: res.data }, () => printGoal(res.data));
12502
11225
  });
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) {
11226
+ 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
11227
  if (!opts.pattern && !opts.goal) fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
12505
11228
  if (opts.pattern && opts.goal) fail(this, "Use only one of --pattern or --goal.", EXIT_CODES.validation);
12506
11229
  const creds = requireCredentials();
@@ -12545,7 +11268,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
12545
11268
  printTable(["URL", "PAGEVIEWS", "VISITORS"], matches.map((item) => [`${item.host}${item.path}`.slice(0, 64), String(item.pageviews), String(item.visitors)]), [64, 10, 10]);
12546
11269
  });
12547
11270
  });
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) {
11271
+ 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
11272
  const creds = requireCredentials();
12550
11273
  const res = await suggestGoals(creds);
12551
11274
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to suggest goals", classifyStatus(res.status), res.errorDetails);
@@ -12574,12 +11297,12 @@ Examples:
12574
11297
 
12575
11298
  // src/commands/funnels.ts
12576
11299
  init_source();
12577
- var import_node_fs12 = __toESM(require("node:fs"));
12578
- var import_node_path10 = __toESM(require("node:path"));
11300
+ var import_node_fs13 = __toESM(require("node:fs"));
11301
+ var import_node_path11 = __toESM(require("node:path"));
12579
11302
  function readSteps(command, file) {
12580
11303
  let parsed;
12581
11304
  try {
12582
- parsed = JSON.parse(import_node_fs12.default.readFileSync(import_node_path10.default.resolve(process.cwd(), file), "utf8"));
11305
+ parsed = JSON.parse(import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), file), "utf8"));
12583
11306
  } catch (error) {
12584
11307
  fail(command, `Invalid --steps-file JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.validation);
12585
11308
  }
@@ -12598,27 +11321,27 @@ function printFunnel(funnel) {
12598
11321
  for (const step of [...funnel.steps].sort((a, b) => a.order - b.order)) console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
12599
11322
  console.log();
12600
11323
  }
12601
- var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
11324
+ var list3 = new Command("list").alias("ls").description("List funnels").action(async function action35() {
12602
11325
  const res = await listFunnels(requireCredentials());
12603
11326
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to list funnels", classifyStatus(res.status), res.errorDetails);
12604
11327
  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
11328
  });
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) {
11329
+ 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
11330
  const res = await createFunnel(requireCredentials(), { name: opts.name, steps: readSteps(this, opts.stepsFile) });
12608
11331
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to create funnel", classifyStatus(res.status), res.errorDetails);
12609
11332
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12610
11333
  });
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) {
11334
+ 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
11335
  const res = await updateFunnel(requireCredentials(), id, { steps: readSteps(this, opts.stepsFile) });
12613
11336
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to update funnel", classifyStatus(res.status), res.errorDetails);
12614
11337
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12615
11338
  });
12616
- var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
11339
+ var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action38(id) {
12617
11340
  const res = await deleteFunnel(requireCredentials(), id);
12618
11341
  if (!res.ok) fail(this, res.error ?? "Failed to delete funnel", classifyStatus(res.status), res.errorDetails);
12619
11342
  output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
12620
11343
  });
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) {
11344
+ 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
11345
  if (!/^[1-9]\d*$/.test(opts.days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
12623
11346
  const days = Number.parseInt(opts.days, 10);
12624
11347
  if (!Number.isSafeInteger(days)) fail(this, "--days must be a positive integer", EXIT_CODES.validation);
@@ -12626,7 +11349,7 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
12626
11349
  if (!res.ok || !res.data) fail(this, res.error ?? "Failed to run funnel", classifyStatus(res.status), res.errorDetails);
12627
11350
  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
11351
  });
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) {
11352
+ 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
11353
  const steps = readSteps(this, opts.stepsFile);
12631
11354
  output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
12632
11355
  });
@@ -12683,11 +11406,11 @@ async function load2(command) {
12683
11406
  }
12684
11407
  return result.data;
12685
11408
  }
12686
- var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
11409
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action41() {
12687
11410
  const { environments } = await load2(this);
12688
11411
  output(this, { environments }, () => printEnvironments(environments.items));
12689
11412
  });
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) {
11413
+ 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
11414
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12692
11415
  if (!normalizedDomain) {
12693
11416
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12730,7 +11453,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
12730
11453
  () => printEnvironments(result.data.environments.items)
12731
11454
  );
12732
11455
  });
12733
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
11456
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action43(domain) {
12734
11457
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12735
11458
  if (!normalizedDomain) {
12736
11459
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12784,11 +11507,11 @@ function printConsent(data) {
12784
11507
  console.log(`Getter: ${data.consentMode.getter || "-"}`);
12785
11508
  console.log(`Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`);
12786
11509
  }
12787
- var get2 = new Command("get").description("Show consent settings").action(async function action45() {
11510
+ var get2 = new Command("get").description("Show consent settings").action(async function action44() {
12788
11511
  const data = await load3(this);
12789
11512
  output(this, data, () => printConsent(data));
12790
11513
  });
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) {
11514
+ 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
11515
  const consentMode = { enabled: true, provider: opts.provider, requiredGroup: opts.group };
12793
11516
  if (opts.getter !== void 0) consentMode.getter = opts.getter;
12794
11517
  const result = await updateConsent(requireCredentials(), { consentMode, applyCanonicalSplit: true });
@@ -12796,7 +11519,7 @@ var set = new Command("set").description("Enable and configure consent gating").
12796
11519
  warn(this, result.data.precompile);
12797
11520
  output(this, result.data, () => printConsent(result.data));
12798
11521
  });
12799
- var off = new Command("off").description("Disable consent gating").action(async function action47() {
11522
+ var off = new Command("off").description("Disable consent gating").action(async function action46() {
12800
11523
  const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
12801
11524
  if (!result.ok || !result.data) fail(this, result.error ?? "Failed to disable consent settings", classifyStatus(result.status), result.errorDetails);
12802
11525
  warn(this, result.data.precompile);
@@ -12831,21 +11554,21 @@ async function patchWithRetry(command, build) {
12831
11554
  warn2(command, result.data.precompile);
12832
11555
  return result.data;
12833
11556
  }
12834
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
11557
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action47() {
12835
11558
  const data = await load4(this);
12836
11559
  output(this, data, () => printDomains(data));
12837
11560
  });
12838
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
11561
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action48(domain) {
12839
11562
  const added = domain.map(normalizeDomain);
12840
11563
  const data = await patchWithRetry(this, (current) => Array.from(/* @__PURE__ */ new Set([...current.additionalDomains, ...added])));
12841
11564
  output(this, data, () => printDomains(data));
12842
11565
  });
12843
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
11566
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action49(domain) {
12844
11567
  const removeSet = new Set(domain.map(normalizeDomain));
12845
11568
  const data = await patchWithRetry(this, (current) => current.additionalDomains.filter((d) => !removeSet.has(normalizeDomain(d))));
12846
11569
  output(this, data, () => printDomains(data));
12847
11570
  });
12848
- var set2 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
11571
+ var set2 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action50(domain) {
12849
11572
  const replacement = domain.map(normalizeDomain);
12850
11573
  const data = await patchWithRetry(this, () => replacement);
12851
11574
  output(this, data, () => printDomains(data));
@@ -12864,7 +11587,11 @@ function normalizeDomain2(value) {
12864
11587
  return "";
12865
11588
  }
12866
11589
  }
12867
- var QA_FETCH_TIMEOUT_MS = 12e3;
11590
+ var QA_FETCH_TIMEOUT_MS = 15e3;
11591
+ function qaFetchTimeoutMs() {
11592
+ const testOverride = Number(cliTestSetting("QA_FETCH_TIMEOUT_MS"));
11593
+ return Number.isFinite(testOverride) && testOverride > 0 ? testOverride : QA_FETCH_TIMEOUT_MS;
11594
+ }
12868
11595
  var NAMED_HTML_ENTITIES = {
12869
11596
  amp: "&",
12870
11597
  lt: "<",
@@ -12902,29 +11629,29 @@ async function patchWithRetry2(command, build) {
12902
11629
  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
11630
  return result.data;
12904
11631
  }
12905
- var get3 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
11632
+ var get3 = new Command("get").description("Show cross-domain handover settings").action(async function action51() {
12906
11633
  const data = await load5(this);
12907
11634
  output(this, data, () => printSettings(data.externalTracking));
12908
11635
  });
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) {
11636
+ 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
11637
  const domains = opts.domain.map(normalizeDomain2);
12911
11638
  if (domains.some((domain) => !domain)) fail(this, "Every --domain value must be a valid hostname containing a dot", EXIT_CODES.validation);
12912
11639
  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
11640
  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
11641
  output(this, data, () => printSettings(data.externalTracking));
12915
11642
  });
12916
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
11643
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action53() {
12917
11644
  const data = await patchWithRetry2(this, (current) => ({ externalTracking: { domains: [], signed: true }, ifRevision: current.revision }));
12918
11645
  output(this, data, () => printSettings(data.externalTracking));
12919
11646
  });
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) {
11647
+ 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
11648
  const config = await load5(this);
12922
11649
  const target = normalizeDomain2(opts.toDomain);
12923
11650
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
12924
11651
  let response;
12925
11652
  let html;
12926
11653
  try {
12927
- response = await fetch(opts.from, { signal: AbortSignal.timeout(QA_FETCH_TIMEOUT_MS), redirect: "follow" });
11654
+ response = await fetch(opts.from, { signal: AbortSignal.timeout(qaFetchTimeoutMs()), redirect: "follow" });
12928
11655
  if (!response.ok) fail(this, `Fetching ${opts.from} returned HTTP ${response.status}`, classifyStatus(response.status));
12929
11656
  html = await response.text();
12930
11657
  } catch (error) {
@@ -12934,7 +11661,10 @@ var qa2 = new Command("qa").description("Check static link/form handover paramet
12934
11661
  }
12935
11662
  const param = config.externalTracking.handoverQueryParam;
12936
11663
  const baseUrl = response.url || opts.from;
12937
- const hrefs = [...html.matchAll(/<(a|form)\b[^>]*(?:href|action)\s*=\s*["']([^"']+)["'][^>]*>/gi)].map((m) => decodeHtmlEntities(m[2]));
11664
+ const hrefs = [
11665
+ ...html.matchAll(/<(?:a|form)\b[^>]*\s(?:href|action)\s*=\s*["']([^"']+)["'][^>]*>/gi),
11666
+ ...html.matchAll(/<[a-z][^>]*\s(?:data-href|data-url)\s*=\s*["']([^"']+)["'][^>]*>/gi)
11667
+ ].map((m) => decodeHtmlEntities(m[1]));
12938
11668
  const targetUrls = hrefs.map((raw) => {
12939
11669
  try {
12940
11670
  return new URL(raw, baseUrl);
@@ -12966,6 +11696,7 @@ var DETECTION_MARKERS = [
12966
11696
  { id: "shopware", kind: "platform", verdict: "Shopware", confidence: "high", recipe: { placement: "Shopware storefront base template before </head>." }, match: /shopware|sw-?storefront|data-shopware/i },
12967
11697
  { id: "next_headless", kind: "platform", verdict: "Next.js headless", confidence: "high", recipe: { placement: "Headless app document/root layout before </head> so Apex loads on every route." }, match: /__NEXT_DATA__|\/_next\//i },
12968
11698
  { id: "webflow", kind: "platform", verdict: "Webflow", confidence: "high", recipe: { placement: "Webflow Project Settings \u2192 Custom Code \u2192 Head Code." }, match: /data-wf-page|data-wf-site|webflow\.js|assets\.website-files\.com/i },
11699
+ { id: "borlabs", kind: "cmp", verdict: "Borlabs Cookie", recipe: { consentProvider: "borlabs", consentGroup: "statistics" }, match: /borlabs(?:-cookie)?|BorlabsCookie/i },
12969
11700
  { id: "cookiescript", kind: "cmp", verdict: "CookieScript", recipe: { consentProvider: "cookiescript", consentGroup: "targeting" }, match: /cookie-script\.com|CookieScript|cookiescript/i },
12970
11701
  { id: "usercentrics", kind: "cmp", verdict: "Usercentrics", recipe: { consentProvider: "usercentrics", consentGroup: "marketing" }, match: /app\.usercentrics\.eu|usercentrics\.eu|UC_UI|usercentrics/i },
12971
11702
  { id: "cookiebot", kind: "cmp", verdict: "Cookiebot", recipe: { consentProvider: "cookiebot", consentGroup: "marketing" }, match: /consent\.cookiebot\.com|Cookiebot/i },
@@ -13033,8 +11764,8 @@ function goalMatches(goal, rows) {
13033
11764
  var CHALLENGE_STATUSES = [401, 403, 429, 503];
13034
11765
  var PAGE_FETCH_TIMEOUT_MS = 12e3;
13035
11766
  async function fetchPrimaryHtml(host) {
13036
- if (process.env.APEX_CLI_MOCK_API === "1") {
13037
- const state = process.env.APEX_CLI_MOCK_DOCTOR_STATE;
11767
+ if (cliTestSetting("API") === "1") {
11768
+ const state = cliTestSetting("DOCTOR_STATE");
13038
11769
  if (state === "protected") return { protected: true };
13039
11770
  if (state === "page-error") return { error: "http 404" };
13040
11771
  if (state === "page-timeout") return { error: "The operation was aborted due to timeout" };
@@ -13092,8 +11823,8 @@ function servedConfigUrl(workerBase, shopId) {
13092
11823
  return `${workerBase}/config?shopId=${encodeURIComponent(shopId)}&env=${PRODUCTION_ENVIRONMENT_KEY}`;
13093
11824
  }
13094
11825
  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";
11826
+ if (cliTestSetting("API") === "1") {
11827
+ const state = cliTestSetting("DOCTOR_SERVED_CONFIG") ?? "agree";
13097
11828
  if (state === "unreachable") return { error: "mock worker is unreachable" };
13098
11829
  if (state === "diverge") return { config: { storeOrigins: ["https://example.com"], runtime: { consentMode: { enabled: false, provider: "custom" } } } };
13099
11830
  return { config: { storeOrigins: ["https://example.com", "https://www.example.com", "https://shop.example.com"], runtime: { consentMode: { enabled: true, provider: "cookiescript" } } } };
@@ -13190,7 +11921,7 @@ async function runDoctor(options = {}) {
13190
11921
  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
11922
  checks.push(withCaveat(devPollutionCheck(eventHosts, envsRes.data), inventoryCaveat));
13192
11923
  const primary = hostOnly(domainsRes.data.domain) ?? eventHosts[0];
13193
- const shopId = resolveShopId(options);
11924
+ const shopId = resolveShopId(options) ?? domainsRes.data.shop_id;
13194
11925
  const workerBase = resolveWorkerBase(options);
13195
11926
  const [page, servedResult] = await Promise.all([
13196
11927
  primary ? fetchPrimaryHtml(primary) : Promise.resolve({}),
@@ -13200,10 +11931,11 @@ async function runDoctor(options = {}) {
13200
11931
  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
11932
  else if (page.protected) checks.push({ checkId: "cmp_mismatch", status: "SKIP", evidence: [`Primary storefront ${primary} appears protected or bot-challenged; verify in browser.`] });
13202
11933
  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."] });
11934
+ 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
11935
  else {
13204
11936
  const cmpMarker = matchDetectionMarkers(page.html, "cmp")[0];
13205
11937
  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."] });
11938
+ 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
11939
  } else {
13208
11940
  const vendor = cmpMarker.verdict;
13209
11941
  const mismatch = !consent.enabled || consent.provider && consent.provider.toLowerCase() !== vendor.toLowerCase();
@@ -13257,7 +11989,7 @@ function consentRateCheck(items) {
13257
11989
  const estimate = estimateConsentRate(items);
13258
11990
  return { checkId: "consent_rate", status: "PASS", evidence: [`estimatedAcceptRate=${estimate.estimatedAcceptRate ?? "unknown"}`, `Baseline ${describeWindow(estimate.baselineWindow)}; gated ${describeWindow(estimate.gatedWindow)}`, CAVEAT] };
13259
11991
  }
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) {
11992
+ 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
11993
  const data = await runDoctor(options);
13262
11994
  output(this, data, () => printDoctorReport(data.checks));
13263
11995
  if (data.checks.some((c) => c.status === "FAIL")) process.exitCode = EXIT_CODES.general;
@@ -13271,7 +12003,7 @@ function printDoctorReport(checks) {
13271
12003
  if (check.fix) console.log(` fix: ${check.fix}`);
13272
12004
  }
13273
12005
  }
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) {
12006
+ 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
12007
  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
12008
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
13277
12009
  if (!result.ok || !result.data) fail(this, result.error ?? "Failed to load visitor series", classifyStatus(result.status), result.errorDetails);
@@ -13310,14 +12042,14 @@ function analyticsSubcommand(name, description, defaultDays) {
13310
12042
  command.option("--planning-days <n>", "Planning window in days", "180");
13311
12043
  command.option("--limit <n>", "Maximum page rows", "50");
13312
12044
  }
13313
- return command.action(async function action70(opts) {
12045
+ return command.action(async function action69(opts) {
13314
12046
  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
12047
  if (!result.ok || !result.data) fail(this, result.error ?? `Failed to load ${name} analytics`, classifyStatus(result.status), result.errorDetails);
13316
12048
  output(this, { data: result.data.data }, () => printAnalytics(result.data.data));
13317
12049
  });
13318
12050
  }
13319
12051
  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) {
12052
+ 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
12053
  const creds = requireCredentials();
13322
12054
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
13323
12055
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -13348,7 +12080,7 @@ function urlsCommand() {
13348
12080
  });
13349
12081
  });
13350
12082
  }
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", `
12083
+ 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
12084
  Examples:
13353
12085
  apex analytics stats --days 7
13354
12086
  apex analytics pages --planning-days 180 --limit 25
@@ -13394,18 +12126,10 @@ function privateIp(rawHost) {
13394
12126
  const [a, b] = host.split(".").map(Number);
13395
12127
  return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
13396
12128
  }
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
- }
12129
+ var testResolver;
12130
+ var testFetch;
13407
12131
  async function resolveHost(hostname) {
13408
- const mocked = mockResolve(hostname);
12132
+ const mocked = testResolver?.(hostname) ?? null;
13409
12133
  const addresses = mocked ?? await (0, import_promises3.lookup)(hostname, { all: true });
13410
12134
  return addresses.map((entry) => entry.address);
13411
12135
  }
@@ -13430,44 +12154,6 @@ async function validateUrl(raw, allowedHosts) {
13430
12154
  if (privateAddress) throw new UrlPolicyError(`Refusing ${url.hostname}; DNS resolves to a private address (${privateAddress})`);
13431
12155
  return { url, addresses };
13432
12156
  }
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
12157
  var REQUEST_TIMEOUT_MS = 1e4;
13472
12158
  var RESPONSE_DEADLINE_MS = 15e3;
13473
12159
  var BODY_LIMIT_BYTES = 2 * 1024 * 1024;
@@ -13496,10 +12182,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
13496
12182
  const chunks = [];
13497
12183
  let bytes = 0;
13498
12184
  let settled = false;
13499
- const settle = (action70) => {
12185
+ const settle = (action69) => {
13500
12186
  if (settled) return;
13501
12187
  settled = true;
13502
- action70();
12188
+ action69();
13503
12189
  };
13504
12190
  stream.on("data", (chunk) => {
13505
12191
  if (settled) return;
@@ -13550,11 +12236,11 @@ function pinnedFetch(url, addresses, limits) {
13550
12236
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
13551
12237
  };
13552
12238
  let settled = false;
13553
- const settle = (action70) => {
12239
+ const settle = (action69) => {
13554
12240
  if (settled) return;
13555
12241
  settled = true;
13556
12242
  clearTimeout(deadline);
13557
- action70();
12243
+ action69();
13558
12244
  };
13559
12245
  const req = send2(url, {
13560
12246
  method: "GET",
@@ -13631,7 +12317,7 @@ async function enforceBodyLimit(url, response, limits) {
13631
12317
  async function fetchSafe(raw, allowedHosts, limits) {
13632
12318
  let target = await validateUrl(raw, allowedHosts);
13633
12319
  for (let hop = 0; hop <= 5; hop++) {
13634
- const mocked = mockFetch(target.url);
12320
+ const mocked = testFetch?.(target.url) ?? null;
13635
12321
  const result = mocked ? await enforceBodyLimit(target.url, mocked, limits) : await pinnedFetch(target.url, target.addresses, limits);
13636
12322
  if (![301, 302, 303, 307, 308].includes(result.response.status)) return result;
13637
12323
  const location = result.response.headers.get("location");
@@ -13743,13 +12429,14 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
13743
12429
  message: "Worker served a recognized config document for this shop."
13744
12430
  };
13745
12431
  }
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) {
12432
+ 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
12433
  const creds = requireCredentials();
13748
12434
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
13749
12435
  if (!shopId) fail(this, "Shop ID is required: pass --shop-id <id> or set DRIP_SHOP_ID", EXIT_CODES.validation, { field: "shop-id" });
13750
12436
  const domains = await getInstallDomains(creds);
13751
12437
  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;
12438
+ const mockStoreOrigins = cliTestSetting("STORE_ORIGINS");
12439
+ const storeOrigins = cliTestSetting("API") === "1" && mockStoreOrigins ? mockStoreOrigins.split(",") : domains.data.storeOrigins;
13753
12440
  const origins = opts.url ? [opts.url] : storeOrigins;
13754
12441
  const allowedHosts = new Set(storeOrigins.map((origin) => normalizeHostname(new URL(origin).hostname)));
13755
12442
  const pages = [];
@@ -13808,11 +12495,11 @@ var installCommand = new Command("install").description("Check Apex storefront i
13808
12495
 
13809
12496
  // src/commands/flags.ts
13810
12497
  init_source();
13811
- var import_node_fs13 = __toESM(require("node:fs"));
13812
- var import_node_path11 = __toESM(require("node:path"));
12498
+ var import_node_fs14 = __toESM(require("node:fs"));
12499
+ var import_node_path12 = __toESM(require("node:path"));
13813
12500
 
13814
12501
  // src/edge-status.ts
13815
- function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 420) {
12502
+ function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
13816
12503
  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
12504
  }
13818
12505
  function parsePublicationMetadata(value) {
@@ -13837,7 +12524,7 @@ function compareEdgePublication(currentValue, edgeValue, expectedWorstCaseSecond
13837
12524
  return { ...base, verdict: "live", relationship: edge.publication_revision > current.publication_revision ? "edge_ahead" : "equal" };
13838
12525
  }
13839
12526
  function formatEdgePropagationStatus(status2) {
13840
- const caveat = "This checks one CDN vantage point; other PoPs may lag by up to max-age + SWR.";
12527
+ const caveat = "This checks one CDN vantage point; other PoPs may lag by up to max-age.";
13841
12528
  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
12529
  if (status2.verdict === "unknown") return `Unknown \u2014 edge payload predates publication metadata. ${caveat}`;
13843
12530
  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 +12538,7 @@ function body(command, value, opts) {
13851
12538
  let parsed = {};
13852
12539
  if (value) {
13853
12540
  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;
12541
+ 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
12542
  try {
13856
12543
  parsed = JSON.parse(text);
13857
12544
  } catch (e) {
@@ -13979,18 +12666,18 @@ function write(name) {
13979
12666
  });
13980
12667
  return c;
13981
12668
  }
13982
- function lifecycle(action70) {
13983
- return new Command(action70).description(`${action70[0].toUpperCase()}${action70.slice(1)} a flag`).argument("<id>").action(async function(id) {
12669
+ function lifecycle(action69) {
12670
+ return new Command(action69).description(`${action69[0].toUpperCase()}${action69.slice(1)} a flag`).argument("<id>").action(async function(id) {
13984
12671
  const r = await transitionManagedDelivery(
13985
12672
  requireCredentials(),
13986
12673
  kind,
13987
12674
  id,
13988
- action70
12675
+ action69
13989
12676
  );
13990
12677
  if (!r.ok || !r.data)
13991
12678
  fail(
13992
12679
  this,
13993
- r.error ?? `Failed to ${action70}`,
12680
+ r.error ?? `Failed to ${action69}`,
13994
12681
  classifyStatus(r.status),
13995
12682
  r.errorDetails
13996
12683
  );
@@ -14006,8 +12693,8 @@ Examples:
14006
12693
  `);
14007
12694
 
14008
12695
  // src/commands/backlog.ts
14009
- var import_node_fs14 = __toESM(require("node:fs"));
14010
- var import_node_path12 = __toESM(require("node:path"));
12696
+ var import_node_fs15 = __toESM(require("node:fs"));
12697
+ var import_node_path13 = __toESM(require("node:path"));
14011
12698
  init_source();
14012
12699
  var kind2 = "backlog";
14013
12700
  var singular = "backlog item";
@@ -14015,7 +12702,7 @@ function parse(command, value) {
14015
12702
  if (!value) return {};
14016
12703
  try {
14017
12704
  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;
12705
+ 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
12706
  const body5 = JSON.parse(text);
14020
12707
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
14021
12708
  return body5;
@@ -14040,18 +12727,18 @@ var get5 = new Command("get").description("Get a backlog item").argument("<id>",
14040
12727
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
14041
12728
  output(this, { backlog_item: r.data }, () => detail2(r.data));
14042
12729
  });
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`);
12730
+ function write2(action69) {
12731
+ const c = new Command(action69).description(`${action69 === "create" ? "Create" : "Update"} a ${singular}`);
12732
+ if (action69 === "update") c.argument("<id>", `${singular} ID`);
14046
12733
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
14047
12734
  c.action(async function(...args) {
14048
12735
  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;
12736
+ const id = action69 === "update" ? String(args[0]) : void 0;
12737
+ const raw = o.file ? `@${o.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14051
12738
  const body5 = parse(this, raw);
14052
12739
  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);
12740
+ const r = action69 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
12741
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action69}`, classifyStatus(r.status), r.errorDetails);
14055
12742
  output(this, { backlog_item: r.data }, () => detail2(r.data));
14056
12743
  });
14057
12744
  return c;
@@ -14086,8 +12773,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
14086
12773
  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
12774
 
14088
12775
  // src/commands/signals.ts
14089
- var import_node_fs15 = __toESM(require("node:fs"));
14090
- var import_node_path13 = __toESM(require("node:path"));
12776
+ var import_node_fs16 = __toESM(require("node:fs"));
12777
+ var import_node_path14 = __toESM(require("node:path"));
14091
12778
  init_source();
14092
12779
  var kind3 = "signals";
14093
12780
  var singular2 = "signal";
@@ -14095,7 +12782,7 @@ function parse2(command, value) {
14095
12782
  if (!value) return {};
14096
12783
  try {
14097
12784
  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;
12785
+ 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
12786
  const body5 = JSON.parse(text);
14100
12787
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5)) throw new Error("body must be an object");
14101
12788
  return body5;
@@ -14120,18 +12807,18 @@ var get6 = new Command("get").description("Get a signal").argument("<id>", "sign
14120
12807
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
14121
12808
  output(this, { signal: r.data }, () => detail3(r.data));
14122
12809
  });
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`);
12810
+ function write3(action69) {
12811
+ const c = new Command(action69).description(`${action69 === "create" ? "Create" : "Update"} a ${singular2}`);
12812
+ if (action69 === "update") c.argument("<id>", `${singular2} ID`);
14126
12813
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
14127
12814
  c.action(async function(...args) {
14128
12815
  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;
12816
+ const id = action69 === "update" ? String(args[0]) : void 0;
12817
+ const raw = o.file ? `@${o.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14131
12818
  const body5 = parse2(this, raw);
14132
12819
  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);
12820
+ const r = action69 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
12821
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action69}`, classifyStatus(r.status), r.errorDetails);
14135
12822
  output(this, { signal: r.data }, () => detail3(r.data));
14136
12823
  });
14137
12824
  return c;
@@ -14145,14 +12832,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
14145
12832
 
14146
12833
  // src/commands/personalizations.ts
14147
12834
  init_source();
14148
- var import_node_fs16 = __toESM(require("node:fs"));
14149
- var import_node_path14 = __toESM(require("node:path"));
12835
+ var import_node_fs17 = __toESM(require("node:fs"));
12836
+ var import_node_path15 = __toESM(require("node:path"));
14150
12837
  var kind4 = "personalizations";
14151
12838
  function body2(command, value, opts) {
14152
12839
  let parsed = {};
14153
12840
  if (value) {
14154
12841
  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;
12842
+ 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
12843
  try {
14157
12844
  parsed = JSON.parse(text);
14158
12845
  } catch (e) {
@@ -14249,20 +12936,20 @@ function write4(name) {
14249
12936
  });
14250
12937
  return c;
14251
12938
  }
14252
- function lifecycle2(action70) {
14253
- return new Command(action70).description(
14254
- `${action70[0].toUpperCase()}${action70.slice(1)} a personalization`
12939
+ function lifecycle2(action69) {
12940
+ return new Command(action69).description(
12941
+ `${action69[0].toUpperCase()}${action69.slice(1)} a personalization`
14255
12942
  ).argument("<id>").action(async function(id) {
14256
12943
  const r = await transitionManagedDelivery(
14257
12944
  requireCredentials(),
14258
12945
  kind4,
14259
12946
  id,
14260
- action70
12947
+ action69
14261
12948
  );
14262
12949
  if (!r.ok || !r.data)
14263
12950
  fail(
14264
12951
  this,
14265
- r.error ?? `Failed to ${action70}`,
12952
+ r.error ?? `Failed to ${action69}`,
14266
12953
  classifyStatus(r.status),
14267
12954
  r.errorDetails
14268
12955
  );
@@ -14273,13 +12960,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
14273
12960
 
14274
12961
  // src/commands/targeting-resources.ts
14275
12962
  init_source();
14276
- var import_node_fs17 = __toESM(require("node:fs"));
14277
- var import_node_path15 = __toESM(require("node:path"));
12963
+ var import_node_fs18 = __toESM(require("node:fs"));
12964
+ var import_node_path16 = __toESM(require("node:path"));
14278
12965
  function body3(command, value) {
14279
12966
  if (!value) return {};
14280
12967
  const source = value.startsWith("@") ? value.slice(1) : value;
14281
12968
  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;
12969
+ 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
12970
  return JSON.parse(text);
14284
12971
  } catch (error) {
14285
12972
  fail(
@@ -14358,17 +13045,17 @@ function targetingResourceCommand(kind5) {
14358
13045
  );
14359
13046
  })
14360
13047
  );
14361
- for (const action70 of ["create", "update"]) {
14362
- const command = new Command(action70).description(
14363
- `${action70 === "create" ? "Create" : "Update"} a ${singular3}`
13048
+ for (const action69 of ["create", "update"]) {
13049
+ const command = new Command(action69).description(
13050
+ `${action69 === "create" ? "Create" : "Update"} a ${singular3}`
14364
13051
  );
14365
- if (action70 === "update") command.argument("<id>");
13052
+ if (action69 === "update") command.argument("<id>");
14366
13053
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
14367
13054
  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;
13055
+ const id = action69 === "update" ? String(args[0]) : void 0;
13056
+ const value = opts.file ? `@${opts.file}` : String(args[action69 === "update" ? 1 : 0] ?? "") || void 0;
14370
13057
  const requestBody = body3(this, value);
14371
- if (action70 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
13058
+ if (action69 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
14372
13059
  fail(
14373
13060
  this,
14374
13061
  `Updating a ${singular3} requires at least one field.`,
@@ -14376,7 +13063,7 @@ function targetingResourceCommand(kind5) {
14376
13063
  );
14377
13064
  const response = await requestTargetingResource(
14378
13065
  requireCredentials(),
14379
- action70 === "create" ? "POST" : "PATCH",
13066
+ action69 === "create" ? "POST" : "PATCH",
14380
13067
  kind5,
14381
13068
  id,
14382
13069
  requestBody
@@ -14384,7 +13071,7 @@ function targetingResourceCommand(kind5) {
14384
13071
  if (!response.ok || !response.data)
14385
13072
  fail(
14386
13073
  this,
14387
- response.error ?? `Failed to ${action70}`,
13074
+ response.error ?? `Failed to ${action69}`,
14388
13075
  classifyStatus(response.status),
14389
13076
  response.errorDetails
14390
13077
  );
@@ -14393,7 +13080,7 @@ function targetingResourceCommand(kind5) {
14393
13080
  {
14394
13081
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
14395
13082
  },
14396
- () => console.log(source_default.green(`${action70}d ${singular3}`))
13083
+ () => console.log(source_default.green(`${action69}d ${singular3}`))
14397
13084
  );
14398
13085
  });
14399
13086
  root.addCommand(command);
@@ -14480,7 +13167,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
14480
13167
  // src/commands/brand.ts
14481
13168
  init_source();
14482
13169
  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() {
13170
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action58() {
14484
13171
  const creds = requireCredentials();
14485
13172
  const res = await getBrandContext(creds);
14486
13173
  if (res.status === 404) {
@@ -14513,7 +13200,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
14513
13200
 
14514
13201
  // src/commands/targeting.ts
14515
13202
  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) {
13203
+ 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
13204
  const creds = requireCredentials();
14518
13205
  const res = await testTargeting(creds, url, { status: opts.status });
14519
13206
  if (!res.ok || !res.data) {
@@ -14553,13 +13240,13 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
14553
13240
 
14554
13241
  // src/commands/sos.ts
14555
13242
  init_source();
14556
- var import_node_fs18 = __toESM(require("node:fs"));
14557
- var import_node_path16 = __toESM(require("node:path"));
13243
+ var import_node_fs19 = __toESM(require("node:fs"));
13244
+ var import_node_path17 = __toESM(require("node:path"));
14558
13245
  var SOS_TYPES = /* @__PURE__ */ new Set(["full_handover", "design", "development", "qa", "config", "bug"]);
14559
13246
  function readText(value) {
14560
13247
  if (!value) return void 0;
14561
13248
  if (!value.startsWith("@")) return value;
14562
- return import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), value.slice(1)), "utf8");
13249
+ return import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), value.slice(1)), "utf8");
14563
13250
  }
14564
13251
  function printTicket(ticket, reused) {
14565
13252
  console.log();
@@ -14570,7 +13257,7 @@ function printTicket(ticket, reused) {
14570
13257
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
14571
13258
  console.log();
14572
13259
  }
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) {
13260
+ 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
13261
  if (!SOS_TYPES.has(opts.type)) {
14575
13262
  fail(this, "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.", 3);
14576
13263
  }
@@ -14588,7 +13275,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
14588
13275
  }
14589
13276
  output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
14590
13277
  });
14591
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
13278
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action61() {
14592
13279
  const creds = requireCredentials();
14593
13280
  const res = await listMySos(creds);
14594
13281
  if (!res.ok || !res.data) {
@@ -14641,13 +13328,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
14641
13328
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send).addCommand(markRead);
14642
13329
 
14643
13330
  // src/commands/shops.ts
14644
- var import_node_fs19 = __toESM(require("node:fs"));
14645
- var import_node_path17 = __toESM(require("node:path"));
13331
+ var import_node_fs20 = __toESM(require("node:fs"));
13332
+ var import_node_path18 = __toESM(require("node:path"));
14646
13333
  function body4(command, raw) {
14647
13334
  if (!raw) return {};
14648
13335
  try {
14649
13336
  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;
13337
+ const text = raw.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(source), "utf8") : raw;
14651
13338
  const value = JSON.parse(text);
14652
13339
  if (!value || typeof value !== "object" || Array.isArray(value))
14653
13340
  throw new Error("body must be an object");
@@ -14909,7 +13596,7 @@ function commandSchema(command) {
14909
13596
  };
14910
13597
  }
14911
13598
  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() {
13599
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action69() {
14913
13600
  const schema = commandSchema(program3);
14914
13601
  output(this, schema, () => {
14915
13602
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -14919,16 +13606,16 @@ function createSchemaCommand(program3) {
14919
13606
  }
14920
13607
 
14921
13608
  // src/commands/skills.ts
14922
- var import_node_fs20 = __toESM(require("node:fs"));
14923
- var import_node_path18 = __toESM(require("node:path"));
13609
+ var import_node_fs21 = __toESM(require("node:fs"));
13610
+ var import_node_path19 = __toESM(require("node:path"));
14924
13611
  function packageRoot() {
14925
13612
  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")
13613
+ import_node_path19.default.resolve(__dirname, ".."),
13614
+ import_node_path19.default.resolve(import_node_path19.default.dirname(process.argv[1] ?? process.cwd()), ".."),
13615
+ import_node_path19.default.resolve(process.cwd(), "packages/cli")
14929
13616
  ];
14930
13617
  for (const candidate of candidates) {
14931
- if (import_node_fs20.default.existsSync(import_node_path18.default.join(candidate, "AGENTS.md"))) return candidate;
13618
+ if (import_node_fs21.default.existsSync(import_node_path19.default.join(candidate, "AGENTS.md"))) return candidate;
14932
13619
  }
14933
13620
  throw new Error(
14934
13621
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -14937,24 +13624,24 @@ function packageRoot() {
14937
13624
  function skillPaths() {
14938
13625
  const root = packageRoot();
14939
13626
  return {
14940
- agents: import_node_path18.default.join(root, "AGENTS.md"),
14941
- skill: import_node_path18.default.join(root, "skills", "apex-cli", "SKILL.md")
13627
+ agents: import_node_path19.default.join(root, "AGENTS.md"),
13628
+ skill: import_node_path19.default.join(root, "skills", "apex-cli", "SKILL.md")
14942
13629
  };
14943
13630
  }
14944
13631
  function installSkill(paths, targetRoot) {
14945
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs20.default.existsSync(filePath));
13632
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs21.default.existsSync(filePath));
14946
13633
  if (missing.length > 0) {
14947
13634
  throw new Error(
14948
13635
  `Apex CLI package files not found. Missing: ${missing.join(", ")}`
14949
13636
  );
14950
13637
  }
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"));
13638
+ const target = import_node_path19.default.resolve(targetRoot, "apex-cli");
13639
+ import_node_fs21.default.mkdirSync(target, { recursive: true });
13640
+ import_node_fs21.default.copyFileSync(paths.skill, import_node_path19.default.join(target, "SKILL.md"));
13641
+ import_node_fs21.default.copyFileSync(paths.agents, import_node_path19.default.join(target, "AGENTS.md"));
14955
13642
  return target;
14956
13643
  }
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) {
13644
+ 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
13645
  let paths;
14959
13646
  try {
14960
13647
  paths = skillPaths();
@@ -15169,8 +13856,8 @@ async function fetchText(url) {
15169
13856
  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
13857
  return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
15171
13858
  }
15172
- function abs(base, path20) {
15173
- return new URL(path20, base).toString();
13859
+ function abs(base, path21) {
13860
+ return new URL(path21, base).toString();
15174
13861
  }
15175
13862
  function looksLikeShopifyCart(text) {
15176
13863
  try {
@@ -15190,9 +13877,9 @@ async function collectSignals(url) {
15190
13877
  const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
15191
13878
  const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
15192
13879
  if (!protectedHit) {
15193
- for (const path20 of ["/cart.js"]) {
13880
+ for (const path21 of ["/cart.js"]) {
15194
13881
  try {
15195
- const secondary = await fetchText(abs(primary.finalUrl || url, path20));
13882
+ const secondary = await fetchText(abs(primary.finalUrl || url, path21));
15196
13883
  if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
15197
13884
  chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
15198
13885
  }
@@ -15267,7 +13954,7 @@ var prospectCommand = new Command("prospect").description("Pre-sales storefront
15267
13954
 
15268
13955
  // src/commands/webhooks.ts
15269
13956
  init_source();
15270
- var SIGNATURE_DOC = "https://app.drip-apex.com/docs/api/webhooks#signatures";
13957
+ var SIGNATURE_DOC = "https://app.drip-apex.com/docs/developers/webhooks#signatures";
15271
13958
  function requireShopSecretKey(_command) {
15272
13959
  requireCredentials();
15273
13960
  }
@@ -15325,11 +14012,11 @@ function printDeliveries(deliveries2, webhook) {
15325
14012
  function selectedEvents(opts) {
15326
14013
  return [...new Set((opts.event ?? []).map((event) => event.trim()).filter(Boolean))];
15327
14014
  }
15328
- var list14 = new Command("list").alias("ls").description("List webhooks. JSON includes availableEvents; secrets are never returned after create.").action(async function action64() {
14015
+ var list14 = new Command("list").alias("ls").description("List webhooks. JSON includes availableEvents; secrets are never returned after create.").action(async function action63() {
15329
14016
  const data = await loadWebhookIndex(this);
15330
14017
  output(this, data, () => printWebhooks(data.data));
15331
14018
  });
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) {
14019
+ 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
14020
  const events = selectedEvents(opts);
15334
14021
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
15335
14022
  const index = await loadWebhookIndex(this);
@@ -15346,19 +14033,19 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
15346
14033
  console.log(`Signature verification: ${SIGNATURE_DOC}`);
15347
14034
  });
15348
14035
  });
15349
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action66(id) {
14036
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action65(id) {
15350
14037
  requireShopSecretKey(this);
15351
14038
  const result = handleResult(this, await deleteWebhook(requireCredentials(), id), "Failed to delete webhook");
15352
14039
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
15353
14040
  });
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) {
14041
+ 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
14042
  const limit = Number.parseInt(opts.limit, 10);
15356
14043
  if (!Number.isInteger(limit) || limit < 1) fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
15357
14044
  requireShopSecretKey(this);
15358
14045
  const result = handleResult(this, await getWebhookDeliveries(requireCredentials(), id, limit), "Failed to load deliveries");
15359
14046
  output(this, result, () => printDeliveries(result.data, result.webhook));
15360
14047
  });
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) {
14048
+ 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
14049
  if (opts.active && opts.inactive) fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
15363
14050
  const events = selectedEvents(opts);
15364
14051
  const body5 = {};
@@ -15378,7 +14065,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
15378
14065
  const result = handleResult(this, await updateWebhook(requireCredentials(), id, body5), "Failed to update webhook");
15379
14066
  output(this, { webhook: result }, () => printWebhooks([result]));
15380
14067
  });
15381
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action69(id) {
14068
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action68(id) {
15382
14069
  requireShopSecretKey(this);
15383
14070
  const result = handleResult(this, await testWebhook(requireCredentials(), id), "Failed to send test webhook");
15384
14071
  output(this, result, () => console.log(source_default.green(`Sent test delivery ${result.delivery.id}: ${result.delivery.status}`)));
@@ -15386,8 +14073,8 @@ var test3 = new Command("test").description("Send a signed test event to a webho
15386
14073
  var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update4).addCommand(remove4).addCommand(deliveries).addCommand(test3);
15387
14074
 
15388
14075
  // src/commands/config-resources.ts
15389
- var import_node_fs21 = __toESM(require("node:fs"));
15390
- var import_node_path19 = __toESM(require("node:path"));
14076
+ var import_node_fs22 = __toESM(require("node:fs"));
14077
+ var import_node_path20 = __toESM(require("node:path"));
15391
14078
  function show(resource) {
15392
14079
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
15393
14080
  `);
@@ -15472,14 +14159,14 @@ function readFamily(family, label) {
15472
14159
  }
15473
14160
  var assetReads = readFamily("assets", "assets");
15474
14161
  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))
14162
+ const absolute = import_node_path20.default.resolve(process.cwd(), file);
14163
+ if (!import_node_fs22.default.existsSync(absolute))
15477
14164
  fail(this, `File not found: ${file}`, 3);
15478
- const fileStat = import_node_fs21.default.statSync(absolute);
14165
+ const fileStat = import_node_fs22.default.statSync(absolute);
15479
14166
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
15480
14167
  if (fileStat.size > MAX_ASSET_SIZE_BYTES)
15481
14168
  fail(this, `${file} is larger than 10 MB`, 3);
15482
- const mimeType = ASSET_MIME_TYPES[import_node_path19.default.extname(absolute).toLowerCase()];
14169
+ const mimeType = ASSET_MIME_TYPES[import_node_path20.default.extname(absolute).toLowerCase()];
15483
14170
  if (!mimeType)
15484
14171
  fail(
15485
14172
  this,
@@ -15488,8 +14175,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
15488
14175
  );
15489
14176
  const r = await uploadAssets(requireCredentials(), [
15490
14177
  {
15491
- name: import_node_path19.default.basename(absolute),
15492
- bytes: import_node_fs21.default.readFileSync(absolute),
14178
+ name: import_node_path20.default.basename(absolute),
14179
+ bytes: import_node_fs22.default.readFileSync(absolute),
15493
14180
  type: mimeType
15494
14181
  }
15495
14182
  ]);
@@ -15543,6 +14230,19 @@ var snippet = new Command("snippet").description("Generate an external purchase
15543
14230
  var externalPixelsCommand = new Command("external-pixels").description("Manage external pixels").addCommand(snippet);
15544
14231
 
15545
14232
  // src/cli.ts
14233
+ var mockApiEnvironmentKey = ["APEX", "CLI", "MOCK", "API"].join("_");
14234
+ if (process.env[mockApiEnvironmentKey] === "1" && !cliTestHarnessActive()) {
14235
+ const message = "mock harness is not part of the published CLI; use the test artifact";
14236
+ const guardTerminatorIndex = process.argv.indexOf("--");
14237
+ const guardArgv = guardTerminatorIndex === -1 ? process.argv : process.argv.slice(0, guardTerminatorIndex);
14238
+ if (guardArgv.includes("--json") || !process.stdout.isTTY) {
14239
+ writeErrorEnvelope(new CliError(message, EXIT_CODES.general));
14240
+ } else {
14241
+ process.stderr.write(`${message}
14242
+ `);
14243
+ }
14244
+ process.exit(EXIT_CODES.general);
14245
+ }
15546
14246
  function addJsonOption(command) {
15547
14247
  if (!command.options.some((option) => option.long === "--json")) {
15548
14248
  command.option("--json", "Print JSON output");