@drip-apex/apex-cli 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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 path23 = require("node:path");
1163
- var fs27 = require("node:fs");
1162
+ var path24 = require("node:path");
1163
+ var fs28 = 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 (fs27.existsSync(executableFile)) return;
2144
+ if (fs28.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 = path23.resolve(baseDir, baseName);
2163
- if (fs27.existsSync(localBin)) return localBin;
2164
- if (sourceExt.includes(path23.extname(baseName))) return void 0;
2162
+ const localBin = path24.resolve(baseDir, baseName);
2163
+ if (fs28.existsSync(localBin)) return localBin;
2164
+ if (sourceExt.includes(path24.extname(baseName))) return void 0;
2165
2165
  const foundExt = sourceExt.find(
2166
- (ext) => fs27.existsSync(`${localBin}${ext}`)
2166
+ (ext) => fs28.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 = fs27.realpathSync(this._scriptPath);
2178
+ resolvedScriptPath = fs28.realpathSync(this._scriptPath);
2179
2179
  } catch {
2180
2180
  resolvedScriptPath = this._scriptPath;
2181
2181
  }
2182
- executableDir = path23.resolve(
2183
- path23.dirname(resolvedScriptPath),
2182
+ executableDir = path24.resolve(
2183
+ path24.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 = path23.basename(
2190
+ const legacyName = path24.basename(
2191
2191
  this._scriptPath,
2192
- path23.extname(this._scriptPath)
2192
+ path24.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(path23.extname(executableFile));
2203
+ launchWithNode = sourceExt.includes(path24.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 = path23.basename(filename, path23.extname(filename));
3050
+ this._name = path24.basename(filename, path24.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(path24) {
3065
- if (path24 === void 0) return this._executableDir;
3066
- this._executableDir = path24;
3064
+ executableDir(path25) {
3065
+ if (path25 === void 0) return this._executableDir;
3066
+ this._executableDir = path25;
3067
3067
  return this;
3068
3068
  }
3069
3069
  /**
@@ -6158,16 +6158,16 @@ __export(ora_exports, {
6158
6158
  function ora(options) {
6159
6159
  return new Ora(options);
6160
6160
  }
6161
- async function oraPromise(action70, options) {
6162
- const actionIsFunction = typeof action70 === "function";
6163
- const actionIsPromise = typeof action70.then === "function";
6161
+ async function oraPromise(action71, options) {
6162
+ const actionIsFunction = typeof action71 === "function";
6163
+ const actionIsPromise = typeof action71.then === "function";
6164
6164
  if (!actionIsFunction && !actionIsPromise) {
6165
6165
  throw new TypeError("Parameter `action` must be a Function or a Promise");
6166
6166
  }
6167
6167
  const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
6168
6168
  const spinner = ora(options).start();
6169
6169
  try {
6170
- const promise = actionIsFunction ? action70(spinner) : action70;
6170
+ const promise = actionIsFunction ? action71(spinner) : action71;
6171
6171
  const result = await promise;
6172
6172
  spinner.succeed(
6173
6173
  successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
@@ -6988,14 +6988,14 @@ var init_open = __esm({
6988
6988
  }
6989
6989
  const subprocess = import_node_child_process5.default.spawn(command, cliArguments, childProcessOptions);
6990
6990
  if (options.wait) {
6991
- return new Promise((resolve, reject) => {
6991
+ return new Promise((resolve2, reject) => {
6992
6992
  subprocess.once("error", reject);
6993
6993
  subprocess.once("close", (exitCode) => {
6994
6994
  if (!options.allowNonzeroExitCode && exitCode > 0) {
6995
6995
  reject(new Error(`Exited with code ${exitCode}`));
6996
6996
  return;
6997
6997
  }
6998
- resolve(subprocess);
6998
+ resolve2(subprocess);
6999
6999
  });
7000
7000
  });
7001
7001
  }
@@ -7099,7 +7099,7 @@ init_source();
7099
7099
 
7100
7100
  // src/commands/login.ts
7101
7101
  init_source();
7102
- var import_node_crypto = __toESM(require("node:crypto"));
7102
+ var import_node_crypto2 = __toESM(require("node:crypto"));
7103
7103
  var import_node_http = __toESM(require("node:http"));
7104
7104
  var import_node_os5 = __toESM(require("node:os"));
7105
7105
  var import_node_readline2 = __toESM(require("node:readline"));
@@ -7112,7 +7112,7 @@ var import_node_os3 = __toESM(require("node:os"));
7112
7112
  // package.json
7113
7113
  var package_default = {
7114
7114
  name: "@drip-apex/apex-cli",
7115
- version: "0.6.1",
7115
+ version: "0.6.3",
7116
7116
  license: "MIT",
7117
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7118
7118
  private: false,
@@ -7586,6 +7586,9 @@ function findConfigPath() {
7586
7586
  return null;
7587
7587
  }
7588
7588
 
7589
+ // src/api.ts
7590
+ var import_node_crypto = require("node:crypto");
7591
+
7589
7592
  // ../api-client/src/http.ts
7590
7593
  var APEX_API_VERSION_HEADER = "x-apex-api-version";
7591
7594
  var SUPPORTED_APEX_API_VERSION = "1";
@@ -7619,8 +7622,8 @@ function createHttpClient(creds, options) {
7619
7622
  warnedAboutVersionSkew = true;
7620
7623
  warn3(version > 1 ? "CLI is behind the server \u2014 update.\n" : "Server API is older than this CLI supports.\n");
7621
7624
  }
7622
- async function request2(method, path23, body5, requestTimeoutMs, form = false) {
7623
- const mock = options.transport?.(method, path23, body5);
7625
+ async function request2(method, path24, body5, requestTimeoutMs, form = false) {
7626
+ const mock = options.transport?.(method, path24, body5);
7624
7627
  if (mock !== void 0) {
7625
7628
  const version = options.testTransportVersion?.();
7626
7629
  if (version) inspectVersion(new Headers({ [APEX_API_VERSION_HEADER]: version }));
@@ -7638,7 +7641,7 @@ function createHttpClient(creds, options) {
7638
7641
  }
7639
7642
  let response;
7640
7643
  try {
7641
- response = await fetch(`${creds.baseUrl}${path23}`, { method, headers, body: form ? body5 : body5 ? JSON.stringify(body5) : void 0, signal: AbortSignal.timeout(timeoutMs) });
7644
+ response = await fetch(`${creds.baseUrl}${path24}`, { method, headers, body: form ? body5 : body5 ? JSON.stringify(body5) : void 0, signal: AbortSignal.timeout(timeoutMs) });
7642
7645
  } catch (error) {
7643
7646
  return options.onNetworkError({ baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) });
7644
7647
  }
@@ -7647,7 +7650,7 @@ function createHttpClient(creds, options) {
7647
7650
  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") } };
7648
7651
  return { ok: true, status: response.status, data: json };
7649
7652
  }
7650
- return { request: (method, path23, body5, timeoutMs) => request2(method, path23, body5, timeoutMs), requestForm: (method, path23, body5) => request2(method, path23, body5, void 0, true) };
7653
+ return { request: (method, path24, body5, timeoutMs) => request2(method, path24, body5, timeoutMs), requestForm: (method, path24, body5) => request2(method, path24, body5, void 0, true) };
7651
7654
  }
7652
7655
 
7653
7656
  // src/api.ts
@@ -7674,11 +7677,11 @@ function getUserKeyIdentity(creds) {
7674
7677
  function getAccountContext(creds) {
7675
7678
  return userRequest(creds, "GET", "/api/v1/me/context");
7676
7679
  }
7677
- function userRequest(creds, method, path23, body5) {
7680
+ function userRequest(creds, method, path24, body5) {
7678
7681
  return request(
7679
7682
  { ...creds, apiKey: creds.userApiKey ?? "", baseUrl: creds.userBaseUrl ?? creds.baseUrl },
7680
7683
  method,
7681
- path23,
7684
+ path24,
7682
7685
  body5
7683
7686
  );
7684
7687
  }
@@ -7724,11 +7727,11 @@ function sharedClient(creds) {
7724
7727
  }
7725
7728
  return client;
7726
7729
  }
7727
- async function request(creds, method, path23, body5, requestTimeoutMs) {
7728
- return sharedClient(creds).request(method, path23, body5, requestTimeoutMs);
7730
+ async function request(creds, method, path24, body5, requestTimeoutMs) {
7731
+ return sharedClient(creds).request(method, path24, body5, requestTimeoutMs);
7729
7732
  }
7730
- async function requestForm(creds, method, path23, body5) {
7731
- return sharedClient(creds).requestForm(method, path23, body5);
7733
+ async function requestForm(creds, method, path24, body5) {
7734
+ return sharedClient(creds).requestForm(method, path24, body5);
7732
7735
  }
7733
7736
  function listExperiments(creds, opts) {
7734
7737
  const params = new URLSearchParams();
@@ -7802,6 +7805,12 @@ function getExperimentQaRun(creds, id) {
7802
7805
  function updateExperimentQaRun(creds, id, body5) {
7803
7806
  return request(creds, "PATCH", `/api/v1/experiments/${id}/qa/run`, body5);
7804
7807
  }
7808
+ function attachExperimentQaEvidence(creds, id, body5) {
7809
+ return request(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, body5);
7810
+ }
7811
+ function uploadExperimentQaEvidence(creds, id, form) {
7812
+ return requestForm(creds, "POST", `/api/v1/experiments/${id}/qa/evidence`, form);
7813
+ }
7805
7814
  function approveExperimentQa(creds, id) {
7806
7815
  return request(creds, "PATCH", `/api/v1/experiments/${id}/qa`);
7807
7816
  }
@@ -7959,12 +7968,17 @@ function testTargeting(creds, url, opts) {
7959
7968
  return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
7960
7969
  }
7961
7970
  function createSos(creds, body5) {
7962
- return request(
7963
- creds,
7964
- "POST",
7965
- "/api/v1/sos",
7966
- body5
7967
- );
7971
+ const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto.createHash)("sha256").update(
7972
+ [
7973
+ body5.type,
7974
+ body5.experiment_id ?? "",
7975
+ body5.description,
7976
+ body5.sub_class ?? "",
7977
+ body5.loom_url ?? "",
7978
+ String(Math.floor(Date.now() / 36e5))
7979
+ ].join("\0")
7980
+ ).digest("hex");
7981
+ return request(creds, "POST", "/api/v1/sos", { ...body5, idempotency_key });
7968
7982
  }
7969
7983
  function listMySos(creds) {
7970
7984
  return request(creds, "GET", "/api/v1/sos/mine");
@@ -8086,16 +8100,30 @@ function getAdminQuarantine(creds) {
8086
8100
  "/api/v1/admin/tracking/quarantine"
8087
8101
  );
8088
8102
  }
8103
+ function archiveAdminShop(creds, shopId) {
8104
+ return request(
8105
+ userCredentials(creds),
8106
+ "POST",
8107
+ `/api/v1/admin/shops/${encodeURIComponent(shopId)}/archive`
8108
+ );
8109
+ }
8110
+ function restoreAdminShop(creds, shopId) {
8111
+ return request(
8112
+ userCredentials(creds),
8113
+ "POST",
8114
+ `/api/v1/admin/shops/${encodeURIComponent(shopId)}/restore`
8115
+ );
8116
+ }
8089
8117
  function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor) {
8090
8118
  const params = new URLSearchParams();
8091
8119
  if (limit) params.set("limit", String(limit));
8092
8120
  if (cursor) params.set("cursor", cursor);
8093
8121
  const query = params.toString();
8094
- const path23 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8095
- return request(creds, method, path23, body5);
8122
+ const path24 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8123
+ return request(creds, method, path24, body5);
8096
8124
  }
8097
- function managedPath(kind5, id, action70) {
8098
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action70 ? `/${action70}` : ""}`;
8125
+ function managedPath(kind5, id, action71) {
8126
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action71 ? `/${action71}` : ""}`;
8099
8127
  }
8100
8128
  function listManagedDeliveries(creds, kind5, limit) {
8101
8129
  const q = limit ? `?limit=${limit}` : "";
@@ -8113,8 +8141,8 @@ function createManagedDelivery(creds, kind5, body5) {
8113
8141
  function updateManagedDelivery(creds, kind5, id, body5) {
8114
8142
  return request(creds, "PATCH", managedPath(kind5, id), body5);
8115
8143
  }
8116
- function transitionManagedDelivery(creds, kind5, id, action70) {
8117
- return request(creds, "POST", managedPath(kind5, id, action70));
8144
+ function transitionManagedDelivery(creds, kind5, id, action71) {
8145
+ return request(creds, "POST", managedPath(kind5, id, action71));
8118
8146
  }
8119
8147
  function listConfigResources(creds, family, opts) {
8120
8148
  const params = new URLSearchParams();
@@ -8341,9 +8369,9 @@ function resolveSelection(tree, input) {
8341
8369
  function ask(question) {
8342
8370
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
8343
8371
  return new Promise(
8344
- (resolve) => rl.question(question, (answer) => {
8372
+ (resolve2) => rl.question(question, (answer) => {
8345
8373
  rl.close();
8346
- resolve(answer.trim());
8374
+ resolve2(answer.trim());
8347
8375
  })
8348
8376
  );
8349
8377
  }
@@ -8421,7 +8449,7 @@ function prompt(question, hide = false) {
8421
8449
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
8422
8450
  throw new Error("Interactive input is unavailable in non-TTY mode.");
8423
8451
  }
8424
- return new Promise((resolve) => {
8452
+ return new Promise((resolve2) => {
8425
8453
  const rl = import_node_readline2.default.createInterface({
8426
8454
  input: process.stdin,
8427
8455
  output: process.stderr
@@ -8441,13 +8469,13 @@ function prompt(question, hide = false) {
8441
8469
  stdout.write = originalWrite;
8442
8470
  console.error();
8443
8471
  rl.close();
8444
- resolve(answer.trim());
8472
+ resolve2(answer.trim());
8445
8473
  });
8446
8474
  masking = true;
8447
8475
  } else {
8448
8476
  rl.question(question, (answer) => {
8449
8477
  rl.close();
8450
- resolve(answer.trim());
8478
+ resolve2(answer.trim());
8451
8479
  });
8452
8480
  }
8453
8481
  });
@@ -8471,9 +8499,9 @@ function parseFormBody(body5) {
8471
8499
  return new URLSearchParams(body5);
8472
8500
  }
8473
8501
  async function waitForBrowserLogin(baseUrl, userMode = false) {
8474
- const state = import_node_crypto.default.randomBytes(24).toString("base64url");
8502
+ const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
8475
8503
  const clientName = import_node_os5.default.hostname() || "Local machine";
8476
- return new Promise((resolve, reject) => {
8504
+ return new Promise((resolve2, reject) => {
8477
8505
  let settled = false;
8478
8506
  let timeout = null;
8479
8507
  const finish = (err, apiKey) => {
@@ -8485,7 +8513,7 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
8485
8513
  } catch {
8486
8514
  }
8487
8515
  if (err) reject(err);
8488
- else resolve(apiKey ?? "");
8516
+ else resolve2(apiKey ?? "");
8489
8517
  };
8490
8518
  const server = import_node_http.default.createServer((req, res) => {
8491
8519
  if (req.method === "OPTIONS") {
@@ -8803,21 +8831,23 @@ var useCommand = new Command("use").description("Set the current Apex organizati
8803
8831
 
8804
8832
  // src/commands/experiments.ts
8805
8833
  init_source();
8806
- var import_node_crypto2 = __toESM(require("node:crypto"));
8807
- var import_node_fs6 = __toESM(require("node:fs"));
8808
- var import_node_path4 = __toESM(require("node:path"));
8834
+ var import_node_crypto4 = __toESM(require("node:crypto"));
8835
+ var import_node_fs7 = __toESM(require("node:fs"));
8836
+ var import_node_path5 = __toESM(require("node:path"));
8809
8837
 
8810
8838
  // ../api-client/src/qa-preview-url.ts
8839
+ var FORCE_ALIAS_PARAM_NAMES = ["drip_experiment_id", "drip_experiment", "drip_variation_id", "drip_variation"];
8811
8840
  function applyQaPreviewParams(url, params) {
8812
- url.searchParams.set("drip_force", `${params.experimentId}:${params.variationId}`);
8813
- url.searchParams.set("drip_experiment_id", params.experimentId);
8814
- url.searchParams.set("drip_variation_id", params.variationId);
8841
+ url.searchParams.delete("drip_force");
8842
+ for (const name of FORCE_ALIAS_PARAM_NAMES) url.searchParams.delete(name);
8815
8843
  url.searchParams.set("drip_qa", "1");
8816
8844
  if (params.debug !== false) url.searchParams.set("drip_debug", "1");
8817
8845
  if (params.devtools) url.searchParams.set("drip_devtools", "1");
8818
8846
  if (params.screenshot) url.searchParams.set("drip_screenshot", "1");
8819
8847
  if (params.qaSessionId) url.searchParams.set("drip_qa_session", params.qaSessionId);
8820
8848
  if (params.harnessToken) url.searchParams.set("drip_harness", params.harnessToken);
8849
+ const force = `${params.experimentPublicId ?? params.experimentId}:${params.variationPublicId ?? params.variationId}`;
8850
+ url.search += `${url.search ? "&" : "?"}drip_force=${force}`;
8821
8851
  return url;
8822
8852
  }
8823
8853
  function buildQaPreviewUrl(experiment, options = {}) {
@@ -8825,7 +8855,7 @@ function buildQaPreviewUrl(experiment, options = {}) {
8825
8855
  const target = options.storeUrl ?? experiment.store_url;
8826
8856
  if (!target || !variation) return null;
8827
8857
  try {
8828
- return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, ...options });
8858
+ return applyQaPreviewParams(new URL(target), { experimentId: experiment.id, variationId: variation.id, experimentPublicId: experiment.public_id, variationPublicId: variation.public_id, ...options });
8829
8859
  } catch {
8830
8860
  return null;
8831
8861
  }
@@ -8842,6 +8872,192 @@ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
8842
8872
  return url;
8843
8873
  }
8844
8874
 
8875
+ // src/lib/asset-upload.ts
8876
+ var import_node_crypto3 = __toESM(require("node:crypto"));
8877
+ var import_node_fs6 = __toESM(require("node:fs"));
8878
+ var import_node_path4 = __toESM(require("node:path"));
8879
+ var INLINE_DATA_URI_MAX_CHARS = 4096;
8880
+ var DATA_IMAGE_URI_PATTERN = /data:image\/[a-z0-9.+-]+(?:(?:;[^;,\s"'`]*)*;base64,[A-Za-z0-9+/=\s]+|(?:;[^,\s"'`]*)?,[^\s"'`]+)/gi;
8881
+ var QUOTED_LOCAL_IMAGE_PATTERN = /(["'])([^"'\n]+?\.(?:png|jpe?g|gif|webp|svg|avif))\1/gi;
8882
+ var LOCAL_IMAGE_PATTERN = /(?:\.{1,2}\/)?[a-z0-9@_%+.,~/-]+\.(?:png|jpe?g|gif|webp|svg|avif)/gi;
8883
+ var MIME_BY_EXTENSION = {
8884
+ ".png": "image/png",
8885
+ ".jpg": "image/jpeg",
8886
+ ".jpeg": "image/jpeg",
8887
+ ".gif": "image/gif",
8888
+ ".webp": "image/webp",
8889
+ ".svg": "image/svg+xml",
8890
+ ".avif": "image/avif"
8891
+ };
8892
+ var EXTENSION_BY_MIME = {
8893
+ "image/png": ".png",
8894
+ "image/jpeg": ".jpg",
8895
+ "image/gif": ".gif",
8896
+ "image/webp": ".webp",
8897
+ "image/svg+xml": ".svg",
8898
+ "image/avif": ".avif"
8899
+ };
8900
+ function contentHash(bytes) {
8901
+ return import_node_crypto3.default.createHash("sha256").update(bytes).digest("hex");
8902
+ }
8903
+ function deliveryUrl(baseUrl, asset) {
8904
+ const returnedUrl = asset.deliveryUrl ?? asset.delivery_url ?? asset.url;
8905
+ if (returnedUrl) return returnedUrl;
8906
+ const origin = new URL(baseUrl).origin;
8907
+ return `${origin}/api/assets/${encodeURIComponent(asset.id)}/file`;
8908
+ }
8909
+ function extensionForMimeType(mimeType) {
8910
+ const known = EXTENSION_BY_MIME[mimeType];
8911
+ if (known) return known;
8912
+ const subtype = mimeType.split("/", 2)[1]?.toLowerCase();
8913
+ const safeSubtype = subtype?.replace(/[^a-z0-9.+-]/g, "");
8914
+ return safeSubtype ? `.${safeSubtype}` : ".bin";
8915
+ }
8916
+ function dataUriFromMatch(value, match2) {
8917
+ let uri = match2[0];
8918
+ const start = match2.index ?? 0;
8919
+ if (/url\(\s*$/i.test(value.slice(0, start))) {
8920
+ const closingParenthesis = uri.indexOf(")");
8921
+ if (closingParenthesis >= 0) uri = uri.slice(0, closingParenthesis);
8922
+ }
8923
+ return uri;
8924
+ }
8925
+ function surroundingNonWhitespaceNonQuoteRun(value, start, end) {
8926
+ const before = value.slice(0, start).match(/[^\s"']*$/)?.[0] ?? "";
8927
+ const after = value.slice(end).match(/^[^\s"']*/)?.[0] ?? "";
8928
+ return `${before}${value.slice(start, end)}${after}`;
8929
+ }
8930
+ function isInsideRange(start, end, ranges) {
8931
+ return ranges.some((range) => start >= range.start && end <= range.end);
8932
+ }
8933
+ function applyReplacements(value, replacements) {
8934
+ let rewritten = value;
8935
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
8936
+ rewritten = rewritten.slice(0, replacement.start) + replacement.value + rewritten.slice(replacement.end);
8937
+ }
8938
+ return rewritten;
8939
+ }
8940
+ function shouldRewriteExperimentFilesystemAssetReferences(filePath) {
8941
+ const normalized = filePath.replace(/\\/g, "/");
8942
+ if (/^variations\/[^/]+\/.+\.(?:js|ts|css|scss|html|json)$/i.test(normalized)) {
8943
+ return true;
8944
+ }
8945
+ return /^(?:experiment\/index\.(?:js|css)|experiment\/reset\.js)$/i.test(normalized);
8946
+ }
8947
+ function createAssetReferenceRewriter(options) {
8948
+ const uploadsByHash = /* @__PURE__ */ new Map();
8949
+ const uploadOnce = async (file, source) => {
8950
+ const hash = contentHash(file.bytes);
8951
+ let pending = uploadsByHash.get(hash);
8952
+ if (!pending) {
8953
+ pending = options.upload(file).then((asset) => {
8954
+ if (!asset.id) throw new Error(`Asset upload for ${file.name} returned no ID.`);
8955
+ const url = deliveryUrl(options.baseUrl, asset);
8956
+ options.onUpload?.({ name: file.name, url, source });
8957
+ return url;
8958
+ });
8959
+ uploadsByHash.set(hash, pending);
8960
+ pending.catch(() => uploadsByHash.delete(hash));
8961
+ }
8962
+ return pending;
8963
+ };
8964
+ const rewrite = async (value, baseDir = options.baseDir) => {
8965
+ if (options.enabled === false || !value) return value;
8966
+ let rewritten = value;
8967
+ const inlineMatches = [...rewritten.matchAll(DATA_IMAGE_URI_PATTERN)].map((match2) => ({ match: match2, uri: dataUriFromMatch(rewritten, match2) })).filter(({ uri }) => uri.length > INLINE_DATA_URI_MAX_CHARS);
8968
+ for (const { uri } of inlineMatches) {
8969
+ const commaIndex = uri.indexOf(",");
8970
+ const metadata = uri.slice("data:".length, commaIndex);
8971
+ const mimeType = metadata.split(";", 1)[0].toLowerCase();
8972
+ const payload = uri.slice(commaIndex + 1);
8973
+ let bytes;
8974
+ try {
8975
+ bytes = /;base64$/i.test(metadata) ? Buffer.from(payload.replace(/\s/g, ""), "base64") : Buffer.from(decodeURIComponent(payload), "utf8");
8976
+ } catch {
8977
+ continue;
8978
+ }
8979
+ const hash = contentHash(bytes);
8980
+ const name = `inline-${hash.slice(0, 12)}${extensionForMimeType(mimeType)}`;
8981
+ const url = await uploadOnce({ name, bytes, type: mimeType }, "inline");
8982
+ rewritten = rewritten.split(uri).join(url);
8983
+ }
8984
+ const localAssetUrl = async (reference) => {
8985
+ if (reference.startsWith("/") || reference.startsWith("data:") || reference.includes("://")) {
8986
+ return null;
8987
+ }
8988
+ const absolutePath = import_node_path4.default.resolve(baseDir, reference);
8989
+ let realBaseDir;
8990
+ let realPath;
8991
+ try {
8992
+ realBaseDir = import_node_fs6.default.realpathSync(baseDir);
8993
+ realPath = import_node_fs6.default.realpathSync(absolutePath);
8994
+ } catch {
8995
+ return null;
8996
+ }
8997
+ const relativeRealPath = import_node_path4.default.relative(realBaseDir, realPath);
8998
+ if (relativeRealPath.startsWith("..") || import_node_path4.default.isAbsolute(relativeRealPath)) return null;
8999
+ if (!import_node_fs6.default.statSync(realPath).isFile()) return null;
9000
+ const bytes = import_node_fs6.default.readFileSync(realPath);
9001
+ const extension = import_node_path4.default.extname(realPath).toLowerCase();
9002
+ return uploadOnce(
9003
+ {
9004
+ name: import_node_path4.default.basename(absolutePath),
9005
+ bytes,
9006
+ type: MIME_BY_EXTENSION[extension]
9007
+ },
9008
+ "local"
9009
+ );
9010
+ };
9011
+ const quotedReplacements = [];
9012
+ for (const match2 of rewritten.matchAll(QUOTED_LOCAL_IMAGE_PATTERN)) {
9013
+ const reference = match2[2];
9014
+ const matchStart = match2.index ?? 0;
9015
+ const url = await localAssetUrl(reference);
9016
+ if (!url) continue;
9017
+ const start = matchStart + match2[1].length;
9018
+ quotedReplacements.push({ start, end: start + reference.length, value: url });
9019
+ }
9020
+ rewritten = applyReplacements(rewritten, quotedReplacements);
9021
+ const quotedRanges = [...rewritten.matchAll(QUOTED_LOCAL_IMAGE_PATTERN)].map((match2) => ({
9022
+ start: match2.index ?? 0,
9023
+ end: (match2.index ?? 0) + match2[0].length
9024
+ }));
9025
+ const localMatches = [...rewritten.matchAll(LOCAL_IMAGE_PATTERN)];
9026
+ const bareReplacements = [];
9027
+ for (const match2 of localMatches) {
9028
+ const reference = match2[0];
9029
+ const start = match2.index ?? 0;
9030
+ const end = start + reference.length;
9031
+ const surroundingRun = surroundingNonWhitespaceNonQuoteRun(rewritten, start, end);
9032
+ if (reference.startsWith("/") || reference.startsWith("data:") || isInsideRange(start, end, quotedRanges) || surroundingRun.includes("://") || surroundingRun.startsWith("//")) {
9033
+ continue;
9034
+ }
9035
+ const url = await localAssetUrl(reference);
9036
+ if (!url) continue;
9037
+ bareReplacements.push({ start, end, value: url });
9038
+ }
9039
+ return applyReplacements(rewritten, bareReplacements);
9040
+ };
9041
+ const rewriteUnknown = async (value, baseDir = options.baseDir) => {
9042
+ if (typeof value === "string") return rewrite(value, baseDir);
9043
+ if (Array.isArray(value)) {
9044
+ return Promise.all(value.map((entry) => rewriteUnknown(entry, baseDir)));
9045
+ }
9046
+ if (value !== null && typeof value === "object") {
9047
+ return Object.fromEntries(
9048
+ await Promise.all(
9049
+ Object.entries(value).map(async ([key, entry]) => [
9050
+ key,
9051
+ await rewriteUnknown(entry, baseDir)
9052
+ ])
9053
+ )
9054
+ );
9055
+ }
9056
+ return value;
9057
+ };
9058
+ return { rewrite, rewriteUnknown };
9059
+ }
9060
+
8845
9061
  // src/commands/experiments.ts
8846
9062
  function formatDate(iso) {
8847
9063
  return new Date(iso).toLocaleDateString("en-US", {
@@ -8878,7 +9094,19 @@ function confidenceBasisLabel(value) {
8878
9094
  function statisticsStateLabel(value) {
8879
9095
  return value === "untrustworthy" ? "verdict withheld" : String(value ?? "unknown");
8880
9096
  }
8881
- function printExperimentResults(results2) {
9097
+ function formatRevenue(value, currency) {
9098
+ if (value === null || value === void 0) return "\u2014";
9099
+ if (!currency || typeof value !== "number" || !Number.isFinite(value)) return String(value);
9100
+ try {
9101
+ return new Intl.NumberFormat("en-US", {
9102
+ style: "currency",
9103
+ currency: currency.trim().toUpperCase()
9104
+ }).format(value);
9105
+ } catch {
9106
+ return String(value);
9107
+ }
9108
+ }
9109
+ function printExperimentResults(results2, currency) {
8882
9110
  const experiment = results2.experiment ?? {};
8883
9111
  const totals = results2.totals ?? {};
8884
9112
  const decision = results2.decision ?? {};
@@ -8892,7 +9120,7 @@ function printExperimentResults(results2) {
8892
9120
  console.log(` Status: ${statusBadge(String(experiment.status ?? "unknown"))}`);
8893
9121
  console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
8894
9122
  console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
8895
- console.log(` Revenue: ${String(totals.revenue ?? 0)}`);
9123
+ console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
8896
9124
  console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
8897
9125
  console.log(` Statistics: ${statisticsStateLabel(statisticsState)}`);
8898
9126
  if (decision.treatmentName) {
@@ -8915,7 +9143,7 @@ function printExperimentResults(results2) {
8915
9143
  String(arm.visitors ?? 0),
8916
9144
  String(arm.primaryConversions ?? 0),
8917
9145
  typeof arm.primaryRate === "number" ? `${(arm.primaryRate * 100).toFixed(2)}%` : "-",
8918
- String(arm.revenue ?? 0)
9146
+ formatRevenue(arm.revenue, currency)
8919
9147
  ]),
8920
9148
  [32, 8, 6, 8, 10]
8921
9149
  );
@@ -8941,45 +9169,46 @@ var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
8941
9169
  "anti-flicker.json"
8942
9170
  ]
8943
9171
  };
9172
+ var SERVER_OWNED_EXPERIMENT_FILES = /* @__PURE__ */ new Set(["qa.json", "launch.json"]);
8944
9173
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8945
9174
  var pathAlternation = (paths) => paths.map(escapeRegex).join("|");
8946
9175
  var FILESYSTEM_FILE_RE = new RegExp(
8947
9176
  `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
8948
9177
  );
8949
9178
  function sha256Text(value) {
8950
- return import_node_crypto2.default.createHash("sha256").update(value).digest("hex");
9179
+ return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
8951
9180
  }
8952
9181
  function folderPath(folder) {
8953
- return import_node_path4.default.resolve(process.cwd(), folder);
9182
+ return import_node_path5.default.resolve(process.cwd(), folder);
8954
9183
  }
8955
9184
  function filesystemApexDir(folder) {
8956
- return import_node_path4.default.join(folder, ".apex");
9185
+ return import_node_path5.default.join(folder, ".apex");
8957
9186
  }
8958
9187
  function filesystemSessionPath(folder) {
8959
- return import_node_path4.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
9188
+ return import_node_path5.default.join(filesystemApexDir(folder), "experiment-filesystem.json");
8960
9189
  }
8961
9190
  function writeFilesystemSession(folder, session) {
8962
- import_node_fs6.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
8963
- import_node_fs6.default.writeFileSync(
8964
- import_node_path4.default.join(filesystemApexDir(folder), ".gitignore"),
9191
+ import_node_fs7.default.mkdirSync(filesystemApexDir(folder), { recursive: true });
9192
+ import_node_fs7.default.writeFileSync(
9193
+ import_node_path5.default.join(filesystemApexDir(folder), ".gitignore"),
8965
9194
  "experiment-filesystem.json\n",
8966
9195
  "utf8"
8967
9196
  );
8968
- import_node_fs6.default.writeFileSync(filesystemSessionPath(folder), `${JSON.stringify(session, null, 2)}
9197
+ import_node_fs7.default.writeFileSync(filesystemSessionPath(folder), `${JSON.stringify(session, null, 2)}
8969
9198
  `, "utf8");
8970
9199
  }
8971
9200
  function readFilesystemSession(folder) {
8972
9201
  const filePath = filesystemSessionPath(folder);
8973
- if (!import_node_fs6.default.existsSync(filePath)) {
8974
- const workspaceMarker = import_node_path4.default.join(filesystemApexDir(folder), "session.json");
8975
- if (import_node_fs6.default.existsSync(workspaceMarker)) {
9202
+ if (!import_node_fs7.default.existsSync(filePath)) {
9203
+ const workspaceMarker = import_node_path5.default.join(filesystemApexDir(folder), "session.json");
9204
+ if (import_node_fs7.default.existsSync(workspaceMarker)) {
8976
9205
  throw new Error(
8977
9206
  "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."
8978
9207
  );
8979
9208
  }
8980
9209
  throw new Error(`Missing ${filePath}. Run apex experiments pull first.`);
8981
9210
  }
8982
- return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
9211
+ return JSON.parse(import_node_fs7.default.readFileSync(filePath, "utf8"));
8983
9212
  }
8984
9213
  function validateFilesystemPath(relativePath) {
8985
9214
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -8995,42 +9224,87 @@ function writeExperimentFilesystemFiles(folder, files) {
8995
9224
  }));
8996
9225
  for (const file of validatedFiles) {
8997
9226
  const relativePath = file.relativePath;
8998
- const absPath = import_node_path4.default.join(folder, relativePath);
8999
- import_node_fs6.default.mkdirSync(import_node_path4.default.dirname(absPath), { recursive: true });
9000
- import_node_fs6.default.writeFileSync(absPath, file.content, "utf8");
9227
+ const absPath = import_node_path5.default.join(folder, relativePath);
9228
+ import_node_fs7.default.mkdirSync(import_node_path5.default.dirname(absPath), { recursive: true });
9229
+ import_node_fs7.default.writeFileSync(absPath, file.content, "utf8");
9001
9230
  }
9002
9231
  }
9003
- function listExperimentFilesystemFiles(folder) {
9232
+ function listExperimentFilesystemFiles(folder, session) {
9004
9233
  const files = [];
9005
9234
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel) {
9006
- const absPath = import_node_path4.default.join(folder, fileName);
9007
- if (!import_node_fs6.default.existsSync(absPath)) continue;
9008
- const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9009
- files.push({ path: fileName, content, sha256: sha256Text(content) });
9235
+ const absPath = import_node_path5.default.join(folder, fileName);
9236
+ if (!import_node_fs7.default.existsSync(absPath)) continue;
9237
+ const content = import_node_fs7.default.readFileSync(absPath, "utf8");
9238
+ const sha256 = sha256Text(content);
9239
+ if (SERVER_OWNED_EXPERIMENT_FILES.has(fileName)) {
9240
+ if (session.files[fileName]?.sha256 !== sha256) {
9241
+ console.error(
9242
+ `${fileName} is server-owned; local edits are ignored (dashboard flow owns QA state)`
9243
+ );
9244
+ }
9245
+ continue;
9246
+ }
9247
+ files.push({ path: fileName, content, sha256 });
9010
9248
  }
9011
9249
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
9012
9250
  const relativePath = `experiment/${fileName}`;
9013
- const absPath = import_node_path4.default.join(folder, relativePath);
9014
- if (!import_node_fs6.default.existsSync(absPath)) continue;
9015
- const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9251
+ const absPath = import_node_path5.default.join(folder, relativePath);
9252
+ if (!import_node_fs7.default.existsSync(absPath)) continue;
9253
+ const content = import_node_fs7.default.readFileSync(absPath, "utf8");
9016
9254
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
9017
9255
  }
9018
- const variationsRoot = import_node_path4.default.join(folder, "variations");
9019
- if (import_node_fs6.default.existsSync(variationsRoot)) {
9020
- for (const variationDir of import_node_fs6.default.readdirSync(variationsRoot, { withFileTypes: true })) {
9256
+ const variationsRoot = import_node_path5.default.join(folder, "variations");
9257
+ if (import_node_fs7.default.existsSync(variationsRoot)) {
9258
+ for (const variationDir of import_node_fs7.default.readdirSync(variationsRoot, { withFileTypes: true })) {
9021
9259
  if (!variationDir.isDirectory()) continue;
9022
- const base = import_node_path4.default.join(variationsRoot, variationDir.name);
9260
+ const base = import_node_path5.default.join(variationsRoot, variationDir.name);
9023
9261
  for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation) {
9024
- const absPath = import_node_path4.default.join(base, fileName);
9025
- if (!import_node_fs6.default.existsSync(absPath)) continue;
9026
- const relativePath = validateFilesystemPath(import_node_path4.default.relative(folder, absPath));
9027
- const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9262
+ const absPath = import_node_path5.default.join(base, fileName);
9263
+ if (!import_node_fs7.default.existsSync(absPath)) continue;
9264
+ const relativePath = validateFilesystemPath(import_node_path5.default.relative(folder, absPath));
9265
+ const content = import_node_fs7.default.readFileSync(absPath, "utf8");
9028
9266
  files.push({ path: relativePath, content, sha256: sha256Text(content) });
9029
9267
  }
9030
9268
  }
9031
9269
  }
9032
9270
  return files.sort((left, right) => left.path.localeCompare(right.path));
9033
9271
  }
9272
+ async function rewriteExperimentFilesystemAssets(input) {
9273
+ const rewriter = createAssetReferenceRewriter({
9274
+ baseDir: input.folder,
9275
+ baseUrl: input.baseUrl,
9276
+ enabled: input.enabled,
9277
+ upload: async (file) => {
9278
+ const response = await input.upload(input.credentials, [file]);
9279
+ const asset = response.data?.assets?.[0];
9280
+ if (!response.ok || !asset?.id) {
9281
+ throw new Error(response.error ?? `Failed to upload ${file.name}.`);
9282
+ }
9283
+ return {
9284
+ id: asset.id,
9285
+ url: typeof asset.url === "string" ? asset.url : void 0,
9286
+ deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : void 0,
9287
+ delivery_url: typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
9288
+ };
9289
+ },
9290
+ onUpload: ({ name, url }) => {
9291
+ process.stderr.write(`Uploaded asset ${name} -> ${url}
9292
+ `);
9293
+ }
9294
+ });
9295
+ return Promise.all(
9296
+ input.files.map(async (file) => {
9297
+ if (!shouldRewriteExperimentFilesystemAssetReferences(file.path)) {
9298
+ return file;
9299
+ }
9300
+ const content = await rewriter.rewrite(
9301
+ file.content,
9302
+ import_node_path5.default.dirname(import_node_path5.default.join(input.folder, file.path))
9303
+ );
9304
+ return { ...file, content, sha256: sha256Text(content) };
9305
+ })
9306
+ );
9307
+ }
9034
9308
  function printFilesystemPlan(plan) {
9035
9309
  if (plan.errors.length > 0) {
9036
9310
  console.log(source_default.red("\nErrors"));
@@ -9123,6 +9397,15 @@ function printQaRun(run3) {
9123
9397
  console.log(` - ${url.label}: ${source_default.cyan(url.url)}`);
9124
9398
  });
9125
9399
  }
9400
+ if ((run3.evidenceRecords ?? []).length > 0) {
9401
+ console.log(source_default.bold("\nEvidence records"));
9402
+ for (const record2 of run3.evidenceRecords ?? []) {
9403
+ const device = record2.device ? ` \xB7 ${record2.device}` : "";
9404
+ console.log(
9405
+ ` - ${record2.kind} \xB7 ${record2.label} \xB7 ${record2.status}${device} \xB7 ${record2.capturedAt}`
9406
+ );
9407
+ }
9408
+ }
9126
9409
  console.log();
9127
9410
  }
9128
9411
  async function confirmApply(message) {
@@ -9131,8 +9414,8 @@ async function confirmApply(message) {
9131
9414
  }
9132
9415
  const readline3 = await import("node:readline");
9133
9416
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
9134
- const answer = await new Promise((resolve) => {
9135
- rl.question(source_default.yellow(`${message} [y/N] `), resolve);
9417
+ const answer = await new Promise((resolve2) => {
9418
+ rl.question(source_default.yellow(`${message} [y/N] `), resolve2);
9136
9419
  });
9137
9420
  rl.close();
9138
9421
  return answer.toLowerCase() === "y";
@@ -9140,7 +9423,7 @@ async function confirmApply(message) {
9140
9423
  function parseEvidenceInput(value) {
9141
9424
  if (!value) return void 0;
9142
9425
  const source = value.startsWith("@") ? value.slice(1) : value;
9143
- 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;
9426
+ const text = value.startsWith("@") || import_node_fs7.default.existsSync(source) ? import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), source), "utf8") : value;
9144
9427
  const parsed = JSON.parse(text);
9145
9428
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9146
9429
  throw new Error("QA evidence must be a JSON object");
@@ -9164,7 +9447,7 @@ async function resolveExperimentForFilesystemOrId(value) {
9164
9447
  return {
9165
9448
  experimentId: session.experimentId,
9166
9449
  folder: candidate,
9167
- label: import_node_path4.default.basename(candidate)
9450
+ label: import_node_path5.default.basename(candidate)
9168
9451
  };
9169
9452
  }
9170
9453
  const exp = await resolveExperiment(value);
@@ -9190,6 +9473,27 @@ async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experime
9190
9473
  } catch {
9191
9474
  }
9192
9475
  }
9476
+ async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
9477
+ if (!folder) return;
9478
+ try {
9479
+ const refreshed = await getExperimentFilesystem(creds, experimentId);
9480
+ if (!refreshed.ok || !refreshed.data) return;
9481
+ const serverOwned = refreshed.data.files.filter(
9482
+ (file) => SERVER_OWNED_EXPERIMENT_FILES.has(file.path)
9483
+ );
9484
+ if (serverOwned.length === 0) return;
9485
+ const session = readFilesystemSession(folder);
9486
+ writeExperimentFilesystemFiles(folder, serverOwned);
9487
+ writeFilesystemSession(folder, {
9488
+ ...session,
9489
+ files: {
9490
+ ...session.files,
9491
+ ...Object.fromEntries(serverOwned.map((file) => [file.path, { sha256: file.sha256 }]))
9492
+ }
9493
+ });
9494
+ } catch {
9495
+ }
9496
+ }
9193
9497
  async function resolveExperiment(idOrSlug) {
9194
9498
  const creds = requireCredentials();
9195
9499
  if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
@@ -9282,7 +9586,7 @@ var results = new Command("results").description("Read experiment results and ti
9282
9586
  output(
9283
9587
  this,
9284
9588
  { experiment: exp, results: res.data },
9285
- () => printExperimentResults(res.data)
9589
+ () => printExperimentResults(res.data, opts.currency)
9286
9590
  );
9287
9591
  } catch (error) {
9288
9592
  spinner?.fail("Failed to fetch experiment results");
@@ -9329,7 +9633,7 @@ var create = new Command("create").description("Create a new experiment").requir
9329
9633
  "Redirect treatment destination (repeatable; creates one treatment per URL)",
9330
9634
  collectCreateOption,
9331
9635
  []
9332
- ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async function action6(opts) {
9636
+ ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--backlog-item <id>", "Backlog item UUID or Apex backlog key").action(async function action6(opts) {
9333
9637
  if (opts.type !== "mutation" && opts.type !== "redirect") {
9334
9638
  fail(this, "--type must be mutation or redirect", EXIT_CODES.validation);
9335
9639
  }
@@ -9361,6 +9665,53 @@ var create = new Command("create").description("Create a new experiment").requir
9361
9665
  const destinations = opts.destination.map(parseHttpDestination);
9362
9666
  const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
9363
9667
  const creds = requireCredentials();
9668
+ let backlogItemId;
9669
+ if (opts.backlogItem !== void 0) {
9670
+ const ref = opts.backlogItem.trim();
9671
+ if (!ref) {
9672
+ fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
9673
+ }
9674
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(ref)) {
9675
+ backlogItemId = ref;
9676
+ } else if (/^[A-Za-z0-9]{2,8}-\d+$/i.test(ref)) {
9677
+ let cursor;
9678
+ let resolvedId;
9679
+ do {
9680
+ const page = await listInsights(creds, "backlog", 100, cursor);
9681
+ if (!page.ok || !page.data) {
9682
+ fail(
9683
+ this,
9684
+ page.error ?? `Unable to resolve backlog item "${ref}"`,
9685
+ classifyStatus(page.status),
9686
+ page.errorDetails
9687
+ );
9688
+ }
9689
+ const match2 = page.data.data.find((item) => {
9690
+ const metadata = item.metadata && typeof item.metadata === "object" && !Array.isArray(item.metadata) ? item.metadata : {};
9691
+ return typeof metadata.backlog_key === "string" && metadata.backlog_key.toUpperCase() === ref.toUpperCase();
9692
+ });
9693
+ resolvedId = match2?.id;
9694
+ cursor = page.data.pagination.has_more ? page.data.pagination.next_cursor ?? void 0 : void 0;
9695
+ } while (!resolvedId && cursor);
9696
+ const backlog = resolvedId ? { ok: true, status: 200, data: { id: resolvedId } } : await getInsight(creds, "backlog", ref);
9697
+ if (!backlog.ok || !backlog.data?.id) {
9698
+ fail(
9699
+ this,
9700
+ backlog.error ?? `Backlog item "${ref}" not found`,
9701
+ classifyStatus(backlog.status),
9702
+ backlog.errorDetails
9703
+ );
9704
+ }
9705
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
9706
+ backlog.data.id
9707
+ )) {
9708
+ fail(this, `Backlog item "${ref}" not found`, EXIT_CODES.validation);
9709
+ }
9710
+ backlogItemId = backlog.data.id;
9711
+ } else {
9712
+ fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
9713
+ }
9714
+ }
9364
9715
  const spinner = await createSpinner("Creating experiment...");
9365
9716
  const res = await createExperiment(creds, {
9366
9717
  name: opts.name,
@@ -9368,6 +9719,7 @@ var create = new Command("create").description("Create a new experiment").requir
9368
9719
  description: opts.description ?? null,
9369
9720
  delivery_type: opts.type,
9370
9721
  traffic_allocation: traffic / 100,
9722
+ ...backlogItemId ? { backlog_item_id: backlogItemId } : {},
9371
9723
  ...sourceRules.length > 0 ? {
9372
9724
  store_url: sourceStoreUrl(sourceRules),
9373
9725
  targeting: {
@@ -9403,8 +9755,15 @@ var create = new Command("create").description("Create a new experiment").requir
9403
9755
  res.errorDetails
9404
9756
  );
9405
9757
  }
9406
- spinner?.succeed("Experiment created");
9407
- output(this, { experiment: res.data }, () => printExperiment(res.data));
9758
+ const outcome = res.status === 200 ? "already_linked" : "created";
9759
+ spinner?.succeed(
9760
+ outcome === "created" ? "Experiment created" : `Already linked \u2014 returning existing experiment ${res.data.test_key ?? res.data.slug ?? res.data.id}`
9761
+ );
9762
+ output(
9763
+ this,
9764
+ { outcome, backlog_item_id: backlogItemId ?? null, experiment: res.data },
9765
+ () => printExperiment(res.data)
9766
+ );
9408
9767
  });
9409
9768
  var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
9410
9769
  const creds = requireCredentials();
@@ -9469,8 +9828,8 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9469
9828
  input: process.stdin,
9470
9829
  output: process.stderr
9471
9830
  });
9472
- const answer = await new Promise((resolve) => {
9473
- rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve);
9831
+ const answer = await new Promise((resolve2) => {
9832
+ rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve2);
9474
9833
  });
9475
9834
  rl.close();
9476
9835
  if (answer.toLowerCase() !== "y") {
@@ -9524,7 +9883,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9524
9883
  {
9525
9884
  folder,
9526
9885
  experimentId: exp.id,
9527
- files: res.data.files.map((file) => import_node_path4.default.join(folder, file.path))
9886
+ files: res.data.files.map((file) => import_node_path5.default.join(folder, file.path))
9528
9887
  },
9529
9888
  () => {
9530
9889
  spinner.succeed(`Pulled ${exp.name}`);
@@ -9541,7 +9900,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9541
9900
  try {
9542
9901
  const folder = folderPath(folderArg);
9543
9902
  const session = readFilesystemSession(folder);
9544
- const files = listExperimentFilesystemFiles(folder);
9903
+ const files = listExperimentFilesystemFiles(folder, session);
9545
9904
  const res = await planExperimentFilesystem(creds, session.experimentId, { files });
9546
9905
  if (!res.ok || !res.data) {
9547
9906
  fail(
@@ -9558,6 +9917,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9558
9917
  plan: res.data
9559
9918
  });
9560
9919
  }
9920
+ await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId);
9561
9921
  output(
9562
9922
  this,
9563
9923
  { folder, experimentId: session.experimentId, plan: res.data },
@@ -9568,12 +9928,19 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9568
9928
  fail(this, error instanceof Error ? error.message : String(error));
9569
9929
  }
9570
9930
  });
9571
- 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) {
9931
+ 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").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action12(folderArg, opts) {
9572
9932
  const creds = requireCredentials();
9573
9933
  try {
9574
9934
  const folder = folderPath(folderArg);
9575
9935
  const session = readFilesystemSession(folder);
9576
- const files = listExperimentFilesystemFiles(folder);
9936
+ const files = await rewriteExperimentFilesystemAssets({
9937
+ files: listExperimentFilesystemFiles(folder, session),
9938
+ folder,
9939
+ baseUrl: creds.baseUrl,
9940
+ enabled: opts.assetUpload !== false,
9941
+ upload: uploadAssets,
9942
+ credentials: creds
9943
+ });
9577
9944
  const planRes = await planExperimentFilesystem(creds, session.experimentId, { files });
9578
9945
  if (!planRes.ok || !planRes.data) {
9579
9946
  fail(
@@ -9585,6 +9952,12 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9585
9952
  }
9586
9953
  if (!planRes.data.ok) {
9587
9954
  if (!shouldUseJson(this)) printFilesystemPlan(planRes.data);
9955
+ if (opts.assetUpload === false && planRes.data.blockingIssues?.some((issue) => /inline|hosted asset/i.test(issue.message))) {
9956
+ process.stderr.write(
9957
+ `Retry with hosted asset upload: apex experiments apply ${folderArg}
9958
+ `
9959
+ );
9960
+ }
9588
9961
  fail(this, blockingIssueSummary(planRes.data.blockingIssues), EXIT_CODES.validation, {
9589
9962
  errors: planRes.data.errors,
9590
9963
  plan: planRes.data
@@ -9810,7 +10183,93 @@ var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled
9810
10183
  throw error;
9811
10184
  }
9812
10185
  });
9813
- var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen);
10186
+ function parseJsonValue(value) {
10187
+ const source = value.startsWith("@") ? value.slice(1) : value;
10188
+ const text = value.startsWith("@") || import_node_fs7.default.existsSync(source) ? import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), source), "utf8") : value;
10189
+ return JSON.parse(text);
10190
+ }
10191
+ function collectFile(value, previous = []) {
10192
+ return [...previous, value];
10193
+ }
10194
+ function normalizeEvidenceDevice(value) {
10195
+ if (!value) return void 0;
10196
+ const normalized = value.trim().toLowerCase();
10197
+ if (normalized === "desktop_chrome" || normalized === "desktop_safari") return "desktop";
10198
+ if (normalized === "iphone_15_pro" || normalized === "galaxy_s24") return "mobile";
10199
+ if (normalized === "ipad_pro") return "tablet";
10200
+ return normalized;
10201
+ }
10202
+ function resolveEvidenceVariation(experiment, ref) {
10203
+ if (!ref) return void 0;
10204
+ const exact = experiment.variations?.find(
10205
+ (variation) => variation.id === ref || variation.slug === ref
10206
+ );
10207
+ if (exact) return exact.id;
10208
+ const matches = (experiment.variations ?? []).filter(
10209
+ (variation) => variation.name.toLowerCase() === ref.toLowerCase()
10210
+ );
10211
+ if (matches.length === 1) return matches[0].id;
10212
+ if (matches.length > 1) {
10213
+ throw new Error(`Ambiguous --variation value "${ref}". Use a variation ID or slug.`);
10214
+ }
10215
+ throw new Error(`Unknown --variation value: ${ref}`);
10216
+ }
10217
+ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or upload typed QA evidence").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--job <jobId>", "Attach a terminal screenshot job").option("--file <path>", "Upload an evidence image; repeat for more files", collectFile, []).option("--label <text>", "Evidence label").option("--device <device>", "desktop, mobile, tablet, or a raw device value").option("--variation <ref>", "Variation ID, slug, or name").option("--status <status>", "Evidence status").option("--kind <kind>", "Evidence kind", "screenshot").option("--records <json-or-@path>", "Typed evidence record array").option("--json", "Print the QA run in the standard JSON envelope").action(async function action18(target, opts) {
10218
+ const modes = Number(Boolean(opts.job)) + Number(opts.file.length > 0) + Number(Boolean(opts.records));
10219
+ if (modes !== 1) {
10220
+ fail(this, "Provide exactly one of --job, --file, or --records.", EXIT_CODES.validation);
10221
+ }
10222
+ const creds = requireCredentials();
10223
+ const spinner = await createSpinner("Writing QA evidence...");
10224
+ try {
10225
+ const resolved = await resolveExperimentForFilesystemOrId(target);
10226
+ let response;
10227
+ if (opts.job) {
10228
+ response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
10229
+ screenshot_job_id: opts.job
10230
+ });
10231
+ } else if (opts.records) {
10232
+ const parsed = parseJsonValue(opts.records);
10233
+ if (!Array.isArray(parsed)) throw new Error("--records must contain a JSON array.");
10234
+ response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
10235
+ records: parsed
10236
+ });
10237
+ } else {
10238
+ const experiment = await resolveExperiment(resolved.experimentId);
10239
+ if (!experiment) throw new Error("Experiment not found.");
10240
+ const variationId = resolveEvidenceVariation(experiment, opts.variation);
10241
+ const form = new FormData();
10242
+ for (const filePath of opts.file) {
10243
+ const absolutePath = import_node_path5.default.resolve(process.cwd(), filePath);
10244
+ if (!import_node_fs7.default.existsSync(absolutePath)) throw new Error(`Evidence file not found: ${filePath}`);
10245
+ const bytes = import_node_fs7.default.readFileSync(absolutePath);
10246
+ const extension = import_node_path5.default.extname(absolutePath).toLowerCase();
10247
+ const mimeType = extension === ".png" ? "image/png" : extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : "application/octet-stream";
10248
+ form.append("files", new File([bytes], import_node_path5.default.basename(absolutePath), { type: mimeType }));
10249
+ }
10250
+ if (opts.label) form.append("label", opts.label);
10251
+ const device = normalizeEvidenceDevice(opts.device);
10252
+ if (device) form.append("device", device);
10253
+ if (variationId) form.append("variation_id", variationId);
10254
+ if (opts.status) form.append("status", opts.status);
10255
+ if (opts.kind) form.append("kind", opts.kind);
10256
+ response = await uploadExperimentQaEvidence(creds, resolved.experimentId, form);
10257
+ }
10258
+ if (!response.ok || !response.data) {
10259
+ throw new CliError(
10260
+ response.error ?? "Failed to write QA evidence",
10261
+ classifyStatus(response.status),
10262
+ response.errorDetails
10263
+ );
10264
+ }
10265
+ spinner.stop();
10266
+ output(this, { run: response.data.run }, () => printQaRun(response.data.run));
10267
+ } catch (error) {
10268
+ spinner.fail("Failed to write QA evidence");
10269
+ throw error;
10270
+ }
10271
+ });
10272
+ var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
9814
10273
  var SCREENSHOT_DEVICES = [
9815
10274
  "desktop_chrome",
9816
10275
  "iphone_15_pro",
@@ -9825,11 +10284,11 @@ var screenshots = new Command("screenshots").description(
9825
10284
  ).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(
9826
10285
  "--variations <list>",
9827
10286
  "Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned"
9828
- ).option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option(
10287
+ ).option("--out <dir>", "Download directory", ".").option("--json", "Print the completed raw job payload with signed URLs").option("--no-attach-qa", "Do not attach the completed job to the QA run").option(
9829
10288
  "--timeout <seconds>",
9830
10289
  "Maximum wait for completion; finished results are always downloaded",
9831
10290
  "300"
9832
- ).action(async function action18(experimentRef, opts) {
10291
+ ).action(async function action19(experimentRef, opts) {
9833
10292
  if (opts.url)
9834
10293
  fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
9835
10294
  const timeoutSeconds = Number(opts.timeout);
@@ -9898,7 +10357,7 @@ var screenshots = new Command("screenshots").description(
9898
10357
  if (screenshotNow() >= deadline)
9899
10358
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
9900
10359
  await new Promise(
9901
- (resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
10360
+ (resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
9902
10361
  );
9903
10362
  if (screenshotNow() >= deadline)
9904
10363
  fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
@@ -9934,10 +10393,31 @@ var screenshots = new Command("screenshots").description(
9934
10393
  job = polled.data;
9935
10394
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
9936
10395
  }
10396
+ if (opts.attachQa !== false) {
10397
+ const attached = await attachExperimentQaEvidence(creds, experiment.id, {
10398
+ screenshot_job_id: job.job_id
10399
+ });
10400
+ if (!attached.ok) {
10401
+ if (attached.status === 403) {
10402
+ console.error(
10403
+ source_default.yellow(
10404
+ "Warning: this API key cannot attach QA evidence; screenshots will still download."
10405
+ )
10406
+ );
10407
+ } else {
10408
+ fail(
10409
+ this,
10410
+ attached.error ?? "Failed to attach screenshot job to QA",
10411
+ classifyStatus(attached.status),
10412
+ attached.errorDetails
10413
+ );
10414
+ }
10415
+ }
10416
+ }
9937
10417
  if (job.status === "failed")
9938
10418
  fail(this, `Screenshot job failed: ${job.error ?? "unknown error"}`);
9939
- const outputDir = import_node_path4.default.resolve(process.cwd(), opts.out);
9940
- import_node_fs6.default.mkdirSync(outputDir, { recursive: true });
10419
+ const outputDir = import_node_path5.default.resolve(process.cwd(), opts.out);
10420
+ import_node_fs7.default.mkdirSync(outputDir, { recursive: true });
9941
10421
  const rows = [];
9942
10422
  const errors = [];
9943
10423
  const usedFilenames = /* @__PURE__ */ new Set();
@@ -9979,8 +10459,8 @@ var screenshots = new Command("screenshots").description(
9979
10459
  try {
9980
10460
  const response = await fetch(url);
9981
10461
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
9982
- await import_node_fs6.default.promises.writeFile(
9983
- import_node_path4.default.join(outputDir, filename),
10462
+ await import_node_fs7.default.promises.writeFile(
10463
+ import_node_path5.default.join(outputDir, filename),
9984
10464
  Buffer.from(await response.arrayBuffer())
9985
10465
  );
9986
10466
  workerResults.push({ index, row: [variation.name, device, filename] });
@@ -10020,9 +10500,9 @@ Examples:
10020
10500
 
10021
10501
  // src/commands/push.ts
10022
10502
  init_source();
10023
- var import_node_fs7 = __toESM(require("node:fs"));
10503
+ var import_node_fs8 = __toESM(require("node:fs"));
10024
10504
  var import_node_os6 = __toESM(require("node:os"));
10025
- var import_node_path5 = __toESM(require("node:path"));
10505
+ var import_node_path6 = __toESM(require("node:path"));
10026
10506
  var import_node_url2 = require("node:url");
10027
10507
  function toApiBody(exp) {
10028
10508
  return {
@@ -10045,8 +10525,25 @@ function toApiBody(exp) {
10045
10525
  goal_ids: exp.goals?.map((g) => g.id)
10046
10526
  };
10047
10527
  }
10528
+ async function rewriteExperimentAssets(exp, rewrite, baseDir) {
10529
+ return {
10530
+ ...exp,
10531
+ variations: await Promise.all(
10532
+ exp.variations.map(async (variation) => ({
10533
+ ...variation,
10534
+ mutations: await rewrite.rewriteUnknown(variation.mutations ?? [], baseDir),
10535
+ css: variation.css === void 0 ? void 0 : await rewrite.rewrite(variation.css, baseDir),
10536
+ custom_css: variation.custom_css === void 0 ? void 0 : await rewrite.rewrite(variation.custom_css, baseDir),
10537
+ js: variation.js === void 0 ? void 0 : await rewrite.rewrite(variation.js, baseDir),
10538
+ custom_js: variation.custom_js === void 0 ? void 0 : await rewrite.rewrite(variation.custom_js, baseDir),
10539
+ resetJs: variation.resetJs === void 0 ? void 0 : await rewrite.rewrite(variation.resetJs, baseDir),
10540
+ reset_js: variation.reset_js === void 0 ? void 0 : await rewrite.rewrite(variation.reset_js, baseDir)
10541
+ }))
10542
+ )
10543
+ };
10544
+ }
10048
10545
  async function importConfigModule(absPath) {
10049
- const ext = import_node_path5.default.extname(absPath).toLowerCase();
10546
+ const ext = import_node_path6.default.extname(absPath).toLowerCase();
10050
10547
  if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
10051
10548
  return await import((0, import_node_url2.pathToFileURL)(absPath).href);
10052
10549
  }
@@ -10060,8 +10557,8 @@ async function importConfigModule(absPath) {
10060
10557
  ` + (err instanceof Error ? err.message : String(err))
10061
10558
  );
10062
10559
  }
10063
- const tmpDir = import_node_fs7.default.mkdtempSync(import_node_path5.default.join(import_node_os6.default.tmpdir(), "drip-config-"));
10064
- const outFile = import_node_path5.default.join(tmpDir, "config.mjs");
10560
+ const tmpDir = import_node_fs8.default.mkdtempSync(import_node_path6.default.join(import_node_os6.default.tmpdir(), "drip-config-"));
10561
+ const outFile = import_node_path6.default.join(tmpDir, "config.mjs");
10065
10562
  try {
10066
10563
  await esbuild.build({
10067
10564
  entryPoints: [absPath],
@@ -10077,7 +10574,7 @@ async function importConfigModule(absPath) {
10077
10574
  return await import(href);
10078
10575
  } finally {
10079
10576
  try {
10080
- import_node_fs7.default.rmSync(tmpDir, { recursive: true, force: true });
10577
+ import_node_fs8.default.rmSync(tmpDir, { recursive: true, force: true });
10081
10578
  } catch {
10082
10579
  }
10083
10580
  }
@@ -10087,7 +10584,7 @@ async function importConfigModule(absPath) {
10087
10584
  function isRecord(value) {
10088
10585
  return !!value && typeof value === "object" && !Array.isArray(value);
10089
10586
  }
10090
- var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action19(opts) {
10587
+ var pushCommand = new Command("push").description("Push experiment definitions from config file to the API (idempotent by slug)").option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action20(opts) {
10091
10588
  const creds = requireCredentials();
10092
10589
  const configPath = opts.config ?? findConfigPath();
10093
10590
  if (!configPath) {
@@ -10098,10 +10595,10 @@ var pushCommand = new Command("push").description("Push experiment definitions f
10098
10595
  );
10099
10596
  }
10100
10597
  const spinner = process.stderr.isTTY ? await createSpinner(`Loading config from ${source_default.cyan(configPath)}...`) : null;
10598
+ const absoluteConfigPath = import_node_path6.default.resolve(configPath);
10101
10599
  let experiments;
10102
10600
  try {
10103
- const absPath = import_node_path5.default.resolve(configPath);
10104
- const mod = await importConfigModule(absPath);
10601
+ const mod = await importConfigModule(absoluteConfigPath);
10105
10602
  const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
10106
10603
  if (Array.isArray(exported)) {
10107
10604
  experiments = exported;
@@ -10164,17 +10661,57 @@ var pushCommand = new Command("push").description("Push experiment definitions f
10164
10661
  );
10165
10662
  return;
10166
10663
  }
10664
+ const assetRewriter = createAssetReferenceRewriter({
10665
+ baseDir: import_node_path6.default.dirname(absoluteConfigPath),
10666
+ baseUrl: creds.baseUrl,
10667
+ enabled: opts.assetUpload !== false,
10668
+ upload: async (file) => {
10669
+ const response = await uploadAssets(creds, [file]);
10670
+ const asset = response.data?.assets?.[0];
10671
+ if (!response.ok || !asset?.id) {
10672
+ throw new Error(response.error ?? `Failed to upload ${file.name}.`);
10673
+ }
10674
+ return {
10675
+ id: asset.id,
10676
+ url: typeof asset.url === "string" ? asset.url : void 0,
10677
+ deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : void 0,
10678
+ delivery_url: typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
10679
+ };
10680
+ },
10681
+ onUpload: ({ name, url }) => {
10682
+ process.stderr.write(`Uploaded asset ${name} -> ${url}
10683
+ `);
10684
+ }
10685
+ });
10167
10686
  let created = 0;
10168
10687
  let updated = 0;
10169
10688
  let failed = 0;
10170
10689
  const results2 = [];
10171
10690
  for (const exp of experiments) {
10172
10691
  const s = process.stderr.isTTY ? await createSpinner(`Pushing ${source_default.cyan(exp.slug)}...`) : null;
10173
- const body5 = toApiBody(exp);
10692
+ let body5;
10693
+ try {
10694
+ body5 = toApiBody(
10695
+ await rewriteExperimentAssets(exp, assetRewriter, import_node_path6.default.dirname(absoluteConfigPath))
10696
+ );
10697
+ } catch (error) {
10698
+ const message = error instanceof Error ? error.message : String(error);
10699
+ s?.fail(`Failed: ${exp.slug}`);
10700
+ printError(message);
10701
+ failed++;
10702
+ results2.push({ slug: exp.slug, status: "failed", error: message });
10703
+ continue;
10704
+ }
10174
10705
  const res = await upsertExperimentBySlug(creds, exp.slug, body5);
10175
10706
  if (!res.ok) {
10176
10707
  s?.fail(`Failed: ${exp.slug}`);
10177
10708
  printError(res.error);
10709
+ if (opts.assetUpload === false && /inline|hosted asset/i.test(res.error ?? "")) {
10710
+ process.stderr.write(
10711
+ `Retry with hosted asset upload: apex push --config ${configPath}
10712
+ `
10713
+ );
10714
+ }
10178
10715
  failed++;
10179
10716
  results2.push({
10180
10717
  slug: exp.slug,
@@ -10353,25 +10890,25 @@ var previewCommand = new Command("preview").description("Open QA preview URL for
10353
10890
 
10354
10891
  // src/commands/harness.ts
10355
10892
  init_source();
10356
- var import_node_fs8 = __toESM(require("node:fs"));
10357
- var import_node_path6 = __toESM(require("node:path"));
10893
+ var import_node_fs9 = __toESM(require("node:fs"));
10894
+ var import_node_path7 = __toESM(require("node:path"));
10358
10895
  var import_node_child_process6 = require("node:child_process");
10359
10896
  function slugify(value, fallback2 = "variation") {
10360
10897
  const slug = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
10361
10898
  return slug || fallback2;
10362
10899
  }
10363
10900
  function readJson(filePath) {
10364
- if (!import_node_fs8.default.existsSync(filePath)) return null;
10365
- return JSON.parse(import_node_fs8.default.readFileSync(filePath, "utf8"));
10901
+ if (!import_node_fs9.default.existsSync(filePath)) return null;
10902
+ return JSON.parse(import_node_fs9.default.readFileSync(filePath, "utf8"));
10366
10903
  }
10367
10904
  function resolveFolder(folder) {
10368
- return import_node_path6.default.resolve(process.cwd(), folder);
10905
+ return import_node_path7.default.resolve(process.cwd(), folder);
10369
10906
  }
10370
10907
  function experimentConfigPath(folder) {
10371
- return import_node_path6.default.join(folder, "experiment.config.json");
10908
+ return import_node_path7.default.join(folder, "experiment.config.json");
10372
10909
  }
10373
10910
  function apexConfigPath(folder) {
10374
- return import_node_path6.default.join(folder, "apex.harness.json");
10911
+ return import_node_path7.default.join(folder, "apex.harness.json");
10375
10912
  }
10376
10913
  function loadHarnessConfig(folder) {
10377
10914
  const experiment = readJson(experimentConfigPath(folder)) ?? {};
@@ -10380,8 +10917,8 @@ function loadHarnessConfig(folder) {
10380
10917
  const targetOrigin = experiment.targetOrigin?.replace(/\/+$/, "") ?? "";
10381
10918
  const defaultPath = experiment.defaultPath ?? "/";
10382
10919
  const targetUrl = targetOrigin ? new URL(defaultPath, targetOrigin).toString() : "";
10383
- const id = experiment.id ?? import_node_path6.default.basename(import_node_path6.default.dirname(folder));
10384
- const variation = import_node_path6.default.basename(folder);
10920
+ const id = experiment.id ?? import_node_path7.default.basename(import_node_path7.default.dirname(folder));
10921
+ const variation = import_node_path7.default.basename(folder);
10385
10922
  return {
10386
10923
  experiment,
10387
10924
  apex: {
@@ -10401,11 +10938,11 @@ function cleanupResetJs(experiment) {
10401
10938
  }
10402
10939
  function runAbHarness(args, cwd) {
10403
10940
  const bin = process.env.AB_HARNESS_BIN || "ab-harness";
10404
- return new Promise((resolve, reject) => {
10941
+ return new Promise((resolve2, reject) => {
10405
10942
  const child = (0, import_node_child_process6.spawn)(bin, args, { cwd, stdio: "inherit" });
10406
10943
  child.on("error", reject);
10407
10944
  child.on("exit", (code) => {
10408
- if (code === 0) resolve();
10945
+ if (code === 0) resolve2();
10409
10946
  else reject(new Error(`${bin} ${args.join(" ")} exited with ${code}`));
10410
10947
  });
10411
10948
  });
@@ -10413,37 +10950,37 @@ function runAbHarness(args, cwd) {
10413
10950
  function artifactPaths(folder, experiment) {
10414
10951
  const variant = experiment.variant;
10415
10952
  if (variant?.endsWith(".bundle.js")) {
10416
- const jsPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
10417
- const cssPath = import_node_path6.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
10418
- return { jsPath, cssPath: import_node_fs8.default.existsSync(cssPath) ? cssPath : null };
10953
+ const jsPath = import_node_path7.default.join(folder, variant.replace(/\.bundle\.js$/, ".js"));
10954
+ const cssPath = import_node_path7.default.join(folder, variant.replace(/\.bundle\.js$/, ".css"));
10955
+ return { jsPath, cssPath: import_node_fs9.default.existsSync(cssPath) ? cssPath : null };
10419
10956
  }
10420
- const buildDir = import_node_path6.default.join(folder, "build");
10421
- const files = import_node_fs8.default.existsSync(buildDir) ? import_node_fs8.default.readdirSync(buildDir) : [];
10957
+ const buildDir = import_node_path7.default.join(folder, "build");
10958
+ const files = import_node_fs9.default.existsSync(buildDir) ? import_node_fs9.default.readdirSync(buildDir) : [];
10422
10959
  const js = files.find(
10423
10960
  (file) => file.endsWith(".js") && !file.endsWith(".bundle.js") && !file.endsWith(".min.js")
10424
10961
  );
10425
10962
  const css = files.find((file) => file.endsWith(".css") && !file.endsWith(".min.css"));
10426
10963
  if (!js) throw new Error("Could not find harness JS artifact in build/");
10427
10964
  return {
10428
- jsPath: import_node_path6.default.join(buildDir, js),
10429
- cssPath: css ? import_node_path6.default.join(buildDir, css) : null
10965
+ jsPath: import_node_path7.default.join(buildDir, js),
10966
+ cssPath: css ? import_node_path7.default.join(buildDir, css) : null
10430
10967
  };
10431
10968
  }
10432
10969
  async function uploadVersion(params) {
10433
10970
  const creds = requireCredentials();
10434
10971
  const { jsPath, cssPath } = artifactPaths(params.folder, params.experiment);
10435
- if (!import_node_fs8.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
10972
+ if (!import_node_fs9.default.existsSync(jsPath)) throw new Error(`Missing JS artifact: ${jsPath}`);
10436
10973
  const form = new FormData();
10437
10974
  form.append(
10438
10975
  "js",
10439
- new Blob([import_node_fs8.default.readFileSync(jsPath)], { type: "application/javascript" }),
10440
- import_node_path6.default.basename(jsPath)
10976
+ new Blob([import_node_fs9.default.readFileSync(jsPath)], { type: "application/javascript" }),
10977
+ import_node_path7.default.basename(jsPath)
10441
10978
  );
10442
- if (cssPath && import_node_fs8.default.existsSync(cssPath) && import_node_fs8.default.statSync(cssPath).size > 0) {
10979
+ if (cssPath && import_node_fs9.default.existsSync(cssPath) && import_node_fs9.default.statSync(cssPath).size > 0) {
10443
10980
  form.append(
10444
10981
  "css",
10445
- new Blob([import_node_fs8.default.readFileSync(cssPath)], { type: "text/css" }),
10446
- import_node_path6.default.basename(cssPath)
10982
+ new Blob([import_node_fs9.default.readFileSync(cssPath)], { type: "text/css" }),
10983
+ import_node_path7.default.basename(cssPath)
10447
10984
  );
10448
10985
  }
10449
10986
  form.append(
@@ -10452,8 +10989,8 @@ async function uploadVersion(params) {
10452
10989
  ...params.apex,
10453
10990
  resetJs: cleanupResetJs(params.experiment),
10454
10991
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
10455
- jsPath: import_node_path6.default.relative(params.folder, jsPath),
10456
- cssPath: cssPath ? import_node_path6.default.relative(params.folder, cssPath) : null
10992
+ jsPath: import_node_path7.default.relative(params.folder, jsPath),
10993
+ cssPath: cssPath ? import_node_path7.default.relative(params.folder, cssPath) : null
10457
10994
  })
10458
10995
  );
10459
10996
  const res = await uploadHarnessVersion(creds, params.session.sessionId, form);
@@ -10491,14 +11028,14 @@ async function createSession(folder) {
10491
11028
  var harnessCommand = new Command("harness").description(
10492
11029
  "Develop AB Test Harness projects through Apex live QA sessions"
10493
11030
  );
10494
- harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action20(folderArg, opts) {
11031
+ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action21(folderArg, opts) {
10495
11032
  const folder = resolveFolder(folderArg);
10496
11033
  const targetPath = apexConfigPath(folder);
10497
- if (import_node_fs8.default.existsSync(targetPath) && !opts.force) {
11034
+ if (import_node_fs9.default.existsSync(targetPath) && !opts.force) {
10498
11035
  fail(this, `${targetPath} already exists. Use --force to overwrite.`, EXIT_CODES.conflict);
10499
11036
  }
10500
11037
  const { apex } = loadHarnessConfig(folder);
10501
- import_node_fs8.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
11038
+ import_node_fs9.default.writeFileSync(targetPath, `${JSON.stringify(apex, null, 2)}
10502
11039
  `, "utf8");
10503
11040
  output(
10504
11041
  this,
@@ -10511,7 +11048,7 @@ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment
10511
11048
  () => printSuccess(`Created ${targetPath}`)
10512
11049
  );
10513
11050
  });
10514
- 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) {
11051
+ 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) {
10515
11052
  const folder = resolveFolder(folderArg);
10516
11053
  const created = await createSession(folder);
10517
11054
  await runAbHarness(["build", folder], folder);
@@ -10564,7 +11101,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
10564
11101
  }
10565
11102
  };
10566
11103
  let debounce = null;
10567
- const watcher = import_node_fs8.default.watch(folder, { recursive: true }, (_event, filename) => {
11104
+ const watcher = import_node_fs9.default.watch(folder, { recursive: true }, (_event, filename) => {
10568
11105
  const name = String(filename ?? "");
10569
11106
  if (!name || name.startsWith("build/") || name.includes("/build/")) return;
10570
11107
  if (debounce) clearTimeout(debounce);
@@ -10578,7 +11115,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
10578
11115
  process.exitCode = 0;
10579
11116
  });
10580
11117
  });
10581
- harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action22(folderArg) {
11118
+ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
10582
11119
  const folder = resolveFolder(folderArg);
10583
11120
  const created = await createSession(folder);
10584
11121
  await runAbHarness(["verify", folder], folder);
@@ -10606,9 +11143,9 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
10606
11143
 
10607
11144
  // src/commands/operator.ts
10608
11145
  init_source();
10609
- var import_node_crypto3 = __toESM(require("node:crypto"));
10610
- var import_node_fs9 = __toESM(require("node:fs"));
10611
- var import_node_path7 = __toESM(require("node:path"));
11146
+ var import_node_crypto5 = __toESM(require("node:crypto"));
11147
+ var import_node_fs10 = __toESM(require("node:fs"));
11148
+ var import_node_path8 = __toESM(require("node:path"));
10612
11149
  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)$/;
10613
11150
  function workspaceIssueFile(field, evidence) {
10614
11151
  if (field === "custom_js" || field === "reset_js") return "src/index.js";
@@ -10652,7 +11189,7 @@ function formatWorkspaceBlockingIssues(value) {
10652
11189
  }).join("\n");
10653
11190
  }
10654
11191
  function sha256Text2(value) {
10655
- return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
11192
+ return import_node_crypto5.default.createHash("sha256").update(value).digest("hex");
10656
11193
  }
10657
11194
  function byteLength(value) {
10658
11195
  return Buffer.byteLength(value, "utf8");
@@ -10662,28 +11199,28 @@ function slugify2(value, fallback2 = "operator") {
10662
11199
  return slug || fallback2;
10663
11200
  }
10664
11201
  function folderPath2(folder) {
10665
- return import_node_path7.default.resolve(process.cwd(), folder);
11202
+ return import_node_path8.default.resolve(process.cwd(), folder);
10666
11203
  }
10667
11204
  function apexDir(folder) {
10668
- return import_node_path7.default.join(folder, ".apex");
11205
+ return import_node_path8.default.join(folder, ".apex");
10669
11206
  }
10670
11207
  function sessionPath(folder) {
10671
- return import_node_path7.default.join(apexDir(folder), "session.json");
11208
+ return import_node_path8.default.join(apexDir(folder), "session.json");
10672
11209
  }
10673
11210
  function readJsonFile(filePath) {
10674
- if (!import_node_fs9.default.existsSync(filePath)) return null;
10675
- return JSON.parse(import_node_fs9.default.readFileSync(filePath, "utf8"));
11211
+ if (!import_node_fs10.default.existsSync(filePath)) return null;
11212
+ return JSON.parse(import_node_fs10.default.readFileSync(filePath, "utf8"));
10676
11213
  }
10677
11214
  function writeJsonFile(filePath, value) {
10678
- import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(filePath), { recursive: true });
10679
- import_node_fs9.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
11215
+ import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(filePath), { recursive: true });
11216
+ import_node_fs10.default.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
10680
11217
  `, "utf8");
10681
11218
  }
10682
11219
  function readSession(folder) {
10683
11220
  const session = readJsonFile(sessionPath(folder));
10684
11221
  if (!session) {
10685
- const filesystemMarker = import_node_path7.default.join(apexDir(folder), "experiment-filesystem.json");
10686
- if (import_node_fs9.default.existsSync(filesystemMarker)) {
11222
+ const filesystemMarker = import_node_path8.default.join(apexDir(folder), "experiment-filesystem.json");
11223
+ if (import_node_fs10.default.existsSync(filesystemMarker)) {
10687
11224
  throw new Error(
10688
11225
  "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."
10689
11226
  );
@@ -10693,9 +11230,9 @@ function readSession(folder) {
10693
11230
  return session;
10694
11231
  }
10695
11232
  function writeSession(folder, session) {
10696
- import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
10697
- import_node_fs9.default.writeFileSync(
10698
- import_node_path7.default.join(apexDir(folder), ".gitignore"),
11233
+ import_node_fs10.default.mkdirSync(apexDir(folder), { recursive: true });
11234
+ import_node_fs10.default.writeFileSync(
11235
+ import_node_path8.default.join(apexDir(folder), ".gitignore"),
10699
11236
  "session.json\nharness-session.json\n",
10700
11237
  "utf8"
10701
11238
  );
@@ -10719,15 +11256,15 @@ function listLocalWorkspaceFiles(folder) {
10719
11256
  "brand.md"
10720
11257
  ];
10721
11258
  function visit2(dir) {
10722
- if (!import_node_fs9.default.existsSync(dir)) return;
10723
- for (const entry of import_node_fs9.default.readdirSync(dir, { withFileTypes: true })) {
10724
- const absPath = import_node_path7.default.join(dir, entry.name);
11259
+ if (!import_node_fs10.default.existsSync(dir)) return;
11260
+ for (const entry of import_node_fs10.default.readdirSync(dir, { withFileTypes: true })) {
11261
+ const absPath = import_node_path8.default.join(dir, entry.name);
10725
11262
  if (entry.isDirectory()) {
10726
11263
  visit2(absPath);
10727
11264
  continue;
10728
11265
  }
10729
- const relativePath = validateCanonicalPath(import_node_path7.default.relative(folder, absPath));
10730
- const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11266
+ const relativePath = validateCanonicalPath(import_node_path8.default.relative(folder, absPath));
11267
+ const content = import_node_fs10.default.readFileSync(absPath, "utf8");
10731
11268
  files.push({
10732
11269
  path: relativePath,
10733
11270
  absPath,
@@ -10737,11 +11274,11 @@ function listLocalWorkspaceFiles(folder) {
10737
11274
  });
10738
11275
  }
10739
11276
  }
10740
- for (const root of roots) visit2(import_node_path7.default.join(folder, root));
11277
+ for (const root of roots) visit2(import_node_path8.default.join(folder, root));
10741
11278
  for (const file of topLevel) {
10742
- const absPath = import_node_path7.default.join(folder, file);
10743
- if (!import_node_fs9.default.existsSync(absPath)) continue;
10744
- const content = import_node_fs9.default.readFileSync(absPath, "utf8");
11279
+ const absPath = import_node_path8.default.join(folder, file);
11280
+ if (!import_node_fs10.default.existsSync(absPath)) continue;
11281
+ const content = import_node_fs10.default.readFileSync(absPath, "utf8");
10745
11282
  files.push({
10746
11283
  path: file,
10747
11284
  absPath,
@@ -10765,7 +11302,7 @@ function validateJsonFiles(files) {
10765
11302
  }
10766
11303
  }
10767
11304
  function readExperimentConfig(folder) {
10768
- return readJsonFile(import_node_path7.default.join(folder, "experiment.config.json")) ?? {};
11305
+ return readJsonFile(import_node_path8.default.join(folder, "experiment.config.json")) ?? {};
10769
11306
  }
10770
11307
  function resolveVariation(exp, variationArg) {
10771
11308
  const variations = exp.variations ?? [];
@@ -10782,9 +11319,9 @@ function resolveVariation(exp, variationArg) {
10782
11319
  function writeWorkspaceFiles(folder, workspace) {
10783
11320
  for (const file of workspace.files) {
10784
11321
  const relativePath = validateCanonicalPath(file.path);
10785
- const absPath = import_node_path7.default.join(folder, relativePath);
10786
- import_node_fs9.default.mkdirSync(import_node_path7.default.dirname(absPath), { recursive: true });
10787
- import_node_fs9.default.writeFileSync(absPath, file.content, "utf8");
11322
+ const absPath = import_node_path8.default.join(folder, relativePath);
11323
+ import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(absPath), { recursive: true });
11324
+ import_node_fs10.default.writeFileSync(absPath, file.content, "utf8");
10788
11325
  }
10789
11326
  }
10790
11327
  function sessionFromWorkspace(input) {
@@ -10853,10 +11390,10 @@ async function uploadCurrentArtifactVersion(folder, sessionId) {
10853
11390
  form.append(
10854
11391
  "js",
10855
11392
  new Blob([js.content], { type: "application/javascript" }),
10856
- import_node_path7.default.basename(js.path)
11393
+ import_node_path8.default.basename(js.path)
10857
11394
  );
10858
11395
  if (css && css.content.trim()) {
10859
- form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path7.default.basename(css.path));
11396
+ form.append("css", new Blob([css.content], { type: "text/css" }), import_node_path8.default.basename(css.path));
10860
11397
  }
10861
11398
  form.append("manifest", JSON.stringify(manifest));
10862
11399
  const res = await uploadHarnessVersion(creds, sessionId, form);
@@ -10953,7 +11490,7 @@ Examples:
10953
11490
  apex workspace publish ./workspace
10954
11491
  `
10955
11492
  );
10956
- operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function action23(experimentId, opts) {
11493
+ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment ID").requiredOption("--variation <id-or-slug>", "Variation ID, slug, or name").option("--out <path>", "Target folder").action(async function action24(experimentId, opts) {
10957
11494
  try {
10958
11495
  const creds = requireCredentials();
10959
11496
  const expRes = await getExperiment(creds, experimentId);
@@ -10975,7 +11512,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
10975
11512
  classifyStatus(workspace.status),
10976
11513
  workspace.errorDetails
10977
11514
  );
10978
- import_node_fs9.default.mkdirSync(folder, { recursive: true });
11515
+ import_node_fs10.default.mkdirSync(folder, { recursive: true });
10979
11516
  writeWorkspaceFiles(folder, workspace.data);
10980
11517
  writeSession(
10981
11518
  folder,
@@ -11003,7 +11540,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11003
11540
  throw error;
11004
11541
  }
11005
11542
  });
11006
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action24(folderArg) {
11543
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
11007
11544
  try {
11008
11545
  const folder = folderPath2(folderArg);
11009
11546
  const session = readSession(folder);
@@ -11035,14 +11572,14 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
11035
11572
  fail(this, error instanceof Error ? error.message : String(error));
11036
11573
  }
11037
11574
  });
11038
- 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) {
11575
+ 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) {
11039
11576
  try {
11040
11577
  const folder = folderPath2(folderArg);
11041
11578
  const session = readSession(folder);
11042
11579
  const harnessSession = await createExactHarnessSession(folder, session);
11043
11580
  const version = await uploadCurrentArtifactVersion(folder, harnessSession.sessionId);
11044
- import_node_fs9.default.mkdirSync(apexDir(folder), { recursive: true });
11045
- writeJsonFile(import_node_path7.default.join(apexDir(folder), "harness-session.json"), {
11581
+ import_node_fs10.default.mkdirSync(apexDir(folder), { recursive: true });
11582
+ writeJsonFile(import_node_path8.default.join(apexDir(folder), "harness-session.json"), {
11046
11583
  sessionId: harnessSession.sessionId,
11047
11584
  experimentId: harnessSession.experimentId,
11048
11585
  variationId: harnessSession.variationId,
@@ -11097,7 +11634,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11097
11634
  }
11098
11635
  };
11099
11636
  let timer = null;
11100
- const watcher = import_node_fs9.default.watch(folder, { recursive: true }, (_event, filename) => {
11637
+ const watcher = import_node_fs10.default.watch(folder, { recursive: true }, (_event, filename) => {
11101
11638
  const name = String(filename ?? "");
11102
11639
  if (!name || name.startsWith(".apex/") || name.includes("/.apex/")) return;
11103
11640
  if (timer) clearTimeout(timer);
@@ -11114,7 +11651,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11114
11651
  throw error;
11115
11652
  }
11116
11653
  });
11117
- 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) {
11654
+ 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) {
11118
11655
  try {
11119
11656
  const result = await runPublish(folderArg, opts);
11120
11657
  const build = result.build;
@@ -11132,8 +11669,8 @@ operatorCommand.command("publish").argument("[folder]", "Operator workspace fold
11132
11669
 
11133
11670
  // src/commands/roadmap.ts
11134
11671
  init_source();
11135
- var import_node_fs10 = __toESM(require("node:fs"));
11136
- var import_node_path8 = __toESM(require("node:path"));
11672
+ var import_node_fs11 = __toESM(require("node:fs"));
11673
+ var import_node_path9 = __toESM(require("node:path"));
11137
11674
  var LEGACY_OUTPUT_REMOVAL_VERSION = "0.7.0";
11138
11675
  function truncate(value, max) {
11139
11676
  const text = typeof value === "string" ? value : value == null ? "" : String(value);
@@ -11158,7 +11695,7 @@ function briefLine(item) {
11158
11695
  ].filter(Boolean);
11159
11696
  return parts.join("\n");
11160
11697
  }
11161
- 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) {
11698
+ 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) {
11162
11699
  const creds = requireCredentials();
11163
11700
  const limit = parseInt(opts.limit, 10);
11164
11701
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -11212,7 +11749,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
11212
11749
  }
11213
11750
  );
11214
11751
  });
11215
- 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) {
11752
+ 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) {
11216
11753
  if (opts.output !== void 0) {
11217
11754
  process.stderr.write(
11218
11755
  `Warning: --output is deprecated for apex roadmap handoff; use --out instead. --output will be removed in v${LEGACY_OUTPUT_REMOVAL_VERSION}.
@@ -11238,9 +11775,9 @@ var handoff = new Command("handoff").description("Get the full build brief for a
11238
11775
  `;
11239
11776
  const outputData = opts.instructions ? { roadmapItemId: id, instructions: String(payload.instructions ?? "") } : payload;
11240
11777
  if (opts.out || opts.output) {
11241
- const target = import_node_path8.default.resolve(process.cwd(), opts.out ?? opts.output);
11242
- import_node_fs10.default.mkdirSync(import_node_path8.default.dirname(target), { recursive: true });
11243
- import_node_fs10.default.writeFileSync(target, rendered, "utf8");
11778
+ const target = import_node_path9.default.resolve(process.cwd(), opts.out ?? opts.output);
11779
+ import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(target), { recursive: true });
11780
+ import_node_fs11.default.writeFileSync(target, rendered, "utf8");
11244
11781
  output(this, { path: target, roadmapItemId: id }, () => printSuccess(`Wrote ${target}`));
11245
11782
  return;
11246
11783
  }
@@ -11254,8 +11791,8 @@ roadmapCommand.addCommand(handoff);
11254
11791
 
11255
11792
  // src/commands/tracking.ts
11256
11793
  init_source();
11257
- var import_node_fs11 = __toESM(require("node:fs"));
11258
- var import_node_path9 = __toESM(require("node:path"));
11794
+ var import_node_fs12 = __toESM(require("node:fs"));
11795
+ var import_node_path10 = __toESM(require("node:path"));
11259
11796
  function normalizeUrl(input) {
11260
11797
  const value = input.trim();
11261
11798
  if (!value) throw new Error("URL cannot be empty");
@@ -11301,17 +11838,17 @@ function commonCookieDomain(hostnames) {
11301
11838
  return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
11302
11839
  }
11303
11840
  function defaultManifestPath() {
11304
- return import_node_path9.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
11841
+ return import_node_path10.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
11305
11842
  }
11306
11843
  function readManifest(filePath) {
11307
- if (!import_node_fs11.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
11308
- const parsed = JSON.parse(import_node_fs11.default.readFileSync(filePath, "utf8"));
11844
+ if (!import_node_fs12.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
11845
+ const parsed = JSON.parse(import_node_fs12.default.readFileSync(filePath, "utf8"));
11309
11846
  if (parsed.schemaVersion !== 1) throw new Error("Unsupported tracking manifest schema");
11310
11847
  return parsed;
11311
11848
  }
11312
11849
  function writeManifest(filePath, manifest) {
11313
- import_node_fs11.default.mkdirSync(import_node_path9.default.dirname(filePath), { recursive: true });
11314
- import_node_fs11.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
11850
+ import_node_fs12.default.mkdirSync(import_node_path10.default.dirname(filePath), { recursive: true });
11851
+ import_node_fs12.default.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}
11315
11852
  `, "utf8");
11316
11853
  }
11317
11854
  function buildSnippet(shopId, endpoint) {
@@ -11378,7 +11915,7 @@ function buildManifest(urlArgs, opts) {
11378
11915
  };
11379
11916
  }
11380
11917
  function manifestPath(input) {
11381
- return import_node_path9.default.resolve(process.cwd(), input ?? defaultManifestPath());
11918
+ return import_node_path10.default.resolve(process.cwd(), input ?? defaultManifestPath());
11382
11919
  }
11383
11920
  function printManifestPlan(manifest) {
11384
11921
  console.log(source_default.bold("Apex tracking ingestion plan"));
@@ -11431,7 +11968,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
11431
11968
  console.log(`${index + 1}. ${check}`);
11432
11969
  });
11433
11970
  });
11434
- 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) {
11971
+ 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) {
11435
11972
  const creds = requireCredentials();
11436
11973
  const res = await verifyTracking(creds, {
11437
11974
  minutes: Number.parseInt(opts.minutes, 10),
@@ -11485,8 +12022,8 @@ trackingCommand.command("verify").description("Verify live tracking events for t
11485
12022
 
11486
12023
  // src/commands/goals.ts
11487
12024
  init_source();
11488
- var import_node_fs12 = __toESM(require("node:fs"));
11489
- var import_node_path10 = __toESM(require("node:path"));
12025
+ var import_node_fs13 = __toESM(require("node:fs"));
12026
+ var import_node_path11 = __toESM(require("node:path"));
11490
12027
 
11491
12028
  // ../sdk/src/safe-regex.ts
11492
12029
  var MAX_PATTERN_LENGTH = 1024;
@@ -11662,7 +12199,7 @@ function printSuggestions(data) {
11662
12199
  function parseCapture(command, value) {
11663
12200
  if (!value) return void 0;
11664
12201
  const source = value.startsWith("@") ? value.slice(1) : value;
11665
- 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;
12202
+ 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;
11666
12203
  try {
11667
12204
  return JSON.parse(text);
11668
12205
  } catch (error) {
@@ -11688,7 +12225,7 @@ function printGoal(goal) {
11688
12225
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
11689
12226
  console.log();
11690
12227
  }
11691
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action30(opts) {
12228
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
11692
12229
  const creds = requireCredentials();
11693
12230
  const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
11694
12231
  if (!res.ok || !res.data) {
@@ -11712,7 +12249,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
11712
12249
  );
11713
12250
  });
11714
12251
  });
11715
- 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) {
12252
+ 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 action32(opts) {
11716
12253
  const creds = requireCredentials();
11717
12254
  const body5 = cleanBody({
11718
12255
  name: opts.name,
@@ -11741,7 +12278,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
11741
12278
  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(
11742
12279
  "--value-property <property>",
11743
12280
  "Numeric event property for value goals; pass null to clear"
11744
- ).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) {
12281
+ ).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 action33(id, opts) {
11745
12282
  const creds = requireCredentials();
11746
12283
  const body5 = cleanBody({
11747
12284
  name: opts.name,
@@ -11773,7 +12310,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
11773
12310
  var test = new Command("test").description("Dry run a goal URL pattern against traffic").option(
11774
12311
  "--pattern <regex>",
11775
12312
  "URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)"
11776
- ).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action33(opts) {
12313
+ ).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action34(opts) {
11777
12314
  if (!opts.pattern && !opts.goal)
11778
12315
  fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
11779
12316
  if (opts.pattern && opts.goal)
@@ -11855,7 +12392,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
11855
12392
  }
11856
12393
  );
11857
12394
  });
11858
- 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) {
12395
+ 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) {
11859
12396
  const creds = requireCredentials();
11860
12397
  const res = await suggestGoals(creds);
11861
12398
  if (!res.ok || !res.data)
@@ -11879,7 +12416,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
11879
12416
  const readline3 = await import("node:readline");
11880
12417
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
11881
12418
  const answer = await new Promise(
11882
- (resolve) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve)
12419
+ (resolve2) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve2)
11883
12420
  );
11884
12421
  rl.close();
11885
12422
  if (answer.toLowerCase() !== "y") continue;
@@ -11915,12 +12452,12 @@ Examples:
11915
12452
 
11916
12453
  // src/commands/funnels.ts
11917
12454
  init_source();
11918
- var import_node_fs13 = __toESM(require("node:fs"));
11919
- var import_node_path11 = __toESM(require("node:path"));
12455
+ var import_node_fs14 = __toESM(require("node:fs"));
12456
+ var import_node_path12 = __toESM(require("node:path"));
11920
12457
  function readSteps(command, file) {
11921
12458
  let parsed;
11922
12459
  try {
11923
- parsed = JSON.parse(import_node_fs13.default.readFileSync(import_node_path11.default.resolve(process.cwd(), file), "utf8"));
12460
+ parsed = JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(process.cwd(), file), "utf8"));
11924
12461
  } catch (error) {
11925
12462
  fail(
11926
12463
  command,
@@ -11945,7 +12482,7 @@ function printFunnel(funnel) {
11945
12482
  console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
11946
12483
  console.log();
11947
12484
  }
11948
- var list3 = new Command("list").alias("ls").description("List funnels").action(async function action35() {
12485
+ var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
11949
12486
  const res = await listFunnels(requireCredentials());
11950
12487
  if (!res.ok || !res.data)
11951
12488
  fail(
@@ -11964,7 +12501,7 @@ var list3 = new Command("list").alias("ls").description("List funnels").action(a
11964
12501
  )
11965
12502
  );
11966
12503
  });
11967
- 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) {
12504
+ 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) {
11968
12505
  const res = await createFunnel(requireCredentials(), {
11969
12506
  name: opts.name,
11970
12507
  steps: readSteps(this, opts.stepsFile)
@@ -11978,7 +12515,7 @@ var create3 = new Command("create").description("Create a funnel").requiredOptio
11978
12515
  );
11979
12516
  output(this, { funnel: res.data }, () => printFunnel(res.data));
11980
12517
  });
11981
- 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) {
12518
+ 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) {
11982
12519
  const res = await updateFunnel(requireCredentials(), id, {
11983
12520
  steps: readSteps(this, opts.stepsFile)
11984
12521
  });
@@ -11991,7 +12528,7 @@ var update2 = new Command("update").description("Replace a funnel's steps").argu
11991
12528
  );
11992
12529
  output(this, { funnel: res.data }, () => printFunnel(res.data));
11993
12530
  });
11994
- var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action38(id) {
12531
+ var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
11995
12532
  const res = await deleteFunnel(requireCredentials(), id);
11996
12533
  if (!res.ok)
11997
12534
  fail(
@@ -12002,7 +12539,7 @@ var del2 = new Command("delete").description("Delete a funnel").argument("<id>",
12002
12539
  );
12003
12540
  output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
12004
12541
  });
12005
- 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) {
12542
+ 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) {
12006
12543
  if (!/^[1-9]\d*$/.test(opts.days))
12007
12544
  fail(this, "--days must be a positive integer", EXIT_CODES.validation);
12008
12545
  const days = Number.parseInt(opts.days, 10);
@@ -12026,7 +12563,7 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
12026
12563
  )
12027
12564
  );
12028
12565
  });
12029
- var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action40(opts) {
12566
+ var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action41(opts) {
12030
12567
  const steps = readSteps(this, opts.stepsFile);
12031
12568
  output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
12032
12569
  });
@@ -12087,11 +12624,11 @@ async function load2(command) {
12087
12624
  }
12088
12625
  return result.data;
12089
12626
  }
12090
- var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action41() {
12627
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
12091
12628
  const { environments } = await load2(this);
12092
12629
  output(this, { environments }, () => printEnvironments(environments.items));
12093
12630
  });
12094
- 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) {
12631
+ 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) {
12095
12632
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12096
12633
  if (!normalizedDomain) {
12097
12634
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12138,7 +12675,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
12138
12675
  () => printEnvironments(result.data.environments.items)
12139
12676
  );
12140
12677
  });
12141
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action43(domain) {
12678
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
12142
12679
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12143
12680
  if (!normalizedDomain) {
12144
12681
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12202,11 +12739,11 @@ function printConsent(data) {
12202
12739
  `Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`
12203
12740
  );
12204
12741
  }
12205
- var get2 = new Command("get").description("Show consent settings").action(async function action44() {
12742
+ var get2 = new Command("get").description("Show consent settings").action(async function action45() {
12206
12743
  const data = await load3(this);
12207
12744
  output(this, data, () => printConsent(data));
12208
12745
  });
12209
- 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) {
12746
+ 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) {
12210
12747
  const consentMode = {
12211
12748
  enabled: true,
12212
12749
  provider: opts.provider,
@@ -12227,7 +12764,7 @@ var set = new Command("set").description("Enable and configure consent gating").
12227
12764
  warn(this, result.data.precompile);
12228
12765
  output(this, result.data, () => printConsent(result.data));
12229
12766
  });
12230
- var off = new Command("off").description("Disable consent gating").action(async function action46() {
12767
+ var off = new Command("off").description("Disable consent gating").action(async function action47() {
12231
12768
  const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
12232
12769
  if (!result.ok || !result.data)
12233
12770
  fail(
@@ -12241,40 +12778,283 @@ var off = new Command("off").description("Disable consent gating").action(async
12241
12778
  });
12242
12779
  var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
12243
12780
 
12244
- // src/commands/triggers.ts
12245
- var import_node_fs14 = __toESM(require("node:fs"));
12246
- var import_node_path12 = __toESM(require("node:path"));
12781
+ // src/commands/project-code.ts
12782
+ var import_node_fs15 = require("node:fs");
12783
+ var import_node_path13 = require("node:path");
12247
12784
 
12248
- // src/trigger-proposal.ts
12249
- function fieldsFrom(value, prefix = "") {
12250
- if (!value || typeof value !== "object") return [];
12251
- if (Array.isArray(value)) return value.flatMap((item) => fieldsFrom(item, prefix));
12252
- const record2 = value;
12253
- const explicitPath = typeof record2.path === "string" ? record2.path : void 0;
12254
- if (explicitPath) {
12255
- const rawValues = record2.values ?? record2.samples ?? record2.examples ?? record2.example;
12256
- const values = Array.isArray(rawValues) ? rawValues : rawValues === void 0 ? [] : [rawValues];
12257
- const magnitude = [record2.mean, record2.average, record2.max, record2.magnitude].find(
12258
- (candidate) => typeof candidate === "number"
12785
+ // src/commands/runtime-settings-shop.ts
12786
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12787
+ async function identifyRuntimeSettingsShop(command, requested) {
12788
+ let credentials = requireCredentials();
12789
+ if (credentials.credentialKind === "user") {
12790
+ let shopId = requested;
12791
+ if (!uuidPattern.test(requested)) {
12792
+ const context = await getAccountContext({
12793
+ ...credentials,
12794
+ resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
12795
+ });
12796
+ if (!context.ok || !context.data)
12797
+ fail(command, context.error ?? "Shop discovery failed", classifyStatus(context.status));
12798
+ const match2 = context.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
12799
+ if (!match2)
12800
+ fail(command, "--shop domain is not available to this user", EXIT_CODES.validation);
12801
+ shopId = match2.id;
12802
+ }
12803
+ credentials = { ...credentials, resolvedContext: { ...credentials.resolvedContext, shopId } };
12804
+ }
12805
+ const response = await getInstallDomains(credentials);
12806
+ if (!response.ok || !response.data)
12807
+ fail(command, response.error ?? "Shop identity unavailable", classifyStatus(response.status));
12808
+ const { shop_id, domain } = response.data;
12809
+ if (credentials.credentialKind === "user" && shop_id.toLowerCase() !== credentials.resolvedContext?.shopId?.toLowerCase()) {
12810
+ fail(
12811
+ command,
12812
+ "Shop identity does not match the requested user-key context",
12813
+ EXIT_CODES.validation
12259
12814
  );
12260
- return [{ path: explicitPath, values, magnitude }];
12261
12815
  }
12262
- return Object.entries(record2).flatMap(([key, child]) => {
12263
- if (["eventName", "event_name", "name", "count", "frequency", "evidence"].includes(key))
12264
- return [];
12265
- const next = prefix ? `${prefix}.${key}` : key;
12266
- if (child === null || ["string", "number", "boolean"].includes(typeof child)) {
12267
- return [{ path: next, values: [child] }];
12268
- }
12269
- if (Array.isArray(child) && child.every((item) => item === null || typeof item !== "object")) {
12270
- return [{ path: next, values: child }];
12271
- }
12272
- if (Array.isArray(child)) {
12816
+ if (requested.toLowerCase() !== shop_id.toLowerCase() && requested.toLowerCase() !== domain.toLowerCase()) {
12817
+ fail(command, "--shop does not match the authenticated shop", EXIT_CODES.validation);
12818
+ }
12819
+ setEnvelopeContext({ shop_id, credential: credentials.credentialKind ?? "shop" });
12820
+ return { shop_id, domain, credentials };
12821
+ }
12822
+
12823
+ // src/commands/project-code.ts
12824
+ var fields = ["enabled", "css", "helperJs", "js"];
12825
+ function readPatch(command, file) {
12826
+ let input;
12827
+ try {
12828
+ input = JSON.parse((0, import_node_fs15.readFileSync)(file, "utf8"));
12829
+ } catch (error) {
12830
+ fail(command, `Cannot read project code JSON: ${String(error)}`, EXIT_CODES.validation);
12831
+ }
12832
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length === 0) {
12833
+ fail(command, "Project code must be a nonempty partial object", EXIT_CODES.validation);
12834
+ }
12835
+ for (const [key, value] of Object.entries(input)) {
12836
+ if (!fields.includes(key))
12837
+ fail(command, `Unknown project code field: ${key}`, EXIT_CODES.validation);
12838
+ if (key === "enabled" ? typeof value !== "boolean" : value !== null && (typeof value !== "string" || value.length > 2e4)) {
12839
+ fail(
12840
+ command,
12841
+ `${key} must be ${key === "enabled" ? "a boolean" : "null or a string of at most 20,000 characters"}`,
12842
+ EXIT_CODES.validation
12843
+ );
12844
+ }
12845
+ }
12846
+ return input;
12847
+ }
12848
+ async function readCurrent(command, shop) {
12849
+ const response = await getRuntimeSettings(shop.credentials);
12850
+ if (!response.ok || !response.data)
12851
+ fail(
12852
+ command,
12853
+ response.error ?? "Runtime settings read failed",
12854
+ classifyStatus(response.status)
12855
+ );
12856
+ const data = response.data;
12857
+ if (data.shop_id !== shop.shop_id)
12858
+ fail(command, "Runtime settings returned a different shop", EXIT_CODES.validation);
12859
+ const projectCode = data.runtime_settings?.projectCode;
12860
+ if (!projectCode || typeof projectCode.enabled !== "boolean" || [projectCode.css, projectCode.helperJs, projectCode.js].some(
12861
+ (code) => typeof code !== "string"
12862
+ ) || !(data.version === null || typeof data.version === "string" && data.version.length > 0)) {
12863
+ fail(
12864
+ command,
12865
+ "Runtime settings must include complete project code and an opaque version",
12866
+ EXIT_CODES.general
12867
+ );
12868
+ }
12869
+ return { projectCode, version: data.version };
12870
+ }
12871
+ function effectiveDiff(before, patch) {
12872
+ const after = { ...before };
12873
+ if (patch.enabled !== void 0) after.enabled = patch.enabled;
12874
+ for (const key of ["css", "helperJs", "js"]) {
12875
+ if (patch[key] !== void 0) after[key] = patch[key]?.trim() || "";
12876
+ }
12877
+ const changedFields = fields.filter((field) => before[field] !== after[field]);
12878
+ const changes = Object.fromEntries(
12879
+ changedFields.map((field) => [field, { before: before[field], after: after[field] }])
12880
+ );
12881
+ return { changedFields, changes, before, after };
12882
+ }
12883
+ var projectCodeCommand = new Command("project-code").description(
12884
+ "Inspect, preview, and back up shop project code"
12885
+ );
12886
+ projectCodeCommand.addCommand(
12887
+ new Command("get").description("Read complete project code and its opaque version").requiredOption("--shop <domain-or-id>", "Shop domain or UUID").action(async (options, command) => {
12888
+ const { credentials: _credentials, ...identity } = await identifyRuntimeSettingsShop(
12889
+ command,
12890
+ options.shop
12891
+ );
12892
+ const current = await readCurrent(command, { ...identity, credentials: _credentials });
12893
+ output(
12894
+ command,
12895
+ { ...identity, ...current },
12896
+ () => process.stdout.write(`${JSON.stringify({ ...identity, ...current }, null, 2)}
12897
+ `)
12898
+ );
12899
+ })
12900
+ );
12901
+ for (const mode of ["diff", "set"]) {
12902
+ const command = new Command(mode).description(
12903
+ mode === "diff" ? "Preview an effective partial project code change without writing" : "Back up prior code and apply a version-checked partial change"
12904
+ ).requiredOption("--file <path>", "Partial projectCode JSON file").requiredOption("--shop <domain-or-id>", "Shop domain or UUID");
12905
+ if (mode === "set")
12906
+ command.requiredOption(
12907
+ "--backup <path>",
12908
+ "New backup file; an existing file is never replaced"
12909
+ );
12910
+ command.action(async (options, actionCommand) => {
12911
+ const patch = readPatch(actionCommand, options.file);
12912
+ const shop = await identifyRuntimeSettingsShop(actionCommand, options.shop);
12913
+ const current = await readCurrent(actionCommand, shop);
12914
+ const { credentials: _credentials, ...identity } = shop;
12915
+ const diff = effectiveDiff(current.projectCode, patch);
12916
+ if (mode === "diff" || diff.changedFields.length === 0) {
12917
+ const result2 = {
12918
+ ...identity,
12919
+ status: diff.changedFields.length ? "changed" : "unchanged",
12920
+ version: current.version,
12921
+ diff
12922
+ };
12923
+ output(
12924
+ actionCommand,
12925
+ result2,
12926
+ () => process.stdout.write(`${JSON.stringify(result2, null, 2)}
12927
+ `)
12928
+ );
12929
+ return;
12930
+ }
12931
+ const backupPath = (0, import_node_path13.resolve)(options.backup);
12932
+ try {
12933
+ (0, import_node_fs15.writeFileSync)(
12934
+ backupPath,
12935
+ `${JSON.stringify({ ...identity, ...current, capturedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
12936
+ `,
12937
+ { flag: "wx", mode: 384 }
12938
+ );
12939
+ } catch (error) {
12940
+ fail(
12941
+ actionCommand,
12942
+ `Backup failed; project code was not sent: ${String(error)}`,
12943
+ EXIT_CODES.general
12944
+ );
12945
+ }
12946
+ const response = await updateRuntimeSettings(shop.credentials, {
12947
+ projectCode: patch,
12948
+ expectedVersion: current.version
12949
+ });
12950
+ if (!response.ok || !response.data) {
12951
+ fail(
12952
+ actionCommand,
12953
+ response.error ?? "Project code update failed",
12954
+ classifyStatus(response.status),
12955
+ { ...response.errorDetails, backupPath }
12956
+ );
12957
+ }
12958
+ const data = response.data;
12959
+ const warnings = [...data.warnings ?? []];
12960
+ if (data.precompile?.status === "warning" || data.precompile?.status === "error") {
12961
+ warnings.push(
12962
+ `Project code saved; publication ${data.precompile.status}: ${data.precompile.summary}`
12963
+ );
12964
+ }
12965
+ if (data.shop_id !== identity.shop_id)
12966
+ warnings.push(
12967
+ "The write response has a different shop identity; inspect the saved state before another write."
12968
+ );
12969
+ for (const warning of warnings) process.stderr.write(`WARNING: ${warning}
12970
+ `);
12971
+ const result = {
12972
+ ...identity,
12973
+ status: "saved",
12974
+ diff,
12975
+ backupPath,
12976
+ version: data.version,
12977
+ writtenVersion: data.writtenVersion,
12978
+ precompile: data.precompile,
12979
+ warnings
12980
+ };
12981
+ output(
12982
+ actionCommand,
12983
+ result,
12984
+ () => process.stdout.write(`${JSON.stringify(result, null, 2)}
12985
+ `)
12986
+ );
12987
+ });
12988
+ projectCodeCommand.addCommand(command);
12989
+ }
12990
+
12991
+ // src/commands/triggers.ts
12992
+ var import_node_fs16 = __toESM(require("node:fs"));
12993
+ var import_node_path14 = __toESM(require("node:path"));
12994
+
12995
+ // src/trigger-proposal.ts
12996
+ function runtimePath(path24) {
12997
+ return path24.replaceAll("[]", ".0");
12998
+ }
12999
+ function fieldsFrom(value, prefix = "", descriptorList = false) {
13000
+ if (!value || typeof value !== "object") return [];
13001
+ if (Array.isArray(value)) {
13002
+ return value.flatMap(
13003
+ (item, index) => fieldsFrom(
13004
+ item,
13005
+ descriptorList ? prefix : prefix ? `${prefix}.${index}` : String(index),
13006
+ descriptorList
13007
+ )
13008
+ );
13009
+ }
13010
+ const record2 = value;
13011
+ const explicitPath = typeof record2.path === "string" ? record2.path : void 0;
13012
+ if (descriptorList && explicitPath) {
13013
+ const rawValues = record2.values ?? record2.samples ?? record2.examples ?? record2.example;
13014
+ const values = Array.isArray(rawValues) ? rawValues : rawValues === void 0 ? [] : [rawValues];
13015
+ const magnitude = [record2.mean, record2.average, record2.max, record2.magnitude].find(
13016
+ (candidate) => typeof candidate === "number"
13017
+ );
13018
+ const numericType = Array.isArray(record2.types) && record2.types.length > 0 && record2.types.every((type) => type === "num" || type === "numstr" || type === "numstr_comma");
13019
+ return [{ path: runtimePath(explicitPath), values, magnitude, numericType }];
13020
+ }
13021
+ return Object.entries(record2).flatMap(([key, child]) => {
13022
+ if (["eventName", "event_name", "name", "count", "frequency", "evidence"].includes(key))
13023
+ return [];
13024
+ const next = prefix ? `${prefix}.${key}` : key;
13025
+ if (child === null || ["string", "number", "boolean"].includes(typeof child)) {
13026
+ return [{ path: next, values: [child] }];
13027
+ }
13028
+ if (Array.isArray(child) && child.every((item) => item === null || typeof item !== "object")) {
13029
+ return [{ path: next, values: child }];
13030
+ }
13031
+ if (Array.isArray(child)) {
12273
13032
  return [{ path: next, values: [child] }, ...fieldsFrom(child, next)];
12274
13033
  }
12275
13034
  return fieldsFrom(child, next);
12276
13035
  });
12277
13036
  }
13037
+ function isDescriptorArray(value) {
13038
+ const descriptorKeys = /* @__PURE__ */ new Set([
13039
+ "path",
13040
+ "values",
13041
+ "samples",
13042
+ "examples",
13043
+ "example",
13044
+ "mean",
13045
+ "average",
13046
+ "max",
13047
+ "magnitude",
13048
+ "types",
13049
+ "seen",
13050
+ "type_collision"
13051
+ ]);
13052
+ return Array.isArray(value) && value.length > 0 && value.every((item) => {
13053
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
13054
+ const record2 = item;
13055
+ return typeof record2.path === "string" && record2.path.length > 0 && Object.keys(record2).every((key) => descriptorKeys.has(key));
13056
+ });
13057
+ }
12278
13058
  function candidatesFrom(report) {
12279
13059
  if (!report || typeof report !== "object") return [];
12280
13060
  const root = report;
@@ -12292,15 +13072,15 @@ function candidatesFrom(report) {
12292
13072
  }
12293
13073
  return [root];
12294
13074
  }
12295
- function score2(path23, patterns) {
13075
+ function score2(path24, patterns) {
12296
13076
  return patterns.reduce(
12297
- (total, pattern, index) => total + (pattern.test(path23) ? patterns.length - index : 0),
13077
+ (total, pattern, index) => total + (pattern.test(path24) ? patterns.length - index : 0),
12298
13078
  0
12299
13079
  );
12300
13080
  }
12301
- function best(fields, patterns, numeric2 = false) {
12302
- const ranked = fields.filter(
12303
- (field) => !numeric2 || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
13081
+ function best(fields2, patterns, numeric2 = false) {
13082
+ const ranked = fields2.filter(
13083
+ (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
12304
13084
  ).map((field) => ({ field, score: score2(field.path.toLowerCase(), patterns) })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || (b.field.magnitude ?? 0) - (a.field.magnitude ?? 0));
12305
13085
  if (!ranked.length) return [];
12306
13086
  const top = ranked[0].score;
@@ -12322,17 +13102,17 @@ function proposeTriggers(report) {
12322
13102
  eventName
12323
13103
  ))
12324
13104
  continue;
12325
- const fields = fieldsFrom(
12326
- candidate.fields ?? candidate.paths ?? candidate.properties ?? candidate.payload ?? candidate
12327
- );
13105
+ const descriptors = candidate.fields ?? candidate.paths;
13106
+ const payload = candidate.properties ?? candidate.payload ?? candidate;
13107
+ const fields2 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
12328
13108
  const revenues = best(
12329
- fields,
13109
+ fields2,
12330
13110
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
12331
13111
  true
12332
13112
  );
12333
- const orderIds = best(fields, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
12334
- const currencies = best(fields, [/currency/, /(^|\.)cur(rency)?$/]);
12335
- const items = best(fields, [/(^|\.)((line_?)?items|products)$/]);
13113
+ const orderIds = best(fields2, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
13114
+ const currencies = best(fields2, [/currency/, /(^|\.)cur(rency)?$/]);
13115
+ const items = best(fields2, [/(^|\.)((line_?)?items|products)$/]);
12336
13116
  if (!revenues.length || !orderIds.length || !currencies.length) continue;
12337
13117
  for (const revenue of revenues)
12338
13118
  for (const orderId of orderIds)
@@ -12394,6 +13174,20 @@ function proposeTriggers(report) {
12394
13174
  return output2;
12395
13175
  }
12396
13176
 
13177
+ // src/generated/runtime-settings-trigger-constants.json
13178
+ var runtime_settings_trigger_constants_default = {
13179
+ dotPathPattern: "^[A-Za-z_$][\\w$]*(?:\\.(?:\\d+|[A-Za-z_$][\\w$]*))*$",
13180
+ signalPathPattern: "^signal:[A-Za-z_$][\\w$]*$",
13181
+ valueTypes: ["string", "number", "boolean", "string_array", "json"],
13182
+ whenOps: ["eq", "neq", "gt", "lt", "exists", "not_exists"],
13183
+ maxWhenEntries: 5,
13184
+ maxSourcePaths: 5,
13185
+ maxTransformMapEntries: 20,
13186
+ maxTransformMapKeyChars: 100,
13187
+ maxTransformMapValueChars: 100,
13188
+ maxTriggers: 20
13189
+ };
13190
+
12397
13191
  // src/commands/triggers.ts
12398
13192
  var allowed = /* @__PURE__ */ new Set([
12399
13193
  "eventName",
@@ -12402,11 +13196,15 @@ var allowed = /* @__PURE__ */ new Set([
12402
13196
  "reEvaluate",
12403
13197
  "trackPageview",
12404
13198
  "trackEventName",
13199
+ "when",
12405
13200
  "trackRevenue",
12406
- "mappings"
13201
+ "mappings",
13202
+ "properties"
12407
13203
  ]);
12408
- var pathPattern = /^[A-Za-z_$][\w$]*(?:\.(?:\d+|[A-Za-z_$][\w$]*))*$/;
12409
- var valueTypes = /* @__PURE__ */ new Set(["string", "number", "boolean", "string_array", "json"]);
13204
+ var pathPattern = new RegExp(runtime_settings_trigger_constants_default.dotPathPattern);
13205
+ var signalPathPattern = new RegExp(runtime_settings_trigger_constants_default.signalPathPattern);
13206
+ var valueTypes = new Set(runtime_settings_trigger_constants_default.valueTypes);
13207
+ var whenOps = new Set(runtime_settings_trigger_constants_default.whenOps);
12410
13208
  function strictObject(value, location, keys) {
12411
13209
  if (!value || typeof value !== "object" || Array.isArray(value))
12412
13210
  throw new Error(`${location} must be an object`);
@@ -12426,26 +13224,126 @@ function strictPath(value, location, required = false) {
12426
13224
  `${location} must use dot-path syntax with property or array-index segments (for example: ecommerce.items.0.price); bracket syntax is not accepted`
12427
13225
  );
12428
13226
  }
13227
+ function strictSource(value, location, allowSignal, required = false) {
13228
+ if (value === void 0 && !required) return;
13229
+ const sources = Array.isArray(value) ? value : [value];
13230
+ if (Array.isArray(value) && (value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxSourcePaths))
13231
+ throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxSourcePaths} paths`);
13232
+ sources.forEach((source, index) => {
13233
+ const sourceLocation = Array.isArray(value) ? `${location}[${index}]` : location;
13234
+ if (allowSignal && typeof source === "string" && signalPathPattern.test(source)) return;
13235
+ strictPath(source, sourceLocation, true);
13236
+ });
13237
+ }
13238
+ function defaultMatchesValueType(value, valueType) {
13239
+ if (valueType === "number") {
13240
+ if (typeof value !== "string") return Number.isFinite(Number(value));
13241
+ const text = String(value).trim();
13242
+ const normalized = /^-?\d{1,3}(\.\d{3})+,\d+$/.test(text) ? text.replace(/\./g, "").replace(",", ".") : /^-?\d+,\d+$/.test(text) ? text.replace(",", ".") : text;
13243
+ return Number.isFinite(Number(normalized));
13244
+ }
13245
+ if (valueType === "boolean") {
13246
+ if (typeof value === "boolean") return true;
13247
+ return ["1", "true", "yes", "on", "0", "false", "no", "off"].includes(
13248
+ String(value).trim().toLowerCase()
13249
+ );
13250
+ }
13251
+ return true;
13252
+ }
13253
+ function strictTransform(value, valueType, location) {
13254
+ if (value === void 0) return;
13255
+ const transform = strictObject(
13256
+ value,
13257
+ location,
13258
+ /* @__PURE__ */ new Set(["trim", "lowercase", "map", "scale", "default"])
13259
+ );
13260
+ optionalType(transform.trim, "boolean", `${location}.trim`);
13261
+ optionalType(transform.lowercase, "boolean", `${location}.lowercase`);
13262
+ if (transform.map !== void 0) {
13263
+ if (!transform.map || typeof transform.map !== "object" || Array.isArray(transform.map))
13264
+ throw new Error(`${location}.map must be an object`);
13265
+ const map = transform.map;
13266
+ const entries = Object.entries(map);
13267
+ if (entries.length > runtime_settings_trigger_constants_default.maxTransformMapEntries)
13268
+ throw new Error(
13269
+ `${location}.map must contain at most ${runtime_settings_trigger_constants_default.maxTransformMapEntries} entries`
13270
+ );
13271
+ for (const [key, mappedValue] of entries) {
13272
+ if (key.length > runtime_settings_trigger_constants_default.maxTransformMapKeyChars)
13273
+ throw new Error(
13274
+ `${location}.map keys must be at most ${runtime_settings_trigger_constants_default.maxTransformMapKeyChars} characters`
13275
+ );
13276
+ if (typeof mappedValue !== "string" || mappedValue.length > runtime_settings_trigger_constants_default.maxTransformMapValueChars)
13277
+ throw new Error(
13278
+ `${location}.map.${key} must be a string of at most ${runtime_settings_trigger_constants_default.maxTransformMapValueChars} characters`
13279
+ );
13280
+ }
13281
+ }
13282
+ if (transform.scale !== void 0 && (typeof transform.scale !== "number" || !Number.isFinite(transform.scale) || transform.scale === 0))
13283
+ throw new Error(`${location}.scale must be finite and non-zero`);
13284
+ if (transform.default !== void 0 && !["string", "number", "boolean"].includes(typeof transform.default))
13285
+ throw new Error(`${location}.default must be a string, number, or boolean`);
13286
+ if (transform.default !== void 0 && !defaultMatchesValueType(transform.default, valueType))
13287
+ throw new Error(`${location}.default must coerce to the mapping valueType`);
13288
+ }
12429
13289
  function strictMapping(value, location, revenueProperty) {
12430
13290
  const keys = /* @__PURE__ */ new Set([
12431
13291
  "from",
12432
13292
  "to",
12433
13293
  "valueType",
13294
+ "transform",
12434
13295
  ...revenueProperty ? [] : ["ttlDays", "persist"]
12435
13296
  ]);
12436
13297
  const mapping = strictObject(value, location, keys);
12437
- strictPath(mapping.from, `${location}.from`, true);
13298
+ strictSource(mapping.from, `${location}.from`, revenueProperty, true);
12438
13299
  strictPath(mapping.to, `${location}.to`, true);
12439
13300
  if (mapping.valueType !== void 0 && !valueTypes.has(String(mapping.valueType)))
12440
13301
  throw new Error(`${location}.valueType must be string, number, boolean, string_array, or json`);
13302
+ strictTransform(mapping.transform, mapping.valueType, `${location}.transform`);
12441
13303
  if (!revenueProperty) {
12442
13304
  optionalType(mapping.ttlDays, "number", `${location}.ttlDays`);
12443
13305
  optionalType(mapping.persist, "boolean", `${location}.persist`);
12444
13306
  }
12445
13307
  }
13308
+ function strictWhen(value, location) {
13309
+ if (value === void 0) return;
13310
+ if (!Array.isArray(value) || value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxWhenEntries) {
13311
+ throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxWhenEntries} entries`);
13312
+ }
13313
+ value.forEach((entry, index) => {
13314
+ const entryLocation = `${location}[${index}]`;
13315
+ const condition = strictObject(entry, entryLocation, /* @__PURE__ */ new Set(["path", "op", "value"]));
13316
+ strictPath(condition.path, `${entryLocation}.path`, true);
13317
+ if (typeof condition.op !== "string" || !whenOps.has(condition.op)) {
13318
+ throw new Error(
13319
+ `${entryLocation}.op must be ${runtime_settings_trigger_constants_default.whenOps.slice(0, -1).join(", ")}, or ${runtime_settings_trigger_constants_default.whenOps.at(-1)}`
13320
+ );
13321
+ }
13322
+ const hasValue = Object.prototype.hasOwnProperty.call(condition, "value");
13323
+ if (condition.op === "exists" || condition.op === "not_exists") {
13324
+ if (hasValue) throw new Error(`${entryLocation}.value is forbidden for ${condition.op}`);
13325
+ return;
13326
+ }
13327
+ if (!hasValue) throw new Error(`${entryLocation}.value is required for ${condition.op}`);
13328
+ if (condition.op === "gt" || condition.op === "lt") {
13329
+ if (typeof condition.value !== "number" || !Number.isFinite(condition.value)) {
13330
+ throw new Error(`${entryLocation}.value must be a number for ${condition.op}`);
13331
+ }
13332
+ return;
13333
+ }
13334
+ if (!["string", "number", "boolean"].includes(typeof condition.value)) {
13335
+ throw new Error(`${entryLocation}.value must be a string, number, or boolean`);
13336
+ }
13337
+ if (typeof condition.value === "number" && !Number.isFinite(condition.value)) {
13338
+ throw new Error(`${entryLocation}.value must be finite`);
13339
+ }
13340
+ });
13341
+ }
12446
13342
  function strictTriggers(value) {
12447
- if (!Array.isArray(value) || value.length > 20)
12448
- throw new Error("trigger file must contain an array of at most 20 triggers");
13343
+ if (!Array.isArray(value) || value.length > runtime_settings_trigger_constants_default.maxTriggers)
13344
+ throw new Error(
13345
+ `trigger file must contain an array of at most ${runtime_settings_trigger_constants_default.maxTriggers} triggers`
13346
+ );
12449
13347
  return value.map((item, index) => {
12450
13348
  if (!item || typeof item !== "object" || Array.isArray(item))
12451
13349
  throw new Error(`trigger[${index}] must be an object`);
@@ -12458,6 +13356,7 @@ function strictTriggers(value) {
12458
13356
  optionalType(record2[key], "boolean", `trigger[${index}].${key}`);
12459
13357
  for (const key of ["dataLayerName", "trackEventName"])
12460
13358
  optionalType(record2[key], "string", `trigger[${index}].${key}`);
13359
+ strictWhen(record2.when, `trigger[${index}].when`);
12461
13360
  if (record2.trackRevenue !== void 0) {
12462
13361
  const revenue = strictObject(
12463
13362
  record2.trackRevenue,
@@ -12474,7 +13373,7 @@ function strictTriggers(value) {
12474
13373
  );
12475
13374
  optionalType(revenue.enabled, "boolean", `trigger[${index}].trackRevenue.enabled`);
12476
13375
  for (const key of ["revenueFrom", "orderIdFrom", "currencyFrom", "costFrom", "profitFrom"])
12477
- strictPath(revenue[key], `trigger[${index}].trackRevenue.${key}`);
13376
+ strictSource(revenue[key], `trigger[${index}].trackRevenue.${key}`, false);
12478
13377
  if (revenue.properties !== void 0) {
12479
13378
  if (!Array.isArray(revenue.properties))
12480
13379
  throw new Error(`trigger[${index}].trackRevenue.properties must be an array`);
@@ -12490,13 +13389,28 @@ function strictTriggers(value) {
12490
13389
  (mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].mappings[${mappingIndex}]`, false)
12491
13390
  );
12492
13391
  }
13392
+ if (record2.properties !== void 0) {
13393
+ if (!Array.isArray(record2.properties))
13394
+ throw new Error(`trigger[${index}].properties must be an array`);
13395
+ record2.properties.forEach(
13396
+ (mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].properties[${mappingIndex}]`, true)
13397
+ );
13398
+ }
12493
13399
  return record2;
12494
13400
  });
12495
13401
  }
12496
- var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13402
+ function writeWarnings(result) {
13403
+ const warnings = result.warnings;
13404
+ if (!Array.isArray(warnings)) return;
13405
+ for (const warning of warnings) {
13406
+ if (typeof warning === "string") process.stderr.write(`WARNING: ${warning}
13407
+ `);
13408
+ }
13409
+ }
13410
+ var uuidPattern2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12497
13411
  async function identify(command, requestedShop) {
12498
13412
  let credentials = requireCredentials();
12499
- if (credentials.credentialKind === "user" && !uuidPattern.test(requestedShop)) {
13413
+ if (credentials.credentialKind === "user" && !uuidPattern2.test(requestedShop)) {
12500
13414
  const discoveryCredentials = {
12501
13415
  ...credentials,
12502
13416
  resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
@@ -12569,7 +13483,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
12569
13483
  var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger array").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(async function(opts) {
12570
13484
  let triggers;
12571
13485
  try {
12572
- triggers = strictTriggers(JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(opts.file), "utf8")));
13486
+ triggers = strictTriggers(JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.file), "utf8")));
12573
13487
  } catch (error) {
12574
13488
  fail(
12575
13489
  this,
@@ -12584,20 +13498,19 @@ var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger arra
12584
13498
  updated_at: (/* @__PURE__ */ new Date()).toISOString(),
12585
13499
  ...data
12586
13500
  };
12587
- output(
12588
- this,
12589
- result,
12590
- () => process.stdout.write(
13501
+ output(this, result, () => {
13502
+ process.stdout.write(
12591
13503
  `TRIGGERS_SET shop_id=${identity.shop_id} domain=${identity.domain} updated_at=${result.updated_at} count=${triggers.length}
12592
13504
  `
12593
- )
12594
- );
13505
+ );
13506
+ writeWarnings(result);
13507
+ });
12595
13508
  });
12596
13509
  var propose = new Command("propose").requiredOption("--from-file <u2-report.json>", "U2 discovery report").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(function(opts) {
12597
13510
  let proposals;
12598
13511
  let report;
12599
13512
  try {
12600
- report = JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(opts.fromFile), "utf8"));
13513
+ report = JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.fromFile), "utf8"));
12601
13514
  proposals = proposeTriggers(report);
12602
13515
  } catch (error) {
12603
13516
  fail(this, error instanceof Error ? error.message : String(error), 2);
@@ -12656,14 +13569,13 @@ var recipe = new Command("apply-recipe").argument("<recipeId>").requiredOption("
12656
13569
  domain: identity.domain,
12657
13570
  applied_at: (/* @__PURE__ */ new Date()).toISOString()
12658
13571
  };
12659
- output(
12660
- this,
12661
- result,
12662
- () => process.stdout.write(
13572
+ output(this, result, () => {
13573
+ process.stdout.write(
12663
13574
  `TRIGGERS_APPLIED shop_id=${identity.shop_id} domain=${identity.domain} applied_at=${result.applied_at} recipe=${recipeId}
12664
13575
  `
12665
- )
12666
- );
13576
+ );
13577
+ writeWarnings(result);
13578
+ });
12667
13579
  });
12668
13580
  var enable = new Command("enable-capture").requiredOption("--hours <n>").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").option("--url-pattern <p>", "URL pattern", collect, []).action(async function(opts) {
12669
13581
  const hours = Number(opts.hours);
@@ -12750,11 +13662,11 @@ async function patchWithRetry(command, build) {
12750
13662
  warn2(command, result.data.precompile);
12751
13663
  return result.data;
12752
13664
  }
12753
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action47() {
13665
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
12754
13666
  const data = await load4(this);
12755
13667
  output(this, data, () => printDomains(data));
12756
13668
  });
12757
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action48(domain) {
13669
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
12758
13670
  const added = domain.map(normalizeDomain);
12759
13671
  const data = await patchWithRetry(
12760
13672
  this,
@@ -12762,7 +13674,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
12762
13674
  );
12763
13675
  output(this, data, () => printDomains(data));
12764
13676
  });
12765
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action49(domain) {
13677
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
12766
13678
  const removeSet = new Set(domain.map(normalizeDomain));
12767
13679
  const data = await patchWithRetry(
12768
13680
  this,
@@ -12770,7 +13682,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
12770
13682
  );
12771
13683
  output(this, data, () => printDomains(data));
12772
13684
  });
12773
- var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action50(domain) {
13685
+ var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
12774
13686
  const replacement = domain.map(normalizeDomain);
12775
13687
  const data = await patchWithRetry(this, () => replacement);
12776
13688
  output(this, data, () => printDomains(data));
@@ -12862,14 +13774,14 @@ async function patchWithRetry2(command, build) {
12862
13774
  );
12863
13775
  return result.data;
12864
13776
  }
12865
- var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action51() {
13777
+ var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
12866
13778
  const data = await load5(this);
12867
13779
  output(this, data, () => printSettings(data.externalTracking));
12868
13780
  });
12869
13781
  var set4 = new Command("set").description("Enable cross-domain identity handover").requiredOption("--domain <domain...>", "Bound handover domain(s)").option("--param <param>", "Handover query parameter").option(
12870
13782
  "--signed",
12871
13783
  "Require signed handover tokens (the default; unsigning is a platform-support action)"
12872
- ).action(async function action52(opts) {
13784
+ ).action(async function action53(opts) {
12873
13785
  const domains = opts.domain.map(normalizeDomain2);
12874
13786
  if (domains.some((domain) => !domain))
12875
13787
  fail(
@@ -12889,14 +13801,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
12889
13801
  }));
12890
13802
  output(this, data, () => printSettings(data.externalTracking));
12891
13803
  });
12892
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action53() {
13804
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
12893
13805
  const data = await patchWithRetry2(this, (current) => ({
12894
13806
  externalTracking: { domains: [], signed: true },
12895
13807
  ifRevision: current.revision
12896
13808
  }));
12897
13809
  output(this, data, () => printSettings(data.externalTracking));
12898
13810
  });
12899
- 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) {
13811
+ 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) {
12900
13812
  const config = await load5(this);
12901
13813
  const target = normalizeDomain2(opts.toDomain);
12902
13814
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
@@ -13939,7 +14851,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
13939
14851
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
13940
14852
  "--worker <url>",
13941
14853
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
13942
- ).action(async function action55(options) {
14854
+ ).action(async function action56(options) {
13943
14855
  const data = await runDoctor(options);
13944
14856
  if (options.check && data.checks.length === 0)
13945
14857
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
@@ -13961,7 +14873,7 @@ function printDoctorReport(checks) {
13961
14873
  var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
13962
14874
  "--since <date>",
13963
14875
  "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
13964
- ).action(async function action56(opts) {
14876
+ ).action(async function action57(opts) {
13965
14877
  if (opts.since !== void 0 && parseDay(opts.since) === null)
13966
14878
  fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
13967
14879
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
@@ -14021,7 +14933,7 @@ function analyticsSubcommand(name, description, defaultDays) {
14021
14933
  command.option("--planning-days <n>", "Planning window in days", "180");
14022
14934
  command.option("--limit <n>", "Maximum page rows", "50");
14023
14935
  }
14024
- return command.action(async function action70(opts) {
14936
+ return command.action(async function action71(opts) {
14025
14937
  const result = await getAnalytics(requireCredentials(), name, {
14026
14938
  days: positiveInteger(this, opts.days, "--days"),
14027
14939
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -14041,7 +14953,7 @@ function urlsCommand() {
14041
14953
  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(
14042
14954
  "--match <regex>",
14043
14955
  "Client-side regex filter; exhausts inventory pages before filtering"
14044
- ).option("--limit <n>", "Maximum rows to print", "50").action(async function action70(opts) {
14956
+ ).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
14045
14957
  const creds = requireCredentials();
14046
14958
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
14047
14959
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -14230,15 +15142,15 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14230
15142
  `Response from ${hostname} uses unsupported content encoding "${encoding}"`
14231
15143
  )
14232
15144
  );
14233
- return new Promise((resolve, reject) => {
15145
+ return new Promise((resolve2, reject) => {
14234
15146
  const stream = decoderFor(encoding);
14235
15147
  const chunks = [];
14236
15148
  let bytes = 0;
14237
15149
  let settled = false;
14238
- const settle = (action70) => {
15150
+ const settle = (action71) => {
14239
15151
  if (settled) return;
14240
15152
  settled = true;
14241
- action70();
15153
+ action71();
14242
15154
  };
14243
15155
  stream.on("data", (chunk) => {
14244
15156
  if (settled) return;
@@ -14250,7 +15162,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14250
15162
  }
14251
15163
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
14252
15164
  stream.destroy();
14253
- settle(() => resolve({ body: Buffer.concat(chunks), truncated: true }));
15165
+ settle(() => resolve2({ body: Buffer.concat(chunks), truncated: true }));
14254
15166
  return;
14255
15167
  }
14256
15168
  chunks.push(chunk);
@@ -14260,7 +15172,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14260
15172
  "error",
14261
15173
  (error) => settle(() => {
14262
15174
  if (limits.onExceed === "truncate" && inputTruncated && chunks.length > 0)
14263
- resolve({ body: Buffer.concat(chunks), truncated: true });
15175
+ resolve2({ body: Buffer.concat(chunks), truncated: true });
14264
15176
  else
14265
15177
  reject(
14266
15178
  new BodyDecodeError(
@@ -14271,7 +15183,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14271
15183
  );
14272
15184
  stream.on(
14273
15185
  "end",
14274
- () => settle(() => resolve({ body: Buffer.concat(chunks), truncated: false }))
15186
+ () => settle(() => resolve2({ body: Buffer.concat(chunks), truncated: false }))
14275
15187
  );
14276
15188
  stream.end(raw);
14277
15189
  });
@@ -14298,7 +15210,7 @@ function responseFromIncoming(res, body5, decoded) {
14298
15210
  });
14299
15211
  }
14300
15212
  function pinnedFetch(url, addresses, limits) {
14301
- return new Promise((resolve, reject) => {
15213
+ return new Promise((resolve2, reject) => {
14302
15214
  const send3 = url.protocol === "https:" ? import_node_https.request : import_node_http2.request;
14303
15215
  const pinnedLookup = (_hostname, options, callback) => {
14304
15216
  if (options?.all)
@@ -14309,11 +15221,11 @@ function pinnedFetch(url, addresses, limits) {
14309
15221
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
14310
15222
  };
14311
15223
  let settled = false;
14312
- const settle = (action70) => {
15224
+ const settle = (action71) => {
14313
15225
  if (settled) return;
14314
15226
  settled = true;
14315
15227
  clearTimeout(deadline);
14316
- action70();
15228
+ action71();
14317
15229
  };
14318
15230
  const req = send3(
14319
15231
  url,
@@ -14345,7 +15257,7 @@ function pinnedFetch(url, addresses, limits) {
14345
15257
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
14346
15258
  req.destroy();
14347
15259
  settle(
14348
- () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve, reject)
15260
+ () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve2, reject)
14349
15261
  );
14350
15262
  return;
14351
15263
  }
@@ -14360,7 +15272,7 @@ function pinnedFetch(url, addresses, limits) {
14360
15272
  reject(new Error(`No HTTP status from ${url.hostname}`));
14361
15273
  return;
14362
15274
  }
14363
- finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve, reject);
15275
+ finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve2, reject);
14364
15276
  })
14365
15277
  );
14366
15278
  }
@@ -14648,7 +15560,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
14648
15560
  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(
14649
15561
  "--worker-url <url>",
14650
15562
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
14651
- ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action57(opts) {
15563
+ ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
14652
15564
  const creds = requireCredentials();
14653
15565
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
14654
15566
  if (!shopId)
@@ -14792,8 +15704,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
14792
15704
 
14793
15705
  // src/commands/flags.ts
14794
15706
  init_source();
14795
- var import_node_fs15 = __toESM(require("node:fs"));
14796
- var import_node_path13 = __toESM(require("node:path"));
15707
+ var import_node_fs17 = __toESM(require("node:fs"));
15708
+ var import_node_path15 = __toESM(require("node:path"));
14797
15709
 
14798
15710
  // src/edge-status.ts
14799
15711
  function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
@@ -14835,7 +15747,7 @@ function body(command, value, opts) {
14835
15747
  let parsed = {};
14836
15748
  if (value) {
14837
15749
  const source = value.startsWith("@") ? value.slice(1) : value;
14838
- 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;
15750
+ 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;
14839
15751
  try {
14840
15752
  parsed = JSON.parse(text);
14841
15753
  } catch (e) {
@@ -14948,11 +15860,11 @@ function write(name) {
14948
15860
  });
14949
15861
  return c;
14950
15862
  }
14951
- function lifecycle(action70) {
14952
- return new Command(action70).description(`${action70[0].toUpperCase()}${action70.slice(1)} a flag`).argument("<id>").action(async function(id) {
14953
- const r = await transitionManagedDelivery(requireCredentials(), kind, id, action70);
15863
+ function lifecycle(action71) {
15864
+ return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
15865
+ const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
14954
15866
  if (!r.ok || !r.data)
14955
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15867
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
14956
15868
  output(this, { flag: r.data }, () => detail(r.data));
14957
15869
  });
14958
15870
  }
@@ -14968,8 +15880,8 @@ Examples:
14968
15880
  );
14969
15881
 
14970
15882
  // src/commands/backlog.ts
14971
- var import_node_fs16 = __toESM(require("node:fs"));
14972
- var import_node_path14 = __toESM(require("node:path"));
15883
+ var import_node_fs18 = __toESM(require("node:fs"));
15884
+ var import_node_path16 = __toESM(require("node:path"));
14973
15885
  init_source();
14974
15886
  var kind2 = "backlog";
14975
15887
  var singular = "backlog item";
@@ -14977,7 +15889,7 @@ function parse(command, value) {
14977
15889
  if (!value) return {};
14978
15890
  try {
14979
15891
  const source = value.startsWith("@") ? value.slice(1) : value;
14980
- 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;
15892
+ 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;
14981
15893
  const body5 = JSON.parse(text);
14982
15894
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
14983
15895
  throw new Error("body must be an object");
@@ -15027,24 +15939,24 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
15027
15939
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15028
15940
  output(this, { backlog_item: r.data }, () => detail2(r.data));
15029
15941
  });
15030
- function write2(action70) {
15031
- const c = new Command(action70).description(
15032
- `${action70 === "create" ? "Create" : "Update"} a ${singular}`
15942
+ function write2(action71) {
15943
+ const c = new Command(action71).description(
15944
+ `${action71 === "create" ? "Create" : "Update"} a ${singular}`
15033
15945
  );
15034
- if (action70 === "update") c.argument("<id>", `${singular} ID`);
15946
+ if (action71 === "update") c.argument("<id>", `${singular} ID`);
15035
15947
  c.argument("[json]", "Inline JSON, @file, or file path").option(
15036
15948
  "--file <path>",
15037
15949
  "JSON body file"
15038
15950
  );
15039
15951
  c.action(async function(...args) {
15040
15952
  const o = this.opts();
15041
- const id = action70 === "update" ? String(args[0]) : void 0;
15042
- const raw = o.file ? `@${o.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
15953
+ const id = action71 === "update" ? String(args[0]) : void 0;
15954
+ const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15043
15955
  const body5 = parse(this, raw);
15044
15956
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
15045
- const r = action70 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
15957
+ const r = action71 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
15046
15958
  if (!r.ok || !r.data)
15047
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15959
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15048
15960
  output(this, { backlog_item: r.data }, () => detail2(r.data));
15049
15961
  });
15050
15962
  return c;
@@ -15077,7 +15989,7 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15077
15989
  const readline3 = await import("node:readline");
15078
15990
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
15079
15991
  const answer = await new Promise(
15080
- (resolve) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve)
15992
+ (resolve2) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve2)
15081
15993
  );
15082
15994
  rl.close();
15083
15995
  if (answer.toLowerCase() !== "y") {
@@ -15093,8 +16005,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15093
16005
  var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
15094
16006
 
15095
16007
  // src/commands/signals.ts
15096
- var import_node_fs17 = __toESM(require("node:fs"));
15097
- var import_node_path15 = __toESM(require("node:path"));
16008
+ var import_node_fs19 = __toESM(require("node:fs"));
16009
+ var import_node_path17 = __toESM(require("node:path"));
15098
16010
  init_source();
15099
16011
  var kind3 = "signals";
15100
16012
  var singular2 = "signal";
@@ -15102,7 +16014,7 @@ function parse2(command, value) {
15102
16014
  if (!value) return {};
15103
16015
  try {
15104
16016
  const source = value.startsWith("@") ? value.slice(1) : value;
15105
- 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;
16017
+ const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
15106
16018
  const body5 = JSON.parse(text);
15107
16019
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15108
16020
  throw new Error("body must be an object");
@@ -15152,24 +16064,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
15152
16064
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15153
16065
  output(this, { signal: r.data }, () => detail3(r.data));
15154
16066
  });
15155
- function write3(action70) {
15156
- const c = new Command(action70).description(
15157
- `${action70 === "create" ? "Create" : "Update"} a ${singular2}`
16067
+ function write3(action71) {
16068
+ const c = new Command(action71).description(
16069
+ `${action71 === "create" ? "Create" : "Update"} a ${singular2}`
15158
16070
  );
15159
- if (action70 === "update") c.argument("<id>", `${singular2} ID`);
16071
+ if (action71 === "update") c.argument("<id>", `${singular2} ID`);
15160
16072
  c.argument("[json]", "Inline JSON, @file, or file path").option(
15161
16073
  "--file <path>",
15162
16074
  "JSON body file"
15163
16075
  );
15164
16076
  c.action(async function(...args) {
15165
16077
  const o = this.opts();
15166
- const id = action70 === "update" ? String(args[0]) : void 0;
15167
- const raw = o.file ? `@${o.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
16078
+ const id = action71 === "update" ? String(args[0]) : void 0;
16079
+ const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15168
16080
  const body5 = parse2(this, raw);
15169
16081
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
15170
- const r = action70 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
16082
+ const r = action71 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
15171
16083
  if (!r.ok || !r.data)
15172
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
16084
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15173
16085
  output(this, { signal: r.data }, () => detail3(r.data));
15174
16086
  });
15175
16087
  return c;
@@ -15184,14 +16096,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
15184
16096
 
15185
16097
  // src/commands/personalizations.ts
15186
16098
  init_source();
15187
- var import_node_fs18 = __toESM(require("node:fs"));
15188
- var import_node_path16 = __toESM(require("node:path"));
16099
+ var import_node_fs20 = __toESM(require("node:fs"));
16100
+ var import_node_path18 = __toESM(require("node:path"));
15189
16101
  var kind4 = "personalizations";
15190
16102
  function body2(command, value, opts) {
15191
16103
  let parsed = {};
15192
16104
  if (value) {
15193
16105
  const source = value.startsWith("@") ? value.slice(1) : value;
15194
- 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;
16106
+ const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
15195
16107
  try {
15196
16108
  parsed = JSON.parse(text);
15197
16109
  } catch (e) {
@@ -15261,11 +16173,11 @@ function write4(name) {
15261
16173
  });
15262
16174
  return c;
15263
16175
  }
15264
- function lifecycle2(action70) {
15265
- return new Command(action70).description(`${action70[0].toUpperCase()}${action70.slice(1)} a personalization`).argument("<id>").action(async function(id) {
15266
- const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action70);
16176
+ function lifecycle2(action71) {
16177
+ return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
16178
+ const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
15267
16179
  if (!r.ok || !r.data)
15268
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
16180
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15269
16181
  output(this, { personalization: r.data }, () => detail4(r.data));
15270
16182
  });
15271
16183
  }
@@ -15273,13 +16185,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
15273
16185
 
15274
16186
  // src/commands/targeting-resources.ts
15275
16187
  init_source();
15276
- var import_node_fs19 = __toESM(require("node:fs"));
15277
- var import_node_path17 = __toESM(require("node:path"));
16188
+ var import_node_fs21 = __toESM(require("node:fs"));
16189
+ var import_node_path19 = __toESM(require("node:path"));
15278
16190
  function body3(command, value) {
15279
16191
  if (!value) return {};
15280
16192
  const source = value.startsWith("@") ? value.slice(1) : value;
15281
16193
  try {
15282
- const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
16194
+ const text = value.startsWith("@") || import_node_fs21.default.existsSync(source) ? import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), source), "utf8") : value;
15283
16195
  return JSON.parse(text);
15284
16196
  } catch (error) {
15285
16197
  fail(
@@ -15347,21 +16259,21 @@ function targetingResourceCommand(kind5) {
15347
16259
  );
15348
16260
  })
15349
16261
  );
15350
- for (const action70 of ["create", "update"]) {
15351
- const command = new Command(action70).description(
15352
- `${action70 === "create" ? "Create" : "Update"} a ${singular3}`
16262
+ for (const action71 of ["create", "update"]) {
16263
+ const command = new Command(action71).description(
16264
+ `${action71 === "create" ? "Create" : "Update"} a ${singular3}`
15353
16265
  );
15354
- if (action70 === "update") command.argument("<id>");
16266
+ if (action71 === "update") command.argument("<id>");
15355
16267
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
15356
16268
  const opts = this.opts();
15357
- const id = action70 === "update" ? String(args[0]) : void 0;
15358
- const value = opts.file ? `@${opts.file}` : String(args[action70 === "update" ? 1 : 0] ?? "") || void 0;
16269
+ const id = action71 === "update" ? String(args[0]) : void 0;
16270
+ const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15359
16271
  const requestBody = body3(this, value);
15360
- if (action70 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
16272
+ if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
15361
16273
  fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
15362
16274
  const response = await requestTargetingResource(
15363
16275
  requireCredentials(),
15364
- action70 === "create" ? "POST" : "PATCH",
16276
+ action71 === "create" ? "POST" : "PATCH",
15365
16277
  kind5,
15366
16278
  id,
15367
16279
  requestBody
@@ -15369,7 +16281,7 @@ function targetingResourceCommand(kind5) {
15369
16281
  if (!response.ok || !response.data)
15370
16282
  fail(
15371
16283
  this,
15372
- response.error ?? `Failed to ${action70}`,
16284
+ response.error ?? `Failed to ${action71}`,
15373
16285
  classifyStatus(response.status),
15374
16286
  response.errorDetails
15375
16287
  );
@@ -15378,7 +16290,7 @@ function targetingResourceCommand(kind5) {
15378
16290
  {
15379
16291
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
15380
16292
  },
15381
- () => console.log(source_default.green(`${action70}d ${singular3}`))
16293
+ () => console.log(source_default.green(`${action71}d ${singular3}`))
15382
16294
  );
15383
16295
  });
15384
16296
  root.addCommand(command);
@@ -15399,8 +16311,8 @@ function targetingResourceCommand(kind5) {
15399
16311
  input: process.stdin,
15400
16312
  output: process.stderr
15401
16313
  });
15402
- const answer = await new Promise((resolve) => {
15403
- rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve);
16314
+ const answer = await new Promise((resolve2) => {
16315
+ rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve2);
15404
16316
  });
15405
16317
  rl.close();
15406
16318
  if (answer.toLowerCase() !== "y") {
@@ -15452,7 +16364,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
15452
16364
  // src/commands/brand.ts
15453
16365
  init_source();
15454
16366
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
15455
- var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action58() {
16367
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
15456
16368
  const creds = requireCredentials();
15457
16369
  const res = await getBrandContext(creds);
15458
16370
  if (res.status === 404) {
@@ -15485,7 +16397,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
15485
16397
 
15486
16398
  // src/commands/targeting.ts
15487
16399
  init_source();
15488
- 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) {
16400
+ 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) {
15489
16401
  const creds = requireCredentials();
15490
16402
  const res = await testTargeting(creds, url, { status: opts.status });
15491
16403
  if (!res.ok || !res.data) {
@@ -15530,13 +16442,21 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
15530
16442
 
15531
16443
  // src/commands/sos.ts
15532
16444
  init_source();
15533
- var import_node_fs20 = __toESM(require("node:fs"));
15534
- var import_node_path18 = __toESM(require("node:path"));
15535
- var SOS_TYPES = /* @__PURE__ */ new Set(["full_handover", "design", "development", "qa", "config", "bug"]);
16445
+ var import_node_fs22 = __toESM(require("node:fs"));
16446
+ var import_node_path20 = __toESM(require("node:path"));
16447
+ var SOS_TYPES = /* @__PURE__ */ new Set([
16448
+ "full_handover",
16449
+ "design",
16450
+ "development",
16451
+ "qa",
16452
+ "config",
16453
+ "bug",
16454
+ "help"
16455
+ ]);
15536
16456
  function readText(value) {
15537
16457
  if (!value) return void 0;
15538
16458
  if (!value.startsWith("@")) return value;
15539
- return import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), value.slice(1)), "utf8");
16459
+ return import_node_fs22.default.readFileSync(import_node_path20.default.resolve(process.cwd(), value.slice(1)), "utf8");
15540
16460
  }
15541
16461
  function printTicket(ticket, reused) {
15542
16462
  console.log();
@@ -15547,11 +16467,26 @@ function printTicket(ticket, reused) {
15547
16467
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
15548
16468
  console.log();
15549
16469
  }
15550
- 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) {
16470
+ function printCreatedTicket(result) {
16471
+ if ("reused" in result) {
16472
+ printTicket(result.ticket, result.reused);
16473
+ return;
16474
+ }
16475
+ console.log();
16476
+ console.log(source_default.bold(result.deduped ? "Reused customer ticket" : "Created customer ticket"));
16477
+ console.log(` Reference: ${source_default.dim(result.ticket.reference)}`);
16478
+ console.log(` Status: ${result.ticket.status}`);
16479
+ console.log();
16480
+ }
16481
+ var create4 = new Command("create").description("Create an Apex SOS ticket").option(
16482
+ "--type <type>",
16483
+ "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
16484
+ "qa"
16485
+ ).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) {
15551
16486
  if (!SOS_TYPES.has(opts.type)) {
15552
16487
  fail(
15553
16488
  this,
15554
- "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.",
16489
+ "Invalid SOS type. Use full_handover, design, development, qa, config, bug, or help. Internal types require a DRIP account.",
15555
16490
  3
15556
16491
  );
15557
16492
  }
@@ -15572,9 +16507,9 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
15572
16507
  res.errorDetails
15573
16508
  );
15574
16509
  }
15575
- output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
16510
+ output(this, res.data, () => printCreatedTicket(res.data));
15576
16511
  });
15577
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action61() {
16512
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
15578
16513
  const creds = requireCredentials();
15579
16514
  const res = await listMySos(creds);
15580
16515
  if (!res.ok || !res.data) {
@@ -15655,13 +16590,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
15655
16590
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
15656
16591
 
15657
16592
  // src/commands/shops.ts
15658
- var import_node_fs21 = __toESM(require("node:fs"));
15659
- var import_node_path19 = __toESM(require("node:path"));
16593
+ var import_node_fs23 = __toESM(require("node:fs"));
16594
+ var import_node_path21 = __toESM(require("node:path"));
15660
16595
  function body4(command, raw) {
15661
16596
  if (!raw) return {};
15662
16597
  try {
15663
16598
  const source = raw.startsWith("@") ? raw.slice(1) : raw;
15664
- const text = raw.startsWith("@") || import_node_fs21.default.existsSync(source) ? import_node_fs21.default.readFileSync(import_node_path19.default.resolve(source), "utf8") : raw;
16599
+ const text = raw.startsWith("@") || import_node_fs23.default.existsSync(source) ? import_node_fs23.default.readFileSync(import_node_path21.default.resolve(source), "utf8") : raw;
15665
16600
  const value = JSON.parse(text);
15666
16601
  if (!value || typeof value !== "object" || Array.isArray(value))
15667
16602
  throw new Error("body must be an object");
@@ -15889,7 +16824,7 @@ function commandSchema(command) {
15889
16824
  };
15890
16825
  }
15891
16826
  function createSchemaCommand(program3) {
15892
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action70() {
16827
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action71() {
15893
16828
  const schema = commandSchema(program3);
15894
16829
  output(this, schema, () => {
15895
16830
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -16042,7 +16977,7 @@ function parseContainer(source, id, dataLayerName = "dataLayer") {
16042
16977
  vtpPaths,
16043
16978
  ecommerceFrom: [...ecommerce][0] ?? null,
16044
16979
  serverContainerUrl: [...serverUrls][0] ?? null,
16045
- piiPaths: vtpPaths.filter((path23) => /email|phone|address|name/i.test(path23))
16980
+ piiPaths: vtpPaths.filter((path24) => /email|phone|address|name/i.test(path24))
16046
16981
  }
16047
16982
  };
16048
16983
  }
@@ -16101,6 +17036,145 @@ function minutesValue(value) {
16101
17036
  throw new CliError("--minutes must be an integer between 1 and 20160", EXIT_CODES.validation);
16102
17037
  return parsed;
16103
17038
  }
17039
+ function isStableField(field) {
17040
+ return field.types.length > 0 && !field.type_collision;
17041
+ }
17042
+ function baseFieldTypes(field) {
17043
+ return new Set(field.types.map((type) => type.split(":")[0]));
17044
+ }
17045
+ function isContainerField(field) {
17046
+ const types = baseFieldTypes(field);
17047
+ return types.has("obj") || types.has("arr");
17048
+ }
17049
+ function hasSyntheticPlaceholder(path24) {
17050
+ return path24.split(".").map((segment) => segment.replace(/\[\]$/, "")).some((segment) => /^<[^>]+>$/.test(segment));
17051
+ }
17052
+ function ambiguousDottedPrefix(event, path24) {
17053
+ const segments = path24.split(".");
17054
+ for (let index = 1; index < segments.length; index += 1) {
17055
+ const prefix = segments.slice(0, index).join(".");
17056
+ const parent = event.fields.find((field) => field.path === prefix);
17057
+ if (!parent || !isContainerField(parent)) return prefix;
17058
+ }
17059
+ return void 0;
17060
+ }
17061
+ function dimensionEligibility(event, field) {
17062
+ const path24 = field.path;
17063
+ if (hasSyntheticPlaceholder(path24)) {
17064
+ return {
17065
+ eligible: false,
17066
+ message: `Path ${JSON.stringify(path24)} is a synthetic placeholder and cannot create a dimension. Choose an observed scalar path.`
17067
+ };
17068
+ }
17069
+ if (isContainerField(field)) {
17070
+ return {
17071
+ eligible: false,
17072
+ message: `Path ${JSON.stringify(path24)} is container-typed. Choose an observed scalar path.`
17073
+ };
17074
+ }
17075
+ if (path24.includes("[]")) {
17076
+ return {
17077
+ eligible: false,
17078
+ message: `Path ${JSON.stringify(path24)} includes an array-element segment and cannot create a reliable dimension. Choose a scalar path outside an array.`
17079
+ };
17080
+ }
17081
+ if (!isStableField(field)) {
17082
+ return {
17083
+ eligible: false,
17084
+ message: `Path ${JSON.stringify(path24)} does not have a stable observed type. Choose a path without a type collision.`
17085
+ };
17086
+ }
17087
+ if (!event.event) {
17088
+ return {
17089
+ eligible: false,
17090
+ message: `Path ${JSON.stringify(path24)} has no observed event name. Choose a path from a named event.`
17091
+ };
17092
+ }
17093
+ const ambiguousPrefix = ambiguousDottedPrefix(event, path24);
17094
+ if (ambiguousPrefix) {
17095
+ return {
17096
+ eligible: false,
17097
+ message: `Path ${JSON.stringify(path24)} could be a literal key containing dots because the discovery report has no observed container at ${JSON.stringify(ambiguousPrefix)}. Apex cannot safely create a capture for it.`
17098
+ };
17099
+ }
17100
+ return { eligible: true };
17101
+ }
17102
+ function isNumericField(field) {
17103
+ const numericTypes = /* @__PURE__ */ new Set(["num", "numstr", "numstr_comma"]);
17104
+ return field.types.length > 0 && field.types.every((type) => numericTypes.has(type.split(":")[0]));
17105
+ }
17106
+ function defaultDimensionLabel(path24) {
17107
+ const words = path24.split(/[._-]+/).filter(Boolean).join(" ");
17108
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : path24;
17109
+ }
17110
+ function quoteShellArgument(value) {
17111
+ return "'" + value.replaceAll("'", "'\\''") + "'";
17112
+ }
17113
+ function createDimensionInvocation(path24, eventName) {
17114
+ return `apex discover --create-dimension ${quoteShellArgument(path24)} --event ${quoteShellArgument(eventName)}`;
17115
+ }
17116
+ function resolveDimensionObservation(command, report, path24, eventName) {
17117
+ const matches = report.data_layer.flatMap(
17118
+ (event) => event.fields.filter((field) => field.path === path24).map((field) => ({ event, field }))
17119
+ );
17120
+ if (!matches.length) {
17121
+ fail(
17122
+ command,
17123
+ `Path ${JSON.stringify(path24)} was not found in the current discovery report. Run apex discover and copy an observed path exactly.`,
17124
+ EXIT_CODES.validation
17125
+ );
17126
+ }
17127
+ let selected2;
17128
+ if (eventName !== void 0) {
17129
+ const eventMatches = matches.filter((match2) => match2.event.event === eventName);
17130
+ if (!eventMatches.length) {
17131
+ fail(
17132
+ command,
17133
+ `Path ${JSON.stringify(path24)} was not observed on event ${JSON.stringify(eventName)}. Run apex discover and copy the event name exactly.`,
17134
+ EXIT_CODES.validation
17135
+ );
17136
+ }
17137
+ const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
17138
+ if (layers.length > 1) {
17139
+ fail(
17140
+ command,
17141
+ `Path ${JSON.stringify(path24)} on event ${JSON.stringify(eventName)} was observed on multiple data layers: ${layers.join(", ")}. Dimensions bind to one layer; this ambiguity cannot be resolved automatically \u2014 capture the value under one layer, or define the dimension in the dashboard with the data layer name set explicitly.`,
17142
+ EXIT_CODES.validation
17143
+ );
17144
+ }
17145
+ selected2 = eventMatches[0];
17146
+ } else {
17147
+ const namedEvents = [
17148
+ ...new Set(matches.map((match2) => match2.event.event).filter(Boolean))
17149
+ ].sort();
17150
+ if (namedEvents.length > 1) {
17151
+ fail(
17152
+ command,
17153
+ `Path ${JSON.stringify(path24)} was observed on multiple events: ${namedEvents.join(", ")}. Retry with --event <name>.`,
17154
+ EXIT_CODES.validation
17155
+ );
17156
+ }
17157
+ const eventMatches = namedEvents.length ? matches.filter((match2) => match2.event.event === namedEvents[0]) : matches;
17158
+ const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
17159
+ if (layers.length > 1) {
17160
+ fail(
17161
+ command,
17162
+ `Path ${JSON.stringify(path24)} on event ${JSON.stringify(namedEvents[0] ?? "")} was observed on multiple data layers: ${layers.join(", ")}. Dimensions bind to one layer; this ambiguity cannot be resolved automatically \u2014 capture the value under one layer, or define the dimension in the dashboard with the data layer name set explicitly.`,
17163
+ EXIT_CODES.validation
17164
+ );
17165
+ }
17166
+ selected2 = eventMatches[0];
17167
+ }
17168
+ if (!selected2)
17169
+ fail(command, "No matching dimension observation was selected", EXIT_CODES.validation);
17170
+ const eligibility = dimensionEligibility(selected2.event, selected2.field);
17171
+ if (!eligibility.eligible) fail(command, eligibility.message, EXIT_CODES.validation);
17172
+ return {
17173
+ eventName: selected2.event.event,
17174
+ dlName: selected2.event.dlName,
17175
+ field: selected2.field
17176
+ };
17177
+ }
16104
17178
  function renderDiscovery(report) {
16105
17179
  const lines = [
16106
17180
  `Observed tracking schema (${report.window.minutes} minutes)`,
@@ -16120,14 +17194,18 @@ function renderDiscovery(report) {
16120
17194
  `
16121
17195
  ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} observation(s)${event.matched ? " \u2014 matched" : ""}; t_ms p50=${event.t_ms_p50 ?? "n/a"}, p95=${event.t_ms_p95 ?? "n/a"}, pre-init=${event.pre_init_count}`
16122
17196
  );
16123
- for (const path23 of event.paths)
17197
+ for (const path24 of event.paths)
16124
17198
  lines.push(
16125
- ` URL ${path23.path}: ${path23.pageviews} pageviews, ${path23.count} pushes, ${path23.pspReturn_count} PSP returns`
17199
+ ` URL ${path24.path}: ${path24.pageviews} pageviews, ${path24.count} pushes, ${path24.pspReturn_count} PSP returns`
16126
17200
  );
16127
- for (const field of event.fields)
17201
+ for (const field of event.fields) {
16128
17202
  lines.push(
16129
17203
  `${field.type_collision ? " \u26A0 TYPE COLLISION " : field.types.some((type) => /^(?:num)(?::|$)/.test(type)) && field.types.some((type) => /^(?:numstr|numstr_comma)(?::|$)/.test(type)) ? " INFO numeric representation " : " "}${field.path}: ${field.types.join(" | ")} (${field.seen})`
16130
17204
  );
17205
+ if (dimensionEligibility(event, field).eligible) {
17206
+ lines.push(` Create dimension: ${createDimensionInvocation(field.path, event.event)}`);
17207
+ }
17208
+ }
16131
17209
  }
16132
17210
  if (report.trigger_diagnoses.length) {
16133
17211
  lines.push("\n\u26A0 Trigger diagnoses");
@@ -16161,16 +17239,30 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
16161
17239
  return `${lines.join("\n")}
16162
17240
  `;
16163
17241
  }
16164
- var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").action(async function action62(opts) {
17242
+ var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function action63(opts) {
16165
17243
  if (opts.gtm && opts.gtmUrl)
16166
17244
  fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
17245
+ if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
17246
+ fail(
17247
+ this,
17248
+ "GTM mining cannot be combined with --create-dimension. Run discovery and creation as separate commands.",
17249
+ EXIT_CODES.validation
17250
+ );
17251
+ }
17252
+ if (!opts.createDimension) {
17253
+ const createOnlyFlag = opts.event !== void 0 ? "--event" : opts.key !== void 0 ? "--key" : opts.label !== void 0 ? "--label" : void 0;
17254
+ if (createOnlyFlag)
17255
+ fail(this, `${createOnlyFlag} requires --create-dimension`, EXIT_CODES.validation);
17256
+ }
16167
17257
  const gtmInput = opts.gtmUrl ?? opts.gtm;
16168
17258
  const minutes = minutesValue(opts.minutes) ?? 10080;
16169
17259
  let report;
16170
17260
  let apiSucceeded = false;
17261
+ let discoveryCredentials;
16171
17262
  let apiFailure;
16172
17263
  try {
16173
- const result = await discoverTrackingSchema(requireCredentials(), minutes);
17264
+ discoveryCredentials = requireCredentials();
17265
+ const result = await discoverTrackingSchema(discoveryCredentials, minutes);
16174
17266
  if (result.ok && result.data) {
16175
17267
  report = result.data;
16176
17268
  apiSucceeded = true;
@@ -16186,7 +17278,7 @@ var discoverCommand = new Command("discover").description("Read back the trackin
16186
17278
  ...error instanceof CliError ? { details: error.details, cliError: error } : {}
16187
17279
  };
16188
17280
  }
16189
- if (!report && !gtmInput) {
17281
+ if (!report && (!gtmInput || opts.createDimension)) {
16190
17282
  if (apiFailure?.cliError) throw apiFailure.cliError;
16191
17283
  fail(
16192
17284
  this,
@@ -16241,20 +17333,92 @@ var discoverCommand = new Command("discover").description("Read back the trackin
16241
17333
  report.gtm = gtm;
16242
17334
  report.sources.gtm_static = gtm.containers.length > 0;
16243
17335
  }
17336
+ if (opts.createDimension) {
17337
+ if (!apiSucceeded || !discoveryCredentials) {
17338
+ fail(
17339
+ this,
17340
+ apiFailure?.message ?? "Discovery API data is required to create a dimension",
17341
+ classifyStatus(apiFailure?.status ?? 0),
17342
+ apiFailure?.details
17343
+ );
17344
+ }
17345
+ const observation = resolveDimensionObservation(
17346
+ this,
17347
+ report,
17348
+ opts.createDimension,
17349
+ opts.event
17350
+ );
17351
+ const definition = {
17352
+ key: opts.key ?? opts.createDimension.toLowerCase(),
17353
+ label: opts.label ?? defaultDimensionLabel(opts.createDimension),
17354
+ value_type: isNumericField(observation.field) ? "number" : "string",
17355
+ capture: {
17356
+ mode: "data_layer",
17357
+ ...observation.dlName !== "dataLayer" ? { dataLayerName: observation.dlName } : {},
17358
+ eventName: observation.eventName,
17359
+ from: opts.createDimension,
17360
+ persist: true
17361
+ },
17362
+ is_dimension: true
17363
+ };
17364
+ let created;
17365
+ try {
17366
+ created = await createInsight(discoveryCredentials, "signals", definition);
17367
+ } catch (error) {
17368
+ const message = error instanceof Error ? error.message : String(error);
17369
+ fail(
17370
+ this,
17371
+ `Dimension was not created: ${message}`,
17372
+ error instanceof CliError ? error.exitCode : EXIT_CODES.general,
17373
+ error instanceof CliError ? error.details : void 0
17374
+ );
17375
+ }
17376
+ if (!created.ok || !created.data) {
17377
+ const reason = created.error ?? "The signals API rejected the request";
17378
+ if (created.status === 409) {
17379
+ fail(
17380
+ this,
17381
+ `Dimension was not created: ${reason} Choose another --key or archive an existing dimension in Audience > Attributes, then retry.`,
17382
+ classifyStatus(created.status),
17383
+ created.errorDetails
17384
+ );
17385
+ }
17386
+ if (created.status === 400) {
17387
+ fail(
17388
+ this,
17389
+ `Dimension was not created: ${reason} Check --create-dimension, --event, --key, and --label, then retry.`,
17390
+ classifyStatus(created.status),
17391
+ created.errorDetails
17392
+ );
17393
+ }
17394
+ fail(
17395
+ this,
17396
+ `Dimension was not created: ${reason}`,
17397
+ classifyStatus(created.status),
17398
+ created.errorDetails
17399
+ );
17400
+ }
17401
+ output(this, { signal: created.data }, () => {
17402
+ process.stdout.write(`Created dimension ${created.data.id}.
17403
+ `);
17404
+ process.stdout.write("Next: open Audience > Attributes in the Apex dashboard.\n");
17405
+ });
17406
+ return;
17407
+ }
16244
17408
  output(this, report, () => process.stdout.write(renderDiscovery(report)));
16245
17409
  });
16246
17410
 
16247
17411
  // src/commands/skills.ts
16248
- var import_node_fs22 = __toESM(require("node:fs"));
16249
- var import_node_path20 = __toESM(require("node:path"));
17412
+ var import_node_fs24 = __toESM(require("node:fs"));
17413
+ var import_node_path22 = __toESM(require("node:path"));
16250
17414
  function packageRoot() {
16251
17415
  const candidates = [
16252
- import_node_path20.default.resolve(__dirname, ".."),
16253
- import_node_path20.default.resolve(import_node_path20.default.dirname(process.argv[1] ?? process.cwd()), ".."),
16254
- import_node_path20.default.resolve(process.cwd(), "packages/cli")
17416
+ import_node_path22.default.resolve(__dirname, ".."),
17417
+ import_node_path22.default.resolve(import_node_path22.default.dirname(process.argv[1] ?? process.cwd()), ".."),
17418
+ import_node_path22.default.resolve(process.cwd(), "packages/cli")
16255
17419
  ];
16256
17420
  for (const candidate of candidates) {
16257
- if (import_node_fs22.default.existsSync(import_node_path20.default.join(candidate, "AGENTS.md"))) return candidate;
17421
+ if (import_node_fs24.default.existsSync(import_node_path22.default.join(candidate, "AGENTS.md"))) return candidate;
16258
17422
  }
16259
17423
  throw new Error(
16260
17424
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -16263,22 +17427,22 @@ function packageRoot() {
16263
17427
  function skillPaths() {
16264
17428
  const root = packageRoot();
16265
17429
  return {
16266
- agents: import_node_path20.default.join(root, "AGENTS.md"),
16267
- skill: import_node_path20.default.join(root, "skills", "apex-cli", "SKILL.md")
17430
+ agents: import_node_path22.default.join(root, "AGENTS.md"),
17431
+ skill: import_node_path22.default.join(root, "skills", "apex-cli", "SKILL.md")
16268
17432
  };
16269
17433
  }
16270
17434
  function installSkill(paths, targetRoot) {
16271
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs22.default.existsSync(filePath));
17435
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs24.default.existsSync(filePath));
16272
17436
  if (missing.length > 0) {
16273
17437
  throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
16274
17438
  }
16275
- const target = import_node_path20.default.resolve(targetRoot, "apex-cli");
16276
- import_node_fs22.default.mkdirSync(target, { recursive: true });
16277
- import_node_fs22.default.copyFileSync(paths.skill, import_node_path20.default.join(target, "SKILL.md"));
16278
- import_node_fs22.default.copyFileSync(paths.agents, import_node_path20.default.join(target, "AGENTS.md"));
17439
+ const target = import_node_path22.default.resolve(targetRoot, "apex-cli");
17440
+ import_node_fs24.default.mkdirSync(target, { recursive: true });
17441
+ import_node_fs24.default.copyFileSync(paths.skill, import_node_path22.default.join(target, "SKILL.md"));
17442
+ import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
16279
17443
  return target;
16280
17444
  }
16281
- 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) {
17445
+ 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 action64(opts) {
16282
17446
  let paths;
16283
17447
  try {
16284
17448
  paths = skillPaths();
@@ -16308,7 +17472,7 @@ function switchValue(value) {
16308
17472
  throw new Error("Use on or off");
16309
17473
  }
16310
17474
  var settingsGet = new Command("settings").description("Show platform operator settings").action(async function() {
16311
- const res = await getAdminSettings(requireCredentials());
17475
+ const res = await getAdminSettings(requireUserCredentials());
16312
17476
  if (!res.ok || !res.data)
16313
17477
  fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
16314
17478
  output(
@@ -16340,14 +17504,14 @@ var set5 = new Command("set").description("Set platform kill switches").option("
16340
17504
  `apex admin settings set ${switches} --confirm`
16341
17505
  );
16342
17506
  }
16343
- const res = await setAdminSettings(requireCredentials(), body5);
17507
+ const res = await setAdminSettings(requireUserCredentials(), body5);
16344
17508
  if (!res.ok || !res.data)
16345
17509
  fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
16346
17510
  output(this, res.data, () => console.log("Platform settings updated."));
16347
17511
  });
16348
17512
  settingsGet.addCommand(set5);
16349
17513
  var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
16350
- const res = await getAdminQuarantine(requireCredentials());
17514
+ const res = await getAdminQuarantine(requireUserCredentials());
16351
17515
  if (!res.ok || !res.data)
16352
17516
  fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
16353
17517
  output(
@@ -16364,7 +17528,56 @@ var quarantine = new Command("quarantine").description("List tracking quarantine
16364
17528
  )
16365
17529
  );
16366
17530
  });
16367
- var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine);
17531
+ async function confirmArchive(command, shopId, force) {
17532
+ if (force) return true;
17533
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
17534
+ confirmationRequired(
17535
+ command,
17536
+ "Archiving a shop requires confirmation in non-TTY mode.",
17537
+ `apex admin shops archive ${JSON.stringify(shopId)} --force`
17538
+ );
17539
+ }
17540
+ const readline3 = await import("node:readline");
17541
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17542
+ const answer = await new Promise((resolve2) => {
17543
+ rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve2);
17544
+ });
17545
+ rl.close();
17546
+ if (answer.toLowerCase() !== "y") {
17547
+ console.error("Aborted.");
17548
+ return false;
17549
+ }
17550
+ return true;
17551
+ }
17552
+ var archive2 = new Command("archive").description("Archive a shop and disable Apex delivery").argument("<shopId>", "Shop ID").option("-f, --force", "Skip confirmation").action(async function(shopId, opts) {
17553
+ if (!await confirmArchive(this, shopId, opts.force)) return;
17554
+ const res = await archiveAdminShop(requireUserCredentials(), shopId);
17555
+ if (!res.ok || !res.data)
17556
+ fail(
17557
+ this,
17558
+ res.error ?? "Failed to archive shop",
17559
+ classifyStatus(res.status),
17560
+ res.errorDetails
17561
+ );
17562
+ output(this, res.data, () => console.log("Shop archived."));
17563
+ });
17564
+ var restore2 = new Command("restore").description("Restore an archived shop").argument("<shopId>", "Shop ID").action(async function(shopId) {
17565
+ const res = await restoreAdminShop(requireUserCredentials(), shopId);
17566
+ if (!res.ok || !res.data)
17567
+ fail(
17568
+ this,
17569
+ res.error ?? "Failed to restore shop",
17570
+ classifyStatus(res.status),
17571
+ res.errorDetails
17572
+ );
17573
+ output(
17574
+ this,
17575
+ res.data,
17576
+ () => console.log("Shop restored. Delivery remains controlled by the kill switch.")
17577
+ );
17578
+ });
17579
+ var shops = new Command("shops").description("Archive and restore shops on the platform admin plane").addCommand(archive2).addCommand(restore2);
17580
+ var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops);
16368
17581
 
16369
17582
  // src/commands/membership.ts
16370
17583
  init_source();
@@ -16381,7 +17594,7 @@ async function confirm(c, force, label, retry) {
16381
17594
  const readline3 = await import("node:readline");
16382
17595
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
16383
17596
  const answer = await new Promise(
16384
- (resolve) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve)
17597
+ (resolve2) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve2)
16385
17598
  );
16386
17599
  rl.close();
16387
17600
  if (answer.toLowerCase() !== "y") {
@@ -16408,7 +17621,12 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
16408
17621
  orgCommand.command("members").action(async function() {
16409
17622
  await run2(this, listOrgMembers(requireUserCredentials()), "list members");
16410
17623
  });
16411
- orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").option("--default-shop-role <role>", "Default shop role", "viewer").action(async function(email, o) {
17624
+ orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
17625
+ new Option(
17626
+ "--default-shop-role <role>",
17627
+ "Default shop role for every brand in the workspace (workspace defaults are internal roles)"
17628
+ ).choices(["editor"]).default("editor")
17629
+ ).action(async function(email, o) {
16412
17630
  if (o.role !== void 0) {
16413
17631
  process.stderr.write(
16414
17632
  "Warning: --role is deprecated for apex org invite; use --org-role instead. --role will be removed in v0.7.0.\n"
@@ -16452,7 +17670,12 @@ var teamCommand = new Command("team").description("Manage a shop team");
16452
17670
  teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
16453
17671
  await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
16454
17672
  });
16455
- teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").requiredOption("--role <role>").action(async function(email, o) {
17673
+ teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
17674
+ new Option(
17675
+ "--role <role>",
17676
+ "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
17677
+ ).choices(["editor", "client_viewer"]).makeOptionMandatory()
17678
+ ).action(async function(email, o) {
16456
17679
  await run2(
16457
17680
  this,
16458
17681
  inviteTeam(requireUserCredentials(), email, o.shop, o.role),
@@ -16466,7 +17689,12 @@ teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id
16466
17689
  "revoke invitation"
16467
17690
  );
16468
17691
  });
16469
- teamCommand.command("set-role").argument("<memberId>").argument("<role>").requiredOption("--shop <id>").action(async function(id, role, o) {
17692
+ teamCommand.command("set-role").argument("<memberId>").addArgument(
17693
+ new Argument(
17694
+ "<role>",
17695
+ "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
17696
+ ).choices(["editor", "client_viewer"])
17697
+ ).requiredOption("--shop <id>").action(async function(id, role, o) {
16470
17698
  await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
16471
17699
  });
16472
17700
  teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
@@ -16636,8 +17864,8 @@ async function fetchText(url) {
16636
17864
  });
16637
17865
  return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
16638
17866
  }
16639
- function abs(base, path23) {
16640
- return new URL(path23, base).toString();
17867
+ function abs(base, path24) {
17868
+ return new URL(path24, base).toString();
16641
17869
  }
16642
17870
  function looksLikeShopifyCart(text) {
16643
17871
  try {
@@ -16657,9 +17885,9 @@ async function collectSignals(url) {
16657
17885
  const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
16658
17886
  const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
16659
17887
  if (!protectedHit) {
16660
- for (const path23 of ["/cart.js"]) {
17888
+ for (const path24 of ["/cart.js"]) {
16661
17889
  try {
16662
- const secondary = await fetchText(abs(primary.finalUrl || url, path23));
17890
+ const secondary = await fetchText(abs(primary.finalUrl || url, path24));
16663
17891
  if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
16664
17892
  chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
16665
17893
  }
@@ -16838,7 +18066,7 @@ function selectedEvents(opts) {
16838
18066
  }
16839
18067
  var list14 = new Command("list").alias("ls").description(
16840
18068
  "List webhooks. JSON includes availableEvents; secrets are never returned after create."
16841
- ).action(async function action64() {
18069
+ ).action(async function action65() {
16842
18070
  const data = await loadWebhookIndex(this);
16843
18071
  output(this, data, () => printWebhooks(data.data));
16844
18072
  });
@@ -16847,7 +18075,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
16847
18075
  "Webhook event to subscribe to; repeat for multiple events",
16848
18076
  (value, previous = []) => [...previous, value],
16849
18077
  []
16850
- ).action(async function action65(opts) {
18078
+ ).action(async function action66(opts) {
16851
18079
  const events = selectedEvents(opts);
16852
18080
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
16853
18081
  const index = await loadWebhookIndex(this);
@@ -16879,7 +18107,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
16879
18107
  }
16880
18108
  );
16881
18109
  });
16882
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action66(id) {
18110
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
16883
18111
  requireShopSecretKey(this);
16884
18112
  const result = handleResult(
16885
18113
  this,
@@ -16888,7 +18116,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
16888
18116
  );
16889
18117
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
16890
18118
  });
16891
- 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) {
18119
+ 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 action68(id, opts) {
16892
18120
  const limit = Number.parseInt(opts.limit, 10);
16893
18121
  if (!Number.isInteger(limit) || limit < 1)
16894
18122
  fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
@@ -16905,7 +18133,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
16905
18133
  "Replacement event; repeat for multiple events",
16906
18134
  (value, previous = []) => [...previous, value],
16907
18135
  []
16908
- ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action68(id, opts) {
18136
+ ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
16909
18137
  if (opts.active && opts.inactive)
16910
18138
  fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
16911
18139
  const events = selectedEvents(opts);
@@ -16938,7 +18166,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
16938
18166
  );
16939
18167
  output(this, { webhook: result }, () => printWebhooks([result]));
16940
18168
  });
16941
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action69(id) {
18169
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
16942
18170
  requireShopSecretKey(this);
16943
18171
  const result = handleResult(
16944
18172
  this,
@@ -16957,10 +18185,10 @@ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks"
16957
18185
 
16958
18186
  // src/commands/simulate.ts
16959
18187
  init_source();
16960
- var import_node_fs23 = __toESM(require("node:fs"));
16961
- var import_node_path21 = __toESM(require("node:path"));
18188
+ var import_node_fs25 = __toESM(require("node:fs"));
18189
+ var import_node_path23 = __toESM(require("node:path"));
16962
18190
  function readJson2(file) {
16963
- const text = file === "-" ? import_node_fs23.default.readFileSync(0, "utf8") : import_node_fs23.default.readFileSync(import_node_path21.default.resolve(file), "utf8");
18191
+ const text = file === "-" ? import_node_fs25.default.readFileSync(0, "utf8") : import_node_fs25.default.readFileSync(import_node_path23.default.resolve(file), "utf8");
16964
18192
  try {
16965
18193
  return JSON.parse(text);
16966
18194
  } catch (error) {
@@ -17082,8 +18310,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
17082
18310
  });
17083
18311
 
17084
18312
  // src/commands/config-resources.ts
17085
- var import_node_fs24 = __toESM(require("node:fs"));
17086
- var import_node_path22 = __toESM(require("node:path"));
18313
+ var import_node_fs26 = __toESM(require("node:fs"));
18314
+ var import_node_path24 = __toESM(require("node:path"));
17087
18315
  function show(resource) {
17088
18316
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
17089
18317
  `);
@@ -17109,8 +18337,8 @@ async function confirmAssetDelete(id) {
17109
18337
  input: process.stdin,
17110
18338
  output: process.stderr
17111
18339
  });
17112
- const answer = await new Promise((resolve) => {
17113
- rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve);
18340
+ const answer = await new Promise((resolve2) => {
18341
+ rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve2);
17114
18342
  });
17115
18343
  rl.close();
17116
18344
  return answer.trim().toLowerCase() === "y";
@@ -17156,12 +18384,12 @@ function readFamily(family, label) {
17156
18384
  }
17157
18385
  var assetReads = readFamily("assets", "assets");
17158
18386
  var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
17159
- const absolute = import_node_path22.default.resolve(process.cwd(), file);
17160
- if (!import_node_fs24.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
17161
- const fileStat = import_node_fs24.default.statSync(absolute);
18387
+ const absolute = import_node_path24.default.resolve(process.cwd(), file);
18388
+ if (!import_node_fs26.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
18389
+ const fileStat = import_node_fs26.default.statSync(absolute);
17162
18390
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
17163
18391
  if (fileStat.size > MAX_ASSET_SIZE_BYTES) fail(this, `${file} is larger than 10 MB`, 3);
17164
- const mimeType = ASSET_MIME_TYPES[import_node_path22.default.extname(absolute).toLowerCase()];
18392
+ const mimeType = ASSET_MIME_TYPES[import_node_path24.default.extname(absolute).toLowerCase()];
17165
18393
  if (!mimeType)
17166
18394
  fail(
17167
18395
  this,
@@ -17170,8 +18398,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
17170
18398
  );
17171
18399
  const r = await uploadAssets(requireCredentials(), [
17172
18400
  {
17173
- name: import_node_path22.default.basename(absolute),
17174
- bytes: import_node_fs24.default.readFileSync(absolute),
18401
+ name: import_node_path24.default.basename(absolute),
18402
+ bytes: import_node_fs26.default.readFileSync(absolute),
17175
18403
  type: mimeType
17176
18404
  }
17177
18405
  ]);
@@ -17261,6 +18489,7 @@ program2.addCommand(funnelsCommand);
17261
18489
  program2.addCommand(environmentsCommand);
17262
18490
  program2.addCommand(consentCommand);
17263
18491
  program2.addCommand(triggersCommand);
18492
+ program2.addCommand(projectCodeCommand);
17264
18493
  program2.addCommand(domainsCommand);
17265
18494
  program2.addCommand(handoverCommand);
17266
18495
  program2.addCommand(analyticsCommand);