@drip-apex/apex-cli 0.6.1 → 0.6.2

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)
@@ -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.2",
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();
@@ -8471,7 +8499,7 @@ 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
8504
  return new Promise((resolve, reject) => {
8477
8505
  let settled = false;
@@ -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) {
@@ -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();
@@ -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);
@@ -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: {
@@ -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)
@@ -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(
@@ -12242,22 +12779,34 @@ var off = new Command("off").description("Disable consent gating").action(async
12242
12779
  var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
12243
12780
 
12244
12781
  // src/commands/triggers.ts
12245
- var import_node_fs14 = __toESM(require("node:fs"));
12246
- var import_node_path12 = __toESM(require("node:path"));
12782
+ var import_node_fs15 = __toESM(require("node:fs"));
12783
+ var import_node_path13 = __toESM(require("node:path"));
12247
12784
 
12248
12785
  // src/trigger-proposal.ts
12249
- function fieldsFrom(value, prefix = "") {
12786
+ function runtimePath(path24) {
12787
+ return path24.replaceAll("[]", ".0");
12788
+ }
12789
+ function fieldsFrom(value, prefix = "", descriptorList = false) {
12250
12790
  if (!value || typeof value !== "object") return [];
12251
- if (Array.isArray(value)) return value.flatMap((item) => fieldsFrom(item, prefix));
12791
+ if (Array.isArray(value)) {
12792
+ return value.flatMap(
12793
+ (item, index) => fieldsFrom(
12794
+ item,
12795
+ descriptorList ? prefix : prefix ? `${prefix}.${index}` : String(index),
12796
+ descriptorList
12797
+ )
12798
+ );
12799
+ }
12252
12800
  const record2 = value;
12253
12801
  const explicitPath = typeof record2.path === "string" ? record2.path : void 0;
12254
- if (explicitPath) {
12802
+ if (descriptorList && explicitPath) {
12255
12803
  const rawValues = record2.values ?? record2.samples ?? record2.examples ?? record2.example;
12256
12804
  const values = Array.isArray(rawValues) ? rawValues : rawValues === void 0 ? [] : [rawValues];
12257
12805
  const magnitude = [record2.mean, record2.average, record2.max, record2.magnitude].find(
12258
12806
  (candidate) => typeof candidate === "number"
12259
12807
  );
12260
- return [{ path: explicitPath, values, magnitude }];
12808
+ const numericType = Array.isArray(record2.types) && record2.types.length > 0 && record2.types.every((type) => type === "num" || type === "numstr" || type === "numstr_comma");
12809
+ return [{ path: runtimePath(explicitPath), values, magnitude, numericType }];
12261
12810
  }
12262
12811
  return Object.entries(record2).flatMap(([key, child]) => {
12263
12812
  if (["eventName", "event_name", "name", "count", "frequency", "evidence"].includes(key))
@@ -12275,6 +12824,27 @@ function fieldsFrom(value, prefix = "") {
12275
12824
  return fieldsFrom(child, next);
12276
12825
  });
12277
12826
  }
12827
+ function isDescriptorArray(value) {
12828
+ const descriptorKeys = /* @__PURE__ */ new Set([
12829
+ "path",
12830
+ "values",
12831
+ "samples",
12832
+ "examples",
12833
+ "example",
12834
+ "mean",
12835
+ "average",
12836
+ "max",
12837
+ "magnitude",
12838
+ "types",
12839
+ "seen",
12840
+ "type_collision"
12841
+ ]);
12842
+ return Array.isArray(value) && value.length > 0 && value.every((item) => {
12843
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
12844
+ const record2 = item;
12845
+ return typeof record2.path === "string" && record2.path.length > 0 && Object.keys(record2).every((key) => descriptorKeys.has(key));
12846
+ });
12847
+ }
12278
12848
  function candidatesFrom(report) {
12279
12849
  if (!report || typeof report !== "object") return [];
12280
12850
  const root = report;
@@ -12292,15 +12862,15 @@ function candidatesFrom(report) {
12292
12862
  }
12293
12863
  return [root];
12294
12864
  }
12295
- function score2(path23, patterns) {
12865
+ function score2(path24, patterns) {
12296
12866
  return patterns.reduce(
12297
- (total, pattern, index) => total + (pattern.test(path23) ? patterns.length - index : 0),
12867
+ (total, pattern, index) => total + (pattern.test(path24) ? patterns.length - index : 0),
12298
12868
  0
12299
12869
  );
12300
12870
  }
12301
12871
  function best(fields, patterns, numeric2 = false) {
12302
12872
  const ranked = fields.filter(
12303
- (field) => !numeric2 || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
12873
+ (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
12304
12874
  ).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
12875
  if (!ranked.length) return [];
12306
12876
  const top = ranked[0].score;
@@ -12322,9 +12892,9 @@ function proposeTriggers(report) {
12322
12892
  eventName
12323
12893
  ))
12324
12894
  continue;
12325
- const fields = fieldsFrom(
12326
- candidate.fields ?? candidate.paths ?? candidate.properties ?? candidate.payload ?? candidate
12327
- );
12895
+ const descriptors = candidate.fields ?? candidate.paths;
12896
+ const payload = candidate.properties ?? candidate.payload ?? candidate;
12897
+ const fields = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
12328
12898
  const revenues = best(
12329
12899
  fields,
12330
12900
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
@@ -12394,6 +12964,20 @@ function proposeTriggers(report) {
12394
12964
  return output2;
12395
12965
  }
12396
12966
 
12967
+ // src/generated/runtime-settings-trigger-constants.json
12968
+ var runtime_settings_trigger_constants_default = {
12969
+ dotPathPattern: "^[A-Za-z_$][\\w$]*(?:\\.(?:\\d+|[A-Za-z_$][\\w$]*))*$",
12970
+ signalPathPattern: "^signal:[A-Za-z_$][\\w$]*$",
12971
+ valueTypes: ["string", "number", "boolean", "string_array", "json"],
12972
+ whenOps: ["eq", "neq", "gt", "lt", "exists", "not_exists"],
12973
+ maxWhenEntries: 5,
12974
+ maxSourcePaths: 5,
12975
+ maxTransformMapEntries: 20,
12976
+ maxTransformMapKeyChars: 100,
12977
+ maxTransformMapValueChars: 100,
12978
+ maxTriggers: 20
12979
+ };
12980
+
12397
12981
  // src/commands/triggers.ts
12398
12982
  var allowed = /* @__PURE__ */ new Set([
12399
12983
  "eventName",
@@ -12402,11 +12986,15 @@ var allowed = /* @__PURE__ */ new Set([
12402
12986
  "reEvaluate",
12403
12987
  "trackPageview",
12404
12988
  "trackEventName",
12989
+ "when",
12405
12990
  "trackRevenue",
12406
- "mappings"
12991
+ "mappings",
12992
+ "properties"
12407
12993
  ]);
12408
- var pathPattern = /^[A-Za-z_$][\w$]*(?:\.(?:\d+|[A-Za-z_$][\w$]*))*$/;
12409
- var valueTypes = /* @__PURE__ */ new Set(["string", "number", "boolean", "string_array", "json"]);
12994
+ var pathPattern = new RegExp(runtime_settings_trigger_constants_default.dotPathPattern);
12995
+ var signalPathPattern = new RegExp(runtime_settings_trigger_constants_default.signalPathPattern);
12996
+ var valueTypes = new Set(runtime_settings_trigger_constants_default.valueTypes);
12997
+ var whenOps = new Set(runtime_settings_trigger_constants_default.whenOps);
12410
12998
  function strictObject(value, location, keys) {
12411
12999
  if (!value || typeof value !== "object" || Array.isArray(value))
12412
13000
  throw new Error(`${location} must be an object`);
@@ -12426,26 +13014,126 @@ function strictPath(value, location, required = false) {
12426
13014
  `${location} must use dot-path syntax with property or array-index segments (for example: ecommerce.items.0.price); bracket syntax is not accepted`
12427
13015
  );
12428
13016
  }
13017
+ function strictSource(value, location, allowSignal, required = false) {
13018
+ if (value === void 0 && !required) return;
13019
+ const sources = Array.isArray(value) ? value : [value];
13020
+ if (Array.isArray(value) && (value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxSourcePaths))
13021
+ throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxSourcePaths} paths`);
13022
+ sources.forEach((source, index) => {
13023
+ const sourceLocation = Array.isArray(value) ? `${location}[${index}]` : location;
13024
+ if (allowSignal && typeof source === "string" && signalPathPattern.test(source)) return;
13025
+ strictPath(source, sourceLocation, true);
13026
+ });
13027
+ }
13028
+ function defaultMatchesValueType(value, valueType) {
13029
+ if (valueType === "number") {
13030
+ if (typeof value !== "string") return Number.isFinite(Number(value));
13031
+ const text = String(value).trim();
13032
+ const normalized = /^-?\d{1,3}(\.\d{3})+,\d+$/.test(text) ? text.replace(/\./g, "").replace(",", ".") : /^-?\d+,\d+$/.test(text) ? text.replace(",", ".") : text;
13033
+ return Number.isFinite(Number(normalized));
13034
+ }
13035
+ if (valueType === "boolean") {
13036
+ if (typeof value === "boolean") return true;
13037
+ return ["1", "true", "yes", "on", "0", "false", "no", "off"].includes(
13038
+ String(value).trim().toLowerCase()
13039
+ );
13040
+ }
13041
+ return true;
13042
+ }
13043
+ function strictTransform(value, valueType, location) {
13044
+ if (value === void 0) return;
13045
+ const transform = strictObject(
13046
+ value,
13047
+ location,
13048
+ /* @__PURE__ */ new Set(["trim", "lowercase", "map", "scale", "default"])
13049
+ );
13050
+ optionalType(transform.trim, "boolean", `${location}.trim`);
13051
+ optionalType(transform.lowercase, "boolean", `${location}.lowercase`);
13052
+ if (transform.map !== void 0) {
13053
+ if (!transform.map || typeof transform.map !== "object" || Array.isArray(transform.map))
13054
+ throw new Error(`${location}.map must be an object`);
13055
+ const map = transform.map;
13056
+ const entries = Object.entries(map);
13057
+ if (entries.length > runtime_settings_trigger_constants_default.maxTransformMapEntries)
13058
+ throw new Error(
13059
+ `${location}.map must contain at most ${runtime_settings_trigger_constants_default.maxTransformMapEntries} entries`
13060
+ );
13061
+ for (const [key, mappedValue] of entries) {
13062
+ if (key.length > runtime_settings_trigger_constants_default.maxTransformMapKeyChars)
13063
+ throw new Error(
13064
+ `${location}.map keys must be at most ${runtime_settings_trigger_constants_default.maxTransformMapKeyChars} characters`
13065
+ );
13066
+ if (typeof mappedValue !== "string" || mappedValue.length > runtime_settings_trigger_constants_default.maxTransformMapValueChars)
13067
+ throw new Error(
13068
+ `${location}.map.${key} must be a string of at most ${runtime_settings_trigger_constants_default.maxTransformMapValueChars} characters`
13069
+ );
13070
+ }
13071
+ }
13072
+ if (transform.scale !== void 0 && (typeof transform.scale !== "number" || !Number.isFinite(transform.scale) || transform.scale === 0))
13073
+ throw new Error(`${location}.scale must be finite and non-zero`);
13074
+ if (transform.default !== void 0 && !["string", "number", "boolean"].includes(typeof transform.default))
13075
+ throw new Error(`${location}.default must be a string, number, or boolean`);
13076
+ if (transform.default !== void 0 && !defaultMatchesValueType(transform.default, valueType))
13077
+ throw new Error(`${location}.default must coerce to the mapping valueType`);
13078
+ }
12429
13079
  function strictMapping(value, location, revenueProperty) {
12430
13080
  const keys = /* @__PURE__ */ new Set([
12431
13081
  "from",
12432
13082
  "to",
12433
13083
  "valueType",
13084
+ "transform",
12434
13085
  ...revenueProperty ? [] : ["ttlDays", "persist"]
12435
13086
  ]);
12436
13087
  const mapping = strictObject(value, location, keys);
12437
- strictPath(mapping.from, `${location}.from`, true);
13088
+ strictSource(mapping.from, `${location}.from`, revenueProperty, true);
12438
13089
  strictPath(mapping.to, `${location}.to`, true);
12439
13090
  if (mapping.valueType !== void 0 && !valueTypes.has(String(mapping.valueType)))
12440
13091
  throw new Error(`${location}.valueType must be string, number, boolean, string_array, or json`);
13092
+ strictTransform(mapping.transform, mapping.valueType, `${location}.transform`);
12441
13093
  if (!revenueProperty) {
12442
13094
  optionalType(mapping.ttlDays, "number", `${location}.ttlDays`);
12443
13095
  optionalType(mapping.persist, "boolean", `${location}.persist`);
12444
13096
  }
12445
13097
  }
13098
+ function strictWhen(value, location) {
13099
+ if (value === void 0) return;
13100
+ if (!Array.isArray(value) || value.length < 1 || value.length > runtime_settings_trigger_constants_default.maxWhenEntries) {
13101
+ throw new Error(`${location} must contain 1 to ${runtime_settings_trigger_constants_default.maxWhenEntries} entries`);
13102
+ }
13103
+ value.forEach((entry, index) => {
13104
+ const entryLocation = `${location}[${index}]`;
13105
+ const condition = strictObject(entry, entryLocation, /* @__PURE__ */ new Set(["path", "op", "value"]));
13106
+ strictPath(condition.path, `${entryLocation}.path`, true);
13107
+ if (typeof condition.op !== "string" || !whenOps.has(condition.op)) {
13108
+ throw new Error(
13109
+ `${entryLocation}.op must be ${runtime_settings_trigger_constants_default.whenOps.slice(0, -1).join(", ")}, or ${runtime_settings_trigger_constants_default.whenOps.at(-1)}`
13110
+ );
13111
+ }
13112
+ const hasValue = Object.prototype.hasOwnProperty.call(condition, "value");
13113
+ if (condition.op === "exists" || condition.op === "not_exists") {
13114
+ if (hasValue) throw new Error(`${entryLocation}.value is forbidden for ${condition.op}`);
13115
+ return;
13116
+ }
13117
+ if (!hasValue) throw new Error(`${entryLocation}.value is required for ${condition.op}`);
13118
+ if (condition.op === "gt" || condition.op === "lt") {
13119
+ if (typeof condition.value !== "number" || !Number.isFinite(condition.value)) {
13120
+ throw new Error(`${entryLocation}.value must be a number for ${condition.op}`);
13121
+ }
13122
+ return;
13123
+ }
13124
+ if (!["string", "number", "boolean"].includes(typeof condition.value)) {
13125
+ throw new Error(`${entryLocation}.value must be a string, number, or boolean`);
13126
+ }
13127
+ if (typeof condition.value === "number" && !Number.isFinite(condition.value)) {
13128
+ throw new Error(`${entryLocation}.value must be finite`);
13129
+ }
13130
+ });
13131
+ }
12446
13132
  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");
13133
+ if (!Array.isArray(value) || value.length > runtime_settings_trigger_constants_default.maxTriggers)
13134
+ throw new Error(
13135
+ `trigger file must contain an array of at most ${runtime_settings_trigger_constants_default.maxTriggers} triggers`
13136
+ );
12449
13137
  return value.map((item, index) => {
12450
13138
  if (!item || typeof item !== "object" || Array.isArray(item))
12451
13139
  throw new Error(`trigger[${index}] must be an object`);
@@ -12458,6 +13146,7 @@ function strictTriggers(value) {
12458
13146
  optionalType(record2[key], "boolean", `trigger[${index}].${key}`);
12459
13147
  for (const key of ["dataLayerName", "trackEventName"])
12460
13148
  optionalType(record2[key], "string", `trigger[${index}].${key}`);
13149
+ strictWhen(record2.when, `trigger[${index}].when`);
12461
13150
  if (record2.trackRevenue !== void 0) {
12462
13151
  const revenue = strictObject(
12463
13152
  record2.trackRevenue,
@@ -12474,7 +13163,7 @@ function strictTriggers(value) {
12474
13163
  );
12475
13164
  optionalType(revenue.enabled, "boolean", `trigger[${index}].trackRevenue.enabled`);
12476
13165
  for (const key of ["revenueFrom", "orderIdFrom", "currencyFrom", "costFrom", "profitFrom"])
12477
- strictPath(revenue[key], `trigger[${index}].trackRevenue.${key}`);
13166
+ strictSource(revenue[key], `trigger[${index}].trackRevenue.${key}`, false);
12478
13167
  if (revenue.properties !== void 0) {
12479
13168
  if (!Array.isArray(revenue.properties))
12480
13169
  throw new Error(`trigger[${index}].trackRevenue.properties must be an array`);
@@ -12490,9 +13179,24 @@ function strictTriggers(value) {
12490
13179
  (mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].mappings[${mappingIndex}]`, false)
12491
13180
  );
12492
13181
  }
13182
+ if (record2.properties !== void 0) {
13183
+ if (!Array.isArray(record2.properties))
13184
+ throw new Error(`trigger[${index}].properties must be an array`);
13185
+ record2.properties.forEach(
13186
+ (mapping, mappingIndex) => strictMapping(mapping, `trigger[${index}].properties[${mappingIndex}]`, true)
13187
+ );
13188
+ }
12493
13189
  return record2;
12494
13190
  });
12495
13191
  }
13192
+ function writeWarnings(result) {
13193
+ const warnings = result.warnings;
13194
+ if (!Array.isArray(warnings)) return;
13195
+ for (const warning of warnings) {
13196
+ if (typeof warning === "string") process.stderr.write(`WARNING: ${warning}
13197
+ `);
13198
+ }
13199
+ }
12496
13200
  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;
12497
13201
  async function identify(command, requestedShop) {
12498
13202
  let credentials = requireCredentials();
@@ -12569,7 +13273,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
12569
13273
  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
13274
  let triggers;
12571
13275
  try {
12572
- triggers = strictTriggers(JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(opts.file), "utf8")));
13276
+ triggers = strictTriggers(JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.file), "utf8")));
12573
13277
  } catch (error) {
12574
13278
  fail(
12575
13279
  this,
@@ -12584,20 +13288,19 @@ var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger arra
12584
13288
  updated_at: (/* @__PURE__ */ new Date()).toISOString(),
12585
13289
  ...data
12586
13290
  };
12587
- output(
12588
- this,
12589
- result,
12590
- () => process.stdout.write(
13291
+ output(this, result, () => {
13292
+ process.stdout.write(
12591
13293
  `TRIGGERS_SET shop_id=${identity.shop_id} domain=${identity.domain} updated_at=${result.updated_at} count=${triggers.length}
12592
13294
  `
12593
- )
12594
- );
13295
+ );
13296
+ writeWarnings(result);
13297
+ });
12595
13298
  });
12596
13299
  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
13300
  let proposals;
12598
13301
  let report;
12599
13302
  try {
12600
- report = JSON.parse(import_node_fs14.default.readFileSync(import_node_path12.default.resolve(opts.fromFile), "utf8"));
13303
+ report = JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.fromFile), "utf8"));
12601
13304
  proposals = proposeTriggers(report);
12602
13305
  } catch (error) {
12603
13306
  fail(this, error instanceof Error ? error.message : String(error), 2);
@@ -12656,14 +13359,13 @@ var recipe = new Command("apply-recipe").argument("<recipeId>").requiredOption("
12656
13359
  domain: identity.domain,
12657
13360
  applied_at: (/* @__PURE__ */ new Date()).toISOString()
12658
13361
  };
12659
- output(
12660
- this,
12661
- result,
12662
- () => process.stdout.write(
13362
+ output(this, result, () => {
13363
+ process.stdout.write(
12663
13364
  `TRIGGERS_APPLIED shop_id=${identity.shop_id} domain=${identity.domain} applied_at=${result.applied_at} recipe=${recipeId}
12664
13365
  `
12665
- )
12666
- );
13366
+ );
13367
+ writeWarnings(result);
13368
+ });
12667
13369
  });
12668
13370
  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
13371
  const hours = Number(opts.hours);
@@ -12750,11 +13452,11 @@ async function patchWithRetry(command, build) {
12750
13452
  warn2(command, result.data.precompile);
12751
13453
  return result.data;
12752
13454
  }
12753
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action47() {
13455
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
12754
13456
  const data = await load4(this);
12755
13457
  output(this, data, () => printDomains(data));
12756
13458
  });
12757
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action48(domain) {
13459
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
12758
13460
  const added = domain.map(normalizeDomain);
12759
13461
  const data = await patchWithRetry(
12760
13462
  this,
@@ -12762,7 +13464,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
12762
13464
  );
12763
13465
  output(this, data, () => printDomains(data));
12764
13466
  });
12765
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action49(domain) {
13467
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
12766
13468
  const removeSet = new Set(domain.map(normalizeDomain));
12767
13469
  const data = await patchWithRetry(
12768
13470
  this,
@@ -12770,7 +13472,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
12770
13472
  );
12771
13473
  output(this, data, () => printDomains(data));
12772
13474
  });
12773
- var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action50(domain) {
13475
+ var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
12774
13476
  const replacement = domain.map(normalizeDomain);
12775
13477
  const data = await patchWithRetry(this, () => replacement);
12776
13478
  output(this, data, () => printDomains(data));
@@ -12862,14 +13564,14 @@ async function patchWithRetry2(command, build) {
12862
13564
  );
12863
13565
  return result.data;
12864
13566
  }
12865
- var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action51() {
13567
+ var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
12866
13568
  const data = await load5(this);
12867
13569
  output(this, data, () => printSettings(data.externalTracking));
12868
13570
  });
12869
13571
  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
13572
  "--signed",
12871
13573
  "Require signed handover tokens (the default; unsigning is a platform-support action)"
12872
- ).action(async function action52(opts) {
13574
+ ).action(async function action53(opts) {
12873
13575
  const domains = opts.domain.map(normalizeDomain2);
12874
13576
  if (domains.some((domain) => !domain))
12875
13577
  fail(
@@ -12889,14 +13591,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
12889
13591
  }));
12890
13592
  output(this, data, () => printSettings(data.externalTracking));
12891
13593
  });
12892
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action53() {
13594
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
12893
13595
  const data = await patchWithRetry2(this, (current) => ({
12894
13596
  externalTracking: { domains: [], signed: true },
12895
13597
  ifRevision: current.revision
12896
13598
  }));
12897
13599
  output(this, data, () => printSettings(data.externalTracking));
12898
13600
  });
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) {
13601
+ 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
13602
  const config = await load5(this);
12901
13603
  const target = normalizeDomain2(opts.toDomain);
12902
13604
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
@@ -13939,7 +14641,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
13939
14641
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
13940
14642
  "--worker <url>",
13941
14643
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
13942
- ).action(async function action55(options) {
14644
+ ).action(async function action56(options) {
13943
14645
  const data = await runDoctor(options);
13944
14646
  if (options.check && data.checks.length === 0)
13945
14647
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
@@ -13961,7 +14663,7 @@ function printDoctorReport(checks) {
13961
14663
  var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
13962
14664
  "--since <date>",
13963
14665
  "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
13964
- ).action(async function action56(opts) {
14666
+ ).action(async function action57(opts) {
13965
14667
  if (opts.since !== void 0 && parseDay(opts.since) === null)
13966
14668
  fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
13967
14669
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
@@ -14021,7 +14723,7 @@ function analyticsSubcommand(name, description, defaultDays) {
14021
14723
  command.option("--planning-days <n>", "Planning window in days", "180");
14022
14724
  command.option("--limit <n>", "Maximum page rows", "50");
14023
14725
  }
14024
- return command.action(async function action70(opts) {
14726
+ return command.action(async function action71(opts) {
14025
14727
  const result = await getAnalytics(requireCredentials(), name, {
14026
14728
  days: positiveInteger(this, opts.days, "--days"),
14027
14729
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -14041,7 +14743,7 @@ function urlsCommand() {
14041
14743
  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
14744
  "--match <regex>",
14043
14745
  "Client-side regex filter; exhausts inventory pages before filtering"
14044
- ).option("--limit <n>", "Maximum rows to print", "50").action(async function action70(opts) {
14746
+ ).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
14045
14747
  const creds = requireCredentials();
14046
14748
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
14047
14749
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -14235,10 +14937,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14235
14937
  const chunks = [];
14236
14938
  let bytes = 0;
14237
14939
  let settled = false;
14238
- const settle = (action70) => {
14940
+ const settle = (action71) => {
14239
14941
  if (settled) return;
14240
14942
  settled = true;
14241
- action70();
14943
+ action71();
14242
14944
  };
14243
14945
  stream.on("data", (chunk) => {
14244
14946
  if (settled) return;
@@ -14309,11 +15011,11 @@ function pinnedFetch(url, addresses, limits) {
14309
15011
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
14310
15012
  };
14311
15013
  let settled = false;
14312
- const settle = (action70) => {
15014
+ const settle = (action71) => {
14313
15015
  if (settled) return;
14314
15016
  settled = true;
14315
15017
  clearTimeout(deadline);
14316
- action70();
15018
+ action71();
14317
15019
  };
14318
15020
  const req = send3(
14319
15021
  url,
@@ -14648,7 +15350,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
14648
15350
  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
15351
  "--worker-url <url>",
14650
15352
  "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) {
15353
+ ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
14652
15354
  const creds = requireCredentials();
14653
15355
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
14654
15356
  if (!shopId)
@@ -14792,8 +15494,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
14792
15494
 
14793
15495
  // src/commands/flags.ts
14794
15496
  init_source();
14795
- var import_node_fs15 = __toESM(require("node:fs"));
14796
- var import_node_path13 = __toESM(require("node:path"));
15497
+ var import_node_fs16 = __toESM(require("node:fs"));
15498
+ var import_node_path14 = __toESM(require("node:path"));
14797
15499
 
14798
15500
  // src/edge-status.ts
14799
15501
  function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
@@ -14835,7 +15537,7 @@ function body(command, value, opts) {
14835
15537
  let parsed = {};
14836
15538
  if (value) {
14837
15539
  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;
15540
+ 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;
14839
15541
  try {
14840
15542
  parsed = JSON.parse(text);
14841
15543
  } catch (e) {
@@ -14948,11 +15650,11 @@ function write(name) {
14948
15650
  });
14949
15651
  return c;
14950
15652
  }
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);
15653
+ function lifecycle(action71) {
15654
+ return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
15655
+ const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
14954
15656
  if (!r.ok || !r.data)
14955
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15657
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
14956
15658
  output(this, { flag: r.data }, () => detail(r.data));
14957
15659
  });
14958
15660
  }
@@ -14968,8 +15670,8 @@ Examples:
14968
15670
  );
14969
15671
 
14970
15672
  // src/commands/backlog.ts
14971
- var import_node_fs16 = __toESM(require("node:fs"));
14972
- var import_node_path14 = __toESM(require("node:path"));
15673
+ var import_node_fs17 = __toESM(require("node:fs"));
15674
+ var import_node_path15 = __toESM(require("node:path"));
14973
15675
  init_source();
14974
15676
  var kind2 = "backlog";
14975
15677
  var singular = "backlog item";
@@ -14977,7 +15679,7 @@ function parse(command, value) {
14977
15679
  if (!value) return {};
14978
15680
  try {
14979
15681
  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;
15682
+ 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;
14981
15683
  const body5 = JSON.parse(text);
14982
15684
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
14983
15685
  throw new Error("body must be an object");
@@ -15027,24 +15729,24 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
15027
15729
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15028
15730
  output(this, { backlog_item: r.data }, () => detail2(r.data));
15029
15731
  });
15030
- function write2(action70) {
15031
- const c = new Command(action70).description(
15032
- `${action70 === "create" ? "Create" : "Update"} a ${singular}`
15732
+ function write2(action71) {
15733
+ const c = new Command(action71).description(
15734
+ `${action71 === "create" ? "Create" : "Update"} a ${singular}`
15033
15735
  );
15034
- if (action70 === "update") c.argument("<id>", `${singular} ID`);
15736
+ if (action71 === "update") c.argument("<id>", `${singular} ID`);
15035
15737
  c.argument("[json]", "Inline JSON, @file, or file path").option(
15036
15738
  "--file <path>",
15037
15739
  "JSON body file"
15038
15740
  );
15039
15741
  c.action(async function(...args) {
15040
15742
  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;
15743
+ const id = action71 === "update" ? String(args[0]) : void 0;
15744
+ const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15043
15745
  const body5 = parse(this, raw);
15044
15746
  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);
15747
+ const r = action71 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
15046
15748
  if (!r.ok || !r.data)
15047
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15749
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15048
15750
  output(this, { backlog_item: r.data }, () => detail2(r.data));
15049
15751
  });
15050
15752
  return c;
@@ -15093,8 +15795,8 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15093
15795
  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
15796
 
15095
15797
  // src/commands/signals.ts
15096
- var import_node_fs17 = __toESM(require("node:fs"));
15097
- var import_node_path15 = __toESM(require("node:path"));
15798
+ var import_node_fs18 = __toESM(require("node:fs"));
15799
+ var import_node_path16 = __toESM(require("node:path"));
15098
15800
  init_source();
15099
15801
  var kind3 = "signals";
15100
15802
  var singular2 = "signal";
@@ -15102,7 +15804,7 @@ function parse2(command, value) {
15102
15804
  if (!value) return {};
15103
15805
  try {
15104
15806
  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;
15807
+ 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;
15106
15808
  const body5 = JSON.parse(text);
15107
15809
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15108
15810
  throw new Error("body must be an object");
@@ -15152,24 +15854,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
15152
15854
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15153
15855
  output(this, { signal: r.data }, () => detail3(r.data));
15154
15856
  });
15155
- function write3(action70) {
15156
- const c = new Command(action70).description(
15157
- `${action70 === "create" ? "Create" : "Update"} a ${singular2}`
15857
+ function write3(action71) {
15858
+ const c = new Command(action71).description(
15859
+ `${action71 === "create" ? "Create" : "Update"} a ${singular2}`
15158
15860
  );
15159
- if (action70 === "update") c.argument("<id>", `${singular2} ID`);
15861
+ if (action71 === "update") c.argument("<id>", `${singular2} ID`);
15160
15862
  c.argument("[json]", "Inline JSON, @file, or file path").option(
15161
15863
  "--file <path>",
15162
15864
  "JSON body file"
15163
15865
  );
15164
15866
  c.action(async function(...args) {
15165
15867
  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;
15868
+ const id = action71 === "update" ? String(args[0]) : void 0;
15869
+ const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15168
15870
  const body5 = parse2(this, raw);
15169
15871
  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);
15872
+ const r = action71 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
15171
15873
  if (!r.ok || !r.data)
15172
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15874
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15173
15875
  output(this, { signal: r.data }, () => detail3(r.data));
15174
15876
  });
15175
15877
  return c;
@@ -15184,14 +15886,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
15184
15886
 
15185
15887
  // src/commands/personalizations.ts
15186
15888
  init_source();
15187
- var import_node_fs18 = __toESM(require("node:fs"));
15188
- var import_node_path16 = __toESM(require("node:path"));
15889
+ var import_node_fs19 = __toESM(require("node:fs"));
15890
+ var import_node_path17 = __toESM(require("node:path"));
15189
15891
  var kind4 = "personalizations";
15190
15892
  function body2(command, value, opts) {
15191
15893
  let parsed = {};
15192
15894
  if (value) {
15193
15895
  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;
15896
+ 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;
15195
15897
  try {
15196
15898
  parsed = JSON.parse(text);
15197
15899
  } catch (e) {
@@ -15261,11 +15963,11 @@ function write4(name) {
15261
15963
  });
15262
15964
  return c;
15263
15965
  }
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);
15966
+ function lifecycle2(action71) {
15967
+ return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
15968
+ const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
15267
15969
  if (!r.ok || !r.data)
15268
- fail(this, r.error ?? `Failed to ${action70}`, classifyStatus(r.status), r.errorDetails);
15970
+ fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15269
15971
  output(this, { personalization: r.data }, () => detail4(r.data));
15270
15972
  });
15271
15973
  }
@@ -15273,13 +15975,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
15273
15975
 
15274
15976
  // src/commands/targeting-resources.ts
15275
15977
  init_source();
15276
- var import_node_fs19 = __toESM(require("node:fs"));
15277
- var import_node_path17 = __toESM(require("node:path"));
15978
+ var import_node_fs20 = __toESM(require("node:fs"));
15979
+ var import_node_path18 = __toESM(require("node:path"));
15278
15980
  function body3(command, value) {
15279
15981
  if (!value) return {};
15280
15982
  const source = value.startsWith("@") ? value.slice(1) : value;
15281
15983
  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;
15984
+ 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;
15283
15985
  return JSON.parse(text);
15284
15986
  } catch (error) {
15285
15987
  fail(
@@ -15347,21 +16049,21 @@ function targetingResourceCommand(kind5) {
15347
16049
  );
15348
16050
  })
15349
16051
  );
15350
- for (const action70 of ["create", "update"]) {
15351
- const command = new Command(action70).description(
15352
- `${action70 === "create" ? "Create" : "Update"} a ${singular3}`
16052
+ for (const action71 of ["create", "update"]) {
16053
+ const command = new Command(action71).description(
16054
+ `${action71 === "create" ? "Create" : "Update"} a ${singular3}`
15353
16055
  );
15354
- if (action70 === "update") command.argument("<id>");
16056
+ if (action71 === "update") command.argument("<id>");
15355
16057
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
15356
16058
  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;
16059
+ const id = action71 === "update" ? String(args[0]) : void 0;
16060
+ const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
15359
16061
  const requestBody = body3(this, value);
15360
- if (action70 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
16062
+ if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
15361
16063
  fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
15362
16064
  const response = await requestTargetingResource(
15363
16065
  requireCredentials(),
15364
- action70 === "create" ? "POST" : "PATCH",
16066
+ action71 === "create" ? "POST" : "PATCH",
15365
16067
  kind5,
15366
16068
  id,
15367
16069
  requestBody
@@ -15369,7 +16071,7 @@ function targetingResourceCommand(kind5) {
15369
16071
  if (!response.ok || !response.data)
15370
16072
  fail(
15371
16073
  this,
15372
- response.error ?? `Failed to ${action70}`,
16074
+ response.error ?? `Failed to ${action71}`,
15373
16075
  classifyStatus(response.status),
15374
16076
  response.errorDetails
15375
16077
  );
@@ -15378,7 +16080,7 @@ function targetingResourceCommand(kind5) {
15378
16080
  {
15379
16081
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
15380
16082
  },
15381
- () => console.log(source_default.green(`${action70}d ${singular3}`))
16083
+ () => console.log(source_default.green(`${action71}d ${singular3}`))
15382
16084
  );
15383
16085
  });
15384
16086
  root.addCommand(command);
@@ -15452,7 +16154,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
15452
16154
  // src/commands/brand.ts
15453
16155
  init_source();
15454
16156
  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() {
16157
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
15456
16158
  const creds = requireCredentials();
15457
16159
  const res = await getBrandContext(creds);
15458
16160
  if (res.status === 404) {
@@ -15485,7 +16187,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
15485
16187
 
15486
16188
  // src/commands/targeting.ts
15487
16189
  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) {
16190
+ 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
16191
  const creds = requireCredentials();
15490
16192
  const res = await testTargeting(creds, url, { status: opts.status });
15491
16193
  if (!res.ok || !res.data) {
@@ -15530,13 +16232,21 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
15530
16232
 
15531
16233
  // src/commands/sos.ts
15532
16234
  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"]);
16235
+ var import_node_fs21 = __toESM(require("node:fs"));
16236
+ var import_node_path19 = __toESM(require("node:path"));
16237
+ var SOS_TYPES = /* @__PURE__ */ new Set([
16238
+ "full_handover",
16239
+ "design",
16240
+ "development",
16241
+ "qa",
16242
+ "config",
16243
+ "bug",
16244
+ "help"
16245
+ ]);
15536
16246
  function readText(value) {
15537
16247
  if (!value) return void 0;
15538
16248
  if (!value.startsWith("@")) return value;
15539
- return import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), value.slice(1)), "utf8");
16249
+ return import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), value.slice(1)), "utf8");
15540
16250
  }
15541
16251
  function printTicket(ticket, reused) {
15542
16252
  console.log();
@@ -15547,11 +16257,26 @@ function printTicket(ticket, reused) {
15547
16257
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
15548
16258
  console.log();
15549
16259
  }
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) {
16260
+ function printCreatedTicket(result) {
16261
+ if ("reused" in result) {
16262
+ printTicket(result.ticket, result.reused);
16263
+ return;
16264
+ }
16265
+ console.log();
16266
+ console.log(source_default.bold(result.deduped ? "Reused customer ticket" : "Created customer ticket"));
16267
+ console.log(` Reference: ${source_default.dim(result.ticket.reference)}`);
16268
+ console.log(` Status: ${result.ticket.status}`);
16269
+ console.log();
16270
+ }
16271
+ var create4 = new Command("create").description("Create an Apex SOS ticket").option(
16272
+ "--type <type>",
16273
+ "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
16274
+ "qa"
16275
+ ).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
16276
  if (!SOS_TYPES.has(opts.type)) {
15552
16277
  fail(
15553
16278
  this,
15554
- "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.",
16279
+ "Invalid SOS type. Use full_handover, design, development, qa, config, bug, or help. Internal types require a DRIP account.",
15555
16280
  3
15556
16281
  );
15557
16282
  }
@@ -15572,9 +16297,9 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
15572
16297
  res.errorDetails
15573
16298
  );
15574
16299
  }
15575
- output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
16300
+ output(this, res.data, () => printCreatedTicket(res.data));
15576
16301
  });
15577
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action61() {
16302
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
15578
16303
  const creds = requireCredentials();
15579
16304
  const res = await listMySos(creds);
15580
16305
  if (!res.ok || !res.data) {
@@ -15655,13 +16380,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
15655
16380
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
15656
16381
 
15657
16382
  // src/commands/shops.ts
15658
- var import_node_fs21 = __toESM(require("node:fs"));
15659
- var import_node_path19 = __toESM(require("node:path"));
16383
+ var import_node_fs22 = __toESM(require("node:fs"));
16384
+ var import_node_path20 = __toESM(require("node:path"));
15660
16385
  function body4(command, raw) {
15661
16386
  if (!raw) return {};
15662
16387
  try {
15663
16388
  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;
16389
+ const text = raw.startsWith("@") || import_node_fs22.default.existsSync(source) ? import_node_fs22.default.readFileSync(import_node_path20.default.resolve(source), "utf8") : raw;
15665
16390
  const value = JSON.parse(text);
15666
16391
  if (!value || typeof value !== "object" || Array.isArray(value))
15667
16392
  throw new Error("body must be an object");
@@ -15889,7 +16614,7 @@ function commandSchema(command) {
15889
16614
  };
15890
16615
  }
15891
16616
  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() {
16617
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action71() {
15893
16618
  const schema = commandSchema(program3);
15894
16619
  output(this, schema, () => {
15895
16620
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -16042,7 +16767,7 @@ function parseContainer(source, id, dataLayerName = "dataLayer") {
16042
16767
  vtpPaths,
16043
16768
  ecommerceFrom: [...ecommerce][0] ?? null,
16044
16769
  serverContainerUrl: [...serverUrls][0] ?? null,
16045
- piiPaths: vtpPaths.filter((path23) => /email|phone|address|name/i.test(path23))
16770
+ piiPaths: vtpPaths.filter((path24) => /email|phone|address|name/i.test(path24))
16046
16771
  }
16047
16772
  };
16048
16773
  }
@@ -16101,6 +16826,145 @@ function minutesValue(value) {
16101
16826
  throw new CliError("--minutes must be an integer between 1 and 20160", EXIT_CODES.validation);
16102
16827
  return parsed;
16103
16828
  }
16829
+ function isStableField(field) {
16830
+ return field.types.length > 0 && !field.type_collision;
16831
+ }
16832
+ function baseFieldTypes(field) {
16833
+ return new Set(field.types.map((type) => type.split(":")[0]));
16834
+ }
16835
+ function isContainerField(field) {
16836
+ const types = baseFieldTypes(field);
16837
+ return types.has("obj") || types.has("arr");
16838
+ }
16839
+ function hasSyntheticPlaceholder(path24) {
16840
+ return path24.split(".").map((segment) => segment.replace(/\[\]$/, "")).some((segment) => /^<[^>]+>$/.test(segment));
16841
+ }
16842
+ function ambiguousDottedPrefix(event, path24) {
16843
+ const segments = path24.split(".");
16844
+ for (let index = 1; index < segments.length; index += 1) {
16845
+ const prefix = segments.slice(0, index).join(".");
16846
+ const parent = event.fields.find((field) => field.path === prefix);
16847
+ if (!parent || !isContainerField(parent)) return prefix;
16848
+ }
16849
+ return void 0;
16850
+ }
16851
+ function dimensionEligibility(event, field) {
16852
+ const path24 = field.path;
16853
+ if (hasSyntheticPlaceholder(path24)) {
16854
+ return {
16855
+ eligible: false,
16856
+ message: `Path ${JSON.stringify(path24)} is a synthetic placeholder and cannot create a dimension. Choose an observed scalar path.`
16857
+ };
16858
+ }
16859
+ if (isContainerField(field)) {
16860
+ return {
16861
+ eligible: false,
16862
+ message: `Path ${JSON.stringify(path24)} is container-typed. Choose an observed scalar path.`
16863
+ };
16864
+ }
16865
+ if (path24.includes("[]")) {
16866
+ return {
16867
+ eligible: false,
16868
+ message: `Path ${JSON.stringify(path24)} includes an array-element segment and cannot create a reliable dimension. Choose a scalar path outside an array.`
16869
+ };
16870
+ }
16871
+ if (!isStableField(field)) {
16872
+ return {
16873
+ eligible: false,
16874
+ message: `Path ${JSON.stringify(path24)} does not have a stable observed type. Choose a path without a type collision.`
16875
+ };
16876
+ }
16877
+ if (!event.event) {
16878
+ return {
16879
+ eligible: false,
16880
+ message: `Path ${JSON.stringify(path24)} has no observed event name. Choose a path from a named event.`
16881
+ };
16882
+ }
16883
+ const ambiguousPrefix = ambiguousDottedPrefix(event, path24);
16884
+ if (ambiguousPrefix) {
16885
+ return {
16886
+ eligible: false,
16887
+ 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.`
16888
+ };
16889
+ }
16890
+ return { eligible: true };
16891
+ }
16892
+ function isNumericField(field) {
16893
+ const numericTypes = /* @__PURE__ */ new Set(["num", "numstr", "numstr_comma"]);
16894
+ return field.types.length > 0 && field.types.every((type) => numericTypes.has(type.split(":")[0]));
16895
+ }
16896
+ function defaultDimensionLabel(path24) {
16897
+ const words = path24.split(/[._-]+/).filter(Boolean).join(" ");
16898
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : path24;
16899
+ }
16900
+ function quoteShellArgument(value) {
16901
+ return "'" + value.replaceAll("'", "'\\''") + "'";
16902
+ }
16903
+ function createDimensionInvocation(path24, eventName) {
16904
+ return `apex discover --create-dimension ${quoteShellArgument(path24)} --event ${quoteShellArgument(eventName)}`;
16905
+ }
16906
+ function resolveDimensionObservation(command, report, path24, eventName) {
16907
+ const matches = report.data_layer.flatMap(
16908
+ (event) => event.fields.filter((field) => field.path === path24).map((field) => ({ event, field }))
16909
+ );
16910
+ if (!matches.length) {
16911
+ fail(
16912
+ command,
16913
+ `Path ${JSON.stringify(path24)} was not found in the current discovery report. Run apex discover and copy an observed path exactly.`,
16914
+ EXIT_CODES.validation
16915
+ );
16916
+ }
16917
+ let selected2;
16918
+ if (eventName !== void 0) {
16919
+ const eventMatches = matches.filter((match2) => match2.event.event === eventName);
16920
+ if (!eventMatches.length) {
16921
+ fail(
16922
+ command,
16923
+ `Path ${JSON.stringify(path24)} was not observed on event ${JSON.stringify(eventName)}. Run apex discover and copy the event name exactly.`,
16924
+ EXIT_CODES.validation
16925
+ );
16926
+ }
16927
+ const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
16928
+ if (layers.length > 1) {
16929
+ fail(
16930
+ command,
16931
+ `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.`,
16932
+ EXIT_CODES.validation
16933
+ );
16934
+ }
16935
+ selected2 = eventMatches[0];
16936
+ } else {
16937
+ const namedEvents = [
16938
+ ...new Set(matches.map((match2) => match2.event.event).filter(Boolean))
16939
+ ].sort();
16940
+ if (namedEvents.length > 1) {
16941
+ fail(
16942
+ command,
16943
+ `Path ${JSON.stringify(path24)} was observed on multiple events: ${namedEvents.join(", ")}. Retry with --event <name>.`,
16944
+ EXIT_CODES.validation
16945
+ );
16946
+ }
16947
+ const eventMatches = namedEvents.length ? matches.filter((match2) => match2.event.event === namedEvents[0]) : matches;
16948
+ const layers = [...new Set(eventMatches.map((match2) => match2.event.dlName))].sort();
16949
+ if (layers.length > 1) {
16950
+ fail(
16951
+ command,
16952
+ `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.`,
16953
+ EXIT_CODES.validation
16954
+ );
16955
+ }
16956
+ selected2 = eventMatches[0];
16957
+ }
16958
+ if (!selected2)
16959
+ fail(command, "No matching dimension observation was selected", EXIT_CODES.validation);
16960
+ const eligibility = dimensionEligibility(selected2.event, selected2.field);
16961
+ if (!eligibility.eligible) fail(command, eligibility.message, EXIT_CODES.validation);
16962
+ return {
16963
+ eventName: selected2.event.event,
16964
+ dlName: selected2.event.dlName,
16965
+ field: selected2.field
16966
+ };
16967
+ }
16104
16968
  function renderDiscovery(report) {
16105
16969
  const lines = [
16106
16970
  `Observed tracking schema (${report.window.minutes} minutes)`,
@@ -16120,14 +16984,18 @@ function renderDiscovery(report) {
16120
16984
  `
16121
16985
  ${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
16986
  );
16123
- for (const path23 of event.paths)
16987
+ for (const path24 of event.paths)
16124
16988
  lines.push(
16125
- ` URL ${path23.path}: ${path23.pageviews} pageviews, ${path23.count} pushes, ${path23.pspReturn_count} PSP returns`
16989
+ ` URL ${path24.path}: ${path24.pageviews} pageviews, ${path24.count} pushes, ${path24.pspReturn_count} PSP returns`
16126
16990
  );
16127
- for (const field of event.fields)
16991
+ for (const field of event.fields) {
16128
16992
  lines.push(
16129
16993
  `${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
16994
  );
16995
+ if (dimensionEligibility(event, field).eligible) {
16996
+ lines.push(` Create dimension: ${createDimensionInvocation(field.path, event.event)}`);
16997
+ }
16998
+ }
16131
16999
  }
16132
17000
  if (report.trigger_diagnoses.length) {
16133
17001
  lines.push("\n\u26A0 Trigger diagnoses");
@@ -16161,16 +17029,30 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
16161
17029
  return `${lines.join("\n")}
16162
17030
  `;
16163
17031
  }
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) {
17032
+ 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
17033
  if (opts.gtm && opts.gtmUrl)
16166
17034
  fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
17035
+ if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
17036
+ fail(
17037
+ this,
17038
+ "GTM mining cannot be combined with --create-dimension. Run discovery and creation as separate commands.",
17039
+ EXIT_CODES.validation
17040
+ );
17041
+ }
17042
+ if (!opts.createDimension) {
17043
+ const createOnlyFlag = opts.event !== void 0 ? "--event" : opts.key !== void 0 ? "--key" : opts.label !== void 0 ? "--label" : void 0;
17044
+ if (createOnlyFlag)
17045
+ fail(this, `${createOnlyFlag} requires --create-dimension`, EXIT_CODES.validation);
17046
+ }
16167
17047
  const gtmInput = opts.gtmUrl ?? opts.gtm;
16168
17048
  const minutes = minutesValue(opts.minutes) ?? 10080;
16169
17049
  let report;
16170
17050
  let apiSucceeded = false;
17051
+ let discoveryCredentials;
16171
17052
  let apiFailure;
16172
17053
  try {
16173
- const result = await discoverTrackingSchema(requireCredentials(), minutes);
17054
+ discoveryCredentials = requireCredentials();
17055
+ const result = await discoverTrackingSchema(discoveryCredentials, minutes);
16174
17056
  if (result.ok && result.data) {
16175
17057
  report = result.data;
16176
17058
  apiSucceeded = true;
@@ -16186,7 +17068,7 @@ var discoverCommand = new Command("discover").description("Read back the trackin
16186
17068
  ...error instanceof CliError ? { details: error.details, cliError: error } : {}
16187
17069
  };
16188
17070
  }
16189
- if (!report && !gtmInput) {
17071
+ if (!report && (!gtmInput || opts.createDimension)) {
16190
17072
  if (apiFailure?.cliError) throw apiFailure.cliError;
16191
17073
  fail(
16192
17074
  this,
@@ -16241,20 +17123,92 @@ var discoverCommand = new Command("discover").description("Read back the trackin
16241
17123
  report.gtm = gtm;
16242
17124
  report.sources.gtm_static = gtm.containers.length > 0;
16243
17125
  }
17126
+ if (opts.createDimension) {
17127
+ if (!apiSucceeded || !discoveryCredentials) {
17128
+ fail(
17129
+ this,
17130
+ apiFailure?.message ?? "Discovery API data is required to create a dimension",
17131
+ classifyStatus(apiFailure?.status ?? 0),
17132
+ apiFailure?.details
17133
+ );
17134
+ }
17135
+ const observation = resolveDimensionObservation(
17136
+ this,
17137
+ report,
17138
+ opts.createDimension,
17139
+ opts.event
17140
+ );
17141
+ const definition = {
17142
+ key: opts.key ?? opts.createDimension.toLowerCase(),
17143
+ label: opts.label ?? defaultDimensionLabel(opts.createDimension),
17144
+ value_type: isNumericField(observation.field) ? "number" : "string",
17145
+ capture: {
17146
+ mode: "data_layer",
17147
+ ...observation.dlName !== "dataLayer" ? { dataLayerName: observation.dlName } : {},
17148
+ eventName: observation.eventName,
17149
+ from: opts.createDimension,
17150
+ persist: true
17151
+ },
17152
+ is_dimension: true
17153
+ };
17154
+ let created;
17155
+ try {
17156
+ created = await createInsight(discoveryCredentials, "signals", definition);
17157
+ } catch (error) {
17158
+ const message = error instanceof Error ? error.message : String(error);
17159
+ fail(
17160
+ this,
17161
+ `Dimension was not created: ${message}`,
17162
+ error instanceof CliError ? error.exitCode : EXIT_CODES.general,
17163
+ error instanceof CliError ? error.details : void 0
17164
+ );
17165
+ }
17166
+ if (!created.ok || !created.data) {
17167
+ const reason = created.error ?? "The signals API rejected the request";
17168
+ if (created.status === 409) {
17169
+ fail(
17170
+ this,
17171
+ `Dimension was not created: ${reason} Choose another --key or archive an existing dimension in Audience > Attributes, then retry.`,
17172
+ classifyStatus(created.status),
17173
+ created.errorDetails
17174
+ );
17175
+ }
17176
+ if (created.status === 400) {
17177
+ fail(
17178
+ this,
17179
+ `Dimension was not created: ${reason} Check --create-dimension, --event, --key, and --label, then retry.`,
17180
+ classifyStatus(created.status),
17181
+ created.errorDetails
17182
+ );
17183
+ }
17184
+ fail(
17185
+ this,
17186
+ `Dimension was not created: ${reason}`,
17187
+ classifyStatus(created.status),
17188
+ created.errorDetails
17189
+ );
17190
+ }
17191
+ output(this, { signal: created.data }, () => {
17192
+ process.stdout.write(`Created dimension ${created.data.id}.
17193
+ `);
17194
+ process.stdout.write("Next: open Audience > Attributes in the Apex dashboard.\n");
17195
+ });
17196
+ return;
17197
+ }
16244
17198
  output(this, report, () => process.stdout.write(renderDiscovery(report)));
16245
17199
  });
16246
17200
 
16247
17201
  // src/commands/skills.ts
16248
- var import_node_fs22 = __toESM(require("node:fs"));
16249
- var import_node_path20 = __toESM(require("node:path"));
17202
+ var import_node_fs23 = __toESM(require("node:fs"));
17203
+ var import_node_path21 = __toESM(require("node:path"));
16250
17204
  function packageRoot() {
16251
17205
  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")
17206
+ import_node_path21.default.resolve(__dirname, ".."),
17207
+ import_node_path21.default.resolve(import_node_path21.default.dirname(process.argv[1] ?? process.cwd()), ".."),
17208
+ import_node_path21.default.resolve(process.cwd(), "packages/cli")
16255
17209
  ];
16256
17210
  for (const candidate of candidates) {
16257
- if (import_node_fs22.default.existsSync(import_node_path20.default.join(candidate, "AGENTS.md"))) return candidate;
17211
+ if (import_node_fs23.default.existsSync(import_node_path21.default.join(candidate, "AGENTS.md"))) return candidate;
16258
17212
  }
16259
17213
  throw new Error(
16260
17214
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -16263,22 +17217,22 @@ function packageRoot() {
16263
17217
  function skillPaths() {
16264
17218
  const root = packageRoot();
16265
17219
  return {
16266
- agents: import_node_path20.default.join(root, "AGENTS.md"),
16267
- skill: import_node_path20.default.join(root, "skills", "apex-cli", "SKILL.md")
17220
+ agents: import_node_path21.default.join(root, "AGENTS.md"),
17221
+ skill: import_node_path21.default.join(root, "skills", "apex-cli", "SKILL.md")
16268
17222
  };
16269
17223
  }
16270
17224
  function installSkill(paths, targetRoot) {
16271
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs22.default.existsSync(filePath));
17225
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs23.default.existsSync(filePath));
16272
17226
  if (missing.length > 0) {
16273
17227
  throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
16274
17228
  }
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"));
17229
+ const target = import_node_path21.default.resolve(targetRoot, "apex-cli");
17230
+ import_node_fs23.default.mkdirSync(target, { recursive: true });
17231
+ import_node_fs23.default.copyFileSync(paths.skill, import_node_path21.default.join(target, "SKILL.md"));
17232
+ import_node_fs23.default.copyFileSync(paths.agents, import_node_path21.default.join(target, "AGENTS.md"));
16279
17233
  return target;
16280
17234
  }
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) {
17235
+ 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
17236
  let paths;
16283
17237
  try {
16284
17238
  paths = skillPaths();
@@ -16308,7 +17262,7 @@ function switchValue(value) {
16308
17262
  throw new Error("Use on or off");
16309
17263
  }
16310
17264
  var settingsGet = new Command("settings").description("Show platform operator settings").action(async function() {
16311
- const res = await getAdminSettings(requireCredentials());
17265
+ const res = await getAdminSettings(requireUserCredentials());
16312
17266
  if (!res.ok || !res.data)
16313
17267
  fail(this, res.error ?? "Failed to read settings", classifyStatus(res.status));
16314
17268
  output(
@@ -16340,14 +17294,14 @@ var set5 = new Command("set").description("Set platform kill switches").option("
16340
17294
  `apex admin settings set ${switches} --confirm`
16341
17295
  );
16342
17296
  }
16343
- const res = await setAdminSettings(requireCredentials(), body5);
17297
+ const res = await setAdminSettings(requireUserCredentials(), body5);
16344
17298
  if (!res.ok || !res.data)
16345
17299
  fail(this, res.error ?? "Failed to update settings", classifyStatus(res.status));
16346
17300
  output(this, res.data, () => console.log("Platform settings updated."));
16347
17301
  });
16348
17302
  settingsGet.addCommand(set5);
16349
17303
  var quarantine = new Command("quarantine").description("List tracking quarantine events").action(async function() {
16350
- const res = await getAdminQuarantine(requireCredentials());
17304
+ const res = await getAdminQuarantine(requireUserCredentials());
16351
17305
  if (!res.ok || !res.data)
16352
17306
  fail(this, res.error ?? "Failed to read quarantine", classifyStatus(res.status));
16353
17307
  output(
@@ -16364,7 +17318,56 @@ var quarantine = new Command("quarantine").description("List tracking quarantine
16364
17318
  )
16365
17319
  );
16366
17320
  });
16367
- var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine);
17321
+ async function confirmArchive(command, shopId, force) {
17322
+ if (force) return true;
17323
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
17324
+ confirmationRequired(
17325
+ command,
17326
+ "Archiving a shop requires confirmation in non-TTY mode.",
17327
+ `apex admin shops archive ${JSON.stringify(shopId)} --force`
17328
+ );
17329
+ }
17330
+ const readline3 = await import("node:readline");
17331
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17332
+ const answer = await new Promise((resolve) => {
17333
+ rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve);
17334
+ });
17335
+ rl.close();
17336
+ if (answer.toLowerCase() !== "y") {
17337
+ console.error("Aborted.");
17338
+ return false;
17339
+ }
17340
+ return true;
17341
+ }
17342
+ 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) {
17343
+ if (!await confirmArchive(this, shopId, opts.force)) return;
17344
+ const res = await archiveAdminShop(requireUserCredentials(), shopId);
17345
+ if (!res.ok || !res.data)
17346
+ fail(
17347
+ this,
17348
+ res.error ?? "Failed to archive shop",
17349
+ classifyStatus(res.status),
17350
+ res.errorDetails
17351
+ );
17352
+ output(this, res.data, () => console.log("Shop archived."));
17353
+ });
17354
+ var restore2 = new Command("restore").description("Restore an archived shop").argument("<shopId>", "Shop ID").action(async function(shopId) {
17355
+ const res = await restoreAdminShop(requireUserCredentials(), shopId);
17356
+ if (!res.ok || !res.data)
17357
+ fail(
17358
+ this,
17359
+ res.error ?? "Failed to restore shop",
17360
+ classifyStatus(res.status),
17361
+ res.errorDetails
17362
+ );
17363
+ output(
17364
+ this,
17365
+ res.data,
17366
+ () => console.log("Shop restored. Delivery remains controlled by the kill switch.")
17367
+ );
17368
+ });
17369
+ var shops = new Command("shops").description("Archive and restore shops on the platform admin plane").addCommand(archive2).addCommand(restore2);
17370
+ var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops);
16368
17371
 
16369
17372
  // src/commands/membership.ts
16370
17373
  init_source();
@@ -16408,7 +17411,12 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
16408
17411
  orgCommand.command("members").action(async function() {
16409
17412
  await run2(this, listOrgMembers(requireUserCredentials()), "list members");
16410
17413
  });
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) {
17414
+ orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
17415
+ new Option(
17416
+ "--default-shop-role <role>",
17417
+ "Default shop role for every brand in the workspace (workspace defaults are internal roles)"
17418
+ ).choices(["editor"]).default("editor")
17419
+ ).action(async function(email, o) {
16412
17420
  if (o.role !== void 0) {
16413
17421
  process.stderr.write(
16414
17422
  "Warning: --role is deprecated for apex org invite; use --org-role instead. --role will be removed in v0.7.0.\n"
@@ -16452,7 +17460,12 @@ var teamCommand = new Command("team").description("Manage a shop team");
16452
17460
  teamCommand.command("members").requiredOption("--shop <id>").action(async function(o) {
16453
17461
  await run2(this, listTeamMembers(requireUserCredentials(), o.shop), "list team");
16454
17462
  });
16455
- teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").requiredOption("--role <role>").action(async function(email, o) {
17463
+ teamCommand.command("invite").argument("<email>").requiredOption("--shop <id>").addOption(
17464
+ new Option(
17465
+ "--role <role>",
17466
+ "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
17467
+ ).choices(["editor", "client_viewer"]).makeOptionMandatory()
17468
+ ).action(async function(email, o) {
16456
17469
  await run2(
16457
17470
  this,
16458
17471
  inviteTeam(requireUserCredentials(), email, o.shop, o.role),
@@ -16466,7 +17479,12 @@ teamCommand.command("revoke-invite").argument("<id>").requiredOption("--shop <id
16466
17479
  "revoke invitation"
16467
17480
  );
16468
17481
  });
16469
- teamCommand.command("set-role").argument("<memberId>").argument("<role>").requiredOption("--shop <id>").action(async function(id, role, o) {
17482
+ teamCommand.command("set-role").argument("<memberId>").addArgument(
17483
+ new Argument(
17484
+ "<role>",
17485
+ "Role: editor (create and run tests, manage settings, invite people) or client_viewer (view tests, results and pipeline)"
17486
+ ).choices(["editor", "client_viewer"])
17487
+ ).requiredOption("--shop <id>").action(async function(id, role, o) {
16470
17488
  await run2(this, setTeamMember(requireUserCredentials(), id, o.shop, role), "set role");
16471
17489
  });
16472
17490
  teamCommand.command("remove").argument("<memberId>").requiredOption("--shop <id>").option("-f, --force").action(async function(id, o) {
@@ -16636,8 +17654,8 @@ async function fetchText(url) {
16636
17654
  });
16637
17655
  return { status: res.status, text: await readResponseText(res), finalUrl: res.url };
16638
17656
  }
16639
- function abs(base, path23) {
16640
- return new URL(path23, base).toString();
17657
+ function abs(base, path24) {
17658
+ return new URL(path24, base).toString();
16641
17659
  }
16642
17660
  function looksLikeShopifyCart(text) {
16643
17661
  try {
@@ -16657,9 +17675,9 @@ async function collectSignals(url) {
16657
17675
  const chunks = [`__apex_primary_status_${primary.status}__`, primary.text];
16658
17676
  const protectedHit = primary.status === 403 || primary.status === 429 || matchDetectionMarkers(primary.text, "protected").length > 0;
16659
17677
  if (!protectedHit) {
16660
- for (const path23 of ["/cart.js"]) {
17678
+ for (const path24 of ["/cart.js"]) {
16661
17679
  try {
16662
- const secondary = await fetchText(abs(primary.finalUrl || url, path23));
17680
+ const secondary = await fetchText(abs(primary.finalUrl || url, path24));
16663
17681
  if (secondary.status >= 200 && secondary.status < 300 && looksLikeShopifyCart(secondary.text)) {
16664
17682
  chunks.push("__apex_secondary_shopify_cart_js__", secondary.text.slice(0, 2e3));
16665
17683
  }
@@ -16838,7 +17856,7 @@ function selectedEvents(opts) {
16838
17856
  }
16839
17857
  var list14 = new Command("list").alias("ls").description(
16840
17858
  "List webhooks. JSON includes availableEvents; secrets are never returned after create."
16841
- ).action(async function action64() {
17859
+ ).action(async function action65() {
16842
17860
  const data = await loadWebhookIndex(this);
16843
17861
  output(this, data, () => printWebhooks(data.data));
16844
17862
  });
@@ -16847,7 +17865,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
16847
17865
  "Webhook event to subscribe to; repeat for multiple events",
16848
17866
  (value, previous = []) => [...previous, value],
16849
17867
  []
16850
- ).action(async function action65(opts) {
17868
+ ).action(async function action66(opts) {
16851
17869
  const events = selectedEvents(opts);
16852
17870
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
16853
17871
  const index = await loadWebhookIndex(this);
@@ -16879,7 +17897,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
16879
17897
  }
16880
17898
  );
16881
17899
  });
16882
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action66(id) {
17900
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
16883
17901
  requireShopSecretKey(this);
16884
17902
  const result = handleResult(
16885
17903
  this,
@@ -16888,7 +17906,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
16888
17906
  );
16889
17907
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
16890
17908
  });
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) {
17909
+ 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
17910
  const limit = Number.parseInt(opts.limit, 10);
16893
17911
  if (!Number.isInteger(limit) || limit < 1)
16894
17912
  fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
@@ -16905,7 +17923,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
16905
17923
  "Replacement event; repeat for multiple events",
16906
17924
  (value, previous = []) => [...previous, value],
16907
17925
  []
16908
- ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action68(id, opts) {
17926
+ ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
16909
17927
  if (opts.active && opts.inactive)
16910
17928
  fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
16911
17929
  const events = selectedEvents(opts);
@@ -16938,7 +17956,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
16938
17956
  );
16939
17957
  output(this, { webhook: result }, () => printWebhooks([result]));
16940
17958
  });
16941
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action69(id) {
17959
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
16942
17960
  requireShopSecretKey(this);
16943
17961
  const result = handleResult(
16944
17962
  this,
@@ -16957,10 +17975,10 @@ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks"
16957
17975
 
16958
17976
  // src/commands/simulate.ts
16959
17977
  init_source();
16960
- var import_node_fs23 = __toESM(require("node:fs"));
16961
- var import_node_path21 = __toESM(require("node:path"));
17978
+ var import_node_fs24 = __toESM(require("node:fs"));
17979
+ var import_node_path22 = __toESM(require("node:path"));
16962
17980
  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");
17981
+ const text = file === "-" ? import_node_fs24.default.readFileSync(0, "utf8") : import_node_fs24.default.readFileSync(import_node_path22.default.resolve(file), "utf8");
16964
17982
  try {
16965
17983
  return JSON.parse(text);
16966
17984
  } catch (error) {
@@ -17082,8 +18100,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
17082
18100
  });
17083
18101
 
17084
18102
  // src/commands/config-resources.ts
17085
- var import_node_fs24 = __toESM(require("node:fs"));
17086
- var import_node_path22 = __toESM(require("node:path"));
18103
+ var import_node_fs25 = __toESM(require("node:fs"));
18104
+ var import_node_path23 = __toESM(require("node:path"));
17087
18105
  function show(resource) {
17088
18106
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
17089
18107
  `);
@@ -17156,12 +18174,12 @@ function readFamily(family, label) {
17156
18174
  }
17157
18175
  var assetReads = readFamily("assets", "assets");
17158
18176
  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);
18177
+ const absolute = import_node_path23.default.resolve(process.cwd(), file);
18178
+ if (!import_node_fs25.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
18179
+ const fileStat = import_node_fs25.default.statSync(absolute);
17162
18180
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
17163
18181
  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()];
18182
+ const mimeType = ASSET_MIME_TYPES[import_node_path23.default.extname(absolute).toLowerCase()];
17165
18183
  if (!mimeType)
17166
18184
  fail(
17167
18185
  this,
@@ -17170,8 +18188,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
17170
18188
  );
17171
18189
  const r = await uploadAssets(requireCredentials(), [
17172
18190
  {
17173
- name: import_node_path22.default.basename(absolute),
17174
- bytes: import_node_fs24.default.readFileSync(absolute),
18191
+ name: import_node_path23.default.basename(absolute),
18192
+ bytes: import_node_fs25.default.readFileSync(absolute),
17175
18193
  type: mimeType
17176
18194
  }
17177
18195
  ]);