@drip-apex/apex-cli 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6158,16 +6158,16 @@ __export(ora_exports, {
6158
6158
  function ora(options) {
6159
6159
  return new Ora(options);
6160
6160
  }
6161
- async function oraPromise(action71, options) {
6162
- const actionIsFunction = typeof action71 === "function";
6163
- const actionIsPromise = typeof action71.then === "function";
6161
+ async function oraPromise(action74, options) {
6162
+ const actionIsFunction = typeof action74 === "function";
6163
+ const actionIsPromise = typeof action74.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 ? action71(spinner) : action71;
6170
+ const promise = actionIsFunction ? action74(spinner) : action74;
6171
6171
  const result = await promise;
6172
6172
  spinner.succeed(
6173
6173
  successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
@@ -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.3",
7115
+ version: "0.8.0",
7116
7116
  license: "MIT",
7117
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7118
7118
  private: false,
@@ -7310,6 +7310,15 @@ function classifyApiError(status2, details) {
7310
7310
  function confirmationRequired(command, message, retry) {
7311
7311
  return fail(command, message, EXIT_CODES.confirmationRequired, { retry });
7312
7312
  }
7313
+ var liveSpinners = /* @__PURE__ */ new Set();
7314
+ var exitFallback;
7315
+ function drainAndExit(code) {
7316
+ for (const spinner of liveSpinners) spinner.stop();
7317
+ process.exitCode = code;
7318
+ if (exitFallback) clearTimeout(exitFallback);
7319
+ exitFallback = setTimeout(() => process.exit(code), 500);
7320
+ exitFallback.unref();
7321
+ }
7313
7322
  async function createSpinner(message) {
7314
7323
  if (jsonMode || !process.stderr.isTTY) {
7315
7324
  return {
@@ -7328,7 +7337,20 @@ async function createSpinner(message) {
7328
7337
  };
7329
7338
  }
7330
7339
  const { default: ora2 } = await Promise.resolve().then(() => (init_ora(), ora_exports));
7331
- return ora2(message).start();
7340
+ const spinner = ora2(message);
7341
+ const start = spinner.start.bind(spinner);
7342
+ spinner.start = (...args) => {
7343
+ liveSpinners.add(spinner);
7344
+ return start(...args);
7345
+ };
7346
+ for (const method of ["stop", "succeed", "fail"]) {
7347
+ const original = spinner[method].bind(spinner);
7348
+ spinner[method] = (...args) => {
7349
+ liveSpinners.delete(spinner);
7350
+ return original(...args);
7351
+ };
7352
+ }
7353
+ return spinner.start();
7332
7354
  }
7333
7355
  async function maybeCheckForUpdate() {
7334
7356
  if (!process.stderr.isTTY || process.env.APEX_NO_UPDATE_CHECK === "1" || CLI_VERSION === "unknown")
@@ -7350,7 +7372,8 @@ async function maybeCheckForUpdate() {
7350
7372
  async function handleCliError(err, command) {
7351
7373
  if (err && typeof err === "object" && "exitCode" in err && err.exitCode === 0) {
7352
7374
  flushFallbackEnvelope();
7353
- process.exit(0);
7375
+ drainAndExit(0);
7376
+ return;
7354
7377
  }
7355
7378
  const commanderExitCode = err && typeof err === "object" && "exitCode" in err && typeof err.exitCode === "number" ? err.exitCode : null;
7356
7379
  const cliError = err instanceof CliError ? err : new CliError(
@@ -7371,7 +7394,7 @@ async function handleCliError(err, command) {
7371
7394
  `);
7372
7395
  }
7373
7396
  }
7374
- process.exit(cliError.exitCode);
7397
+ drainAndExit(cliError.exitCode);
7375
7398
  }
7376
7399
 
7377
7400
  // ../api-client/src/credentials.ts
@@ -7754,6 +7777,15 @@ function upsertExperimentBySlug(creds, slug, body5) {
7754
7777
  function createExperiment(creds, body5) {
7755
7778
  return request(creds, "POST", "/api/v1/experiments", body5);
7756
7779
  }
7780
+ function updateExperiment(creds, id, body5) {
7781
+ return request(creds, "PATCH", `/api/v1/experiments/${encodeURIComponent(id)}`, body5);
7782
+ }
7783
+ function attachExperimentIdea(creds, id, body5) {
7784
+ return request(creds, "POST", `/api/v1/experiments/${encodeURIComponent(id)}/idea`, body5);
7785
+ }
7786
+ function detachExperimentIdea(creds, id) {
7787
+ return request(creds, "DELETE", `/api/v1/experiments/${encodeURIComponent(id)}/idea`);
7788
+ }
7757
7789
  function pauseExperiment(creds, id) {
7758
7790
  return request(creds, "POST", `/api/v1/experiments/${id}/pause`);
7759
7791
  }
@@ -7781,7 +7813,13 @@ function createExperimentScreenshots(creds, id, body5) {
7781
7813
  }
7782
7814
  async function getScreenshotJob(creds, jobId, timeoutMs) {
7783
7815
  await testScreenshotDelay?.(timeoutMs);
7784
- return request(creds, "GET", `/api/v1/screenshots/${jobId}`, void 0, timeoutMs);
7816
+ return request(
7817
+ creds,
7818
+ "GET",
7819
+ `/api/v1/screenshots/${encodeURIComponent(jobId)}`,
7820
+ void 0,
7821
+ timeoutMs
7822
+ );
7785
7823
  }
7786
7824
  function planExperimentFilesystem(creds, id, body5) {
7787
7825
  return request(
@@ -8122,8 +8160,8 @@ function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor
8122
8160
  const path24 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8123
8161
  return request(creds, method, path24, body5);
8124
8162
  }
8125
- function managedPath(kind5, id, action71) {
8126
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action71 ? `/${action71}` : ""}`;
8163
+ function managedPath(kind5, id, action74) {
8164
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action74 ? `/${action74}` : ""}`;
8127
8165
  }
8128
8166
  function listManagedDeliveries(creds, kind5, limit) {
8129
8167
  const q = limit ? `?limit=${limit}` : "";
@@ -8141,8 +8179,8 @@ function createManagedDelivery(creds, kind5, body5) {
8141
8179
  function updateManagedDelivery(creds, kind5, id, body5) {
8142
8180
  return request(creds, "PATCH", managedPath(kind5, id), body5);
8143
8181
  }
8144
- function transitionManagedDelivery(creds, kind5, id, action71) {
8145
- return request(creds, "POST", managedPath(kind5, id, action71));
8182
+ function transitionManagedDelivery(creds, kind5, id, action74) {
8183
+ return request(creds, "POST", managedPath(kind5, id, action74));
8146
8184
  }
8147
8185
  function listConfigResources(creds, family, opts) {
8148
8186
  const params = new URLSearchParams();
@@ -8301,6 +8339,28 @@ function restoreBacklogInsight(creds, id) {
8301
8339
  function deleteInsight(creds, kind5, id) {
8302
8340
  return request(creds, "DELETE", insightPath(kind5, id));
8303
8341
  }
8342
+ function listBacklogFields(creds, includeArchived) {
8343
+ const query = includeArchived ? "?include_archived=true" : "";
8344
+ return request(creds, "GET", `/api/v1/backlog/fields${query}`);
8345
+ }
8346
+ function createBacklogField(creds, body5) {
8347
+ return request(creds, "POST", "/api/v1/backlog/fields", body5);
8348
+ }
8349
+ function updateBacklogField(creds, id, body5) {
8350
+ return request(
8351
+ creds,
8352
+ "PATCH",
8353
+ `/api/v1/backlog/fields/${encodeURIComponent(id)}`,
8354
+ body5
8355
+ );
8356
+ }
8357
+ function archiveBacklogField(creds, id) {
8358
+ return request(
8359
+ creds,
8360
+ "DELETE",
8361
+ `/api/v1/backlog/fields/${encodeURIComponent(id)}`
8362
+ );
8363
+ }
8304
8364
  var listShops = (c) => userRequest(c, "GET", "/api/v1/shops");
8305
8365
  var getShop = (c, id) => userRequest(c, "GET", `/api/v1/shops/${id}`);
8306
8366
  var createShop = (c, body5) => userRequest(c, "POST", "/api/v1/shops", body5);
@@ -8835,6 +8895,43 @@ var import_node_crypto4 = __toESM(require("node:crypto"));
8835
8895
  var import_node_fs7 = __toESM(require("node:fs"));
8836
8896
  var import_node_path5 = __toESM(require("node:path"));
8837
8897
 
8898
+ // src/lib/qa-vocabulary.ts
8899
+ var checklistStatuses = [
8900
+ "pending",
8901
+ "pass",
8902
+ "warning",
8903
+ "fail",
8904
+ "accepted",
8905
+ "skipped"
8906
+ ];
8907
+ var evidenceStatuses = [
8908
+ "captured",
8909
+ "passed",
8910
+ "warning",
8911
+ "failed",
8912
+ "pending",
8913
+ "not_run"
8914
+ ];
8915
+ var evidenceKinds = [
8916
+ "screenshot",
8917
+ "goal",
8918
+ "summary",
8919
+ "launch_capture",
8920
+ "selector_audit",
8921
+ "console_network",
8922
+ "qa_audit",
8923
+ "readiness"
8924
+ ];
8925
+
8926
+ // src/lib/dashboard-url.ts
8927
+ function buildDashboardUrl(baseUrl, shopId, experimentId) {
8928
+ if (!shopId) return null;
8929
+ const url = new URL("/api/shops/switch", baseUrl);
8930
+ url.searchParams.set("shopId", shopId);
8931
+ url.searchParams.set("redirect", "/experiments/" + experimentId);
8932
+ return url.toString();
8933
+ }
8934
+
8838
8935
  // ../api-client/src/qa-preview-url.ts
8839
8936
  var FORCE_ALIAS_PARAM_NAMES = ["drip_experiment_id", "drip_experiment", "drip_variation_id", "drip_variation"];
8840
8937
  function applyQaPreviewParams(url, params) {
@@ -8872,6 +8969,112 @@ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
8872
8969
  return url;
8873
8970
  }
8874
8971
 
8972
+ // src/lib/test-key.ts
8973
+ var TEST_KEY_RE = /^([A-Z0-9]{2,8})-(\d+)$/;
8974
+ function isTestKeyReference(value) {
8975
+ return value === value.toUpperCase() && TEST_KEY_RE.test(value.toUpperCase());
8976
+ }
8977
+
8978
+ // src/lib/effective-page-targeting.ts
8979
+ var ALL_PAGES_LABEL = "All pages";
8980
+ var CHECKOUT_LABEL = "Checkout";
8981
+ var UNKNOWN_PAGE_NAME = "Saved Page";
8982
+ function isRecord(value) {
8983
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8984
+ }
8985
+ function arrayOf(value) {
8986
+ if (Array.isArray(value)) return value;
8987
+ if (value === void 0 || value === null) return [];
8988
+ return [value];
8989
+ }
8990
+ function ruleKey(rule) {
8991
+ if (typeof rule === "string") return JSON.stringify(["include", "simple", rule.trim(), ""]);
8992
+ if (!isRecord(rule)) return "";
8993
+ const candidate = rule;
8994
+ return JSON.stringify([
8995
+ candidate.include === false ? "exclude" : "include",
8996
+ candidate.type ?? "simple",
8997
+ typeof candidate.pattern === "string" ? candidate.pattern : "",
8998
+ candidate.matchType ?? candidate.match_type ?? ""
8999
+ ]);
9000
+ }
9001
+ function authoredUrlRules(raw) {
9002
+ if (Array.isArray(raw.authorUrl)) return raw.authorUrl;
9003
+ const url = [...arrayOf(raw.url), ...arrayOf(raw.urls), ...arrayOf(raw.rules)];
9004
+ if (url.length === 0) return [];
9005
+ const derived = /* @__PURE__ */ new Set();
9006
+ for (const candidate of arrayOf(raw.pageRules)) {
9007
+ if (!isRecord(candidate)) continue;
9008
+ for (const rule of [...arrayOf(candidate.includeRules), ...arrayOf(candidate.excludeRules)]) {
9009
+ derived.add(ruleKey(rule));
9010
+ }
9011
+ }
9012
+ return url.filter((rule) => !derived.has(ruleKey(rule)));
9013
+ }
9014
+ function pageEntries(raw) {
9015
+ const byId = /* @__PURE__ */ new Map();
9016
+ for (const candidate of arrayOf(raw.pageRules)) {
9017
+ if (!isRecord(candidate) || typeof candidate.id !== "string" || !candidate.id) continue;
9018
+ byId.set(candidate.id, {
9019
+ name: typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : null,
9020
+ include: candidate.include !== false
9021
+ });
9022
+ }
9023
+ for (const candidate of arrayOf(raw.pages)) {
9024
+ const id = typeof candidate === "string" ? candidate : isRecord(candidate) && typeof candidate.id === "string" ? candidate.id : "";
9025
+ if (!id) continue;
9026
+ byId.set(id, {
9027
+ name: byId.get(id)?.name ?? null,
9028
+ include: isRecord(candidate) && candidate.include === false ? false : true
9029
+ });
9030
+ }
9031
+ return [...byId.values()];
9032
+ }
9033
+ function urlRuleClause(count) {
9034
+ return count === 1 ? "1 URL rule" : `${count} URL rules`;
9035
+ }
9036
+ function effectivePageTargetingLabel(targeting) {
9037
+ const raw = isRecord(targeting) ? targeting : {};
9038
+ if (raw.surface === "checkout" || isRecord(raw.checkoutBlock) && raw.checkoutBlock.enabled === true) {
9039
+ return CHECKOUT_LABEL;
9040
+ }
9041
+ const pages = pageEntries(raw);
9042
+ const rules = authoredUrlRules(raw);
9043
+ const pagesLabel = pages.map((page) => {
9044
+ const readable = page.name ?? UNKNOWN_PAGE_NAME;
9045
+ return page.include ? readable : `Exclude ${readable}`;
9046
+ }).join(", ");
9047
+ if (pages.length && rules.length) return `${pagesLabel} and ${urlRuleClause(rules.length)}`;
9048
+ if (pages.length) return pagesLabel;
9049
+ if (rules.length) return `${urlRuleClause(rules.length)} (no saved Page)`;
9050
+ return ALL_PAGES_LABEL;
9051
+ }
9052
+
9053
+ // ../api-client/src/idea-reference.ts
9054
+ var IDEA_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9055
+ var IDEA_KEY = /^[A-Za-z0-9]{2,8}-\d+$/;
9056
+ function classifyIdeaReference(reference) {
9057
+ const trimmed = reference.trim();
9058
+ if (IDEA_UUID.test(trimmed)) return { kind: "uuid", value: trimmed };
9059
+ if (IDEA_KEY.test(trimmed))
9060
+ return { kind: "key", value: trimmed.toUpperCase() };
9061
+ return { kind: "invalid", value: trimmed };
9062
+ }
9063
+
9064
+ // ../api-client/src/experiment-create.ts
9065
+ function deriveStoreUrl(patterns) {
9066
+ for (const pattern of patterns) {
9067
+ const trimmed = pattern.trim();
9068
+ if (trimmed.includes("*")) continue;
9069
+ try {
9070
+ const url = new URL(trimmed);
9071
+ if (url.protocol === "http:" || url.protocol === "https:") return trimmed;
9072
+ } catch {
9073
+ }
9074
+ }
9075
+ return null;
9076
+ }
9077
+
8875
9078
  // src/lib/asset-upload.ts
8876
9079
  var import_node_crypto3 = __toESM(require("node:crypto"));
8877
9080
  var import_node_fs6 = __toESM(require("node:fs"));
@@ -8951,6 +9154,7 @@ function createAssetReferenceRewriter(options) {
8951
9154
  let pending = uploadsByHash.get(hash);
8952
9155
  if (!pending) {
8953
9156
  pending = options.upload(file).then((asset) => {
9157
+ if (asset === null) return null;
8954
9158
  if (!asset.id) throw new Error(`Asset upload for ${file.name} returned no ID.`);
8955
9159
  const url = deliveryUrl(options.baseUrl, asset);
8956
9160
  options.onUpload?.({ name: file.name, url, source });
@@ -8979,7 +9183,7 @@ function createAssetReferenceRewriter(options) {
8979
9183
  const hash = contentHash(bytes);
8980
9184
  const name = `inline-${hash.slice(0, 12)}${extensionForMimeType(mimeType)}`;
8981
9185
  const url = await uploadOnce({ name, bytes, type: mimeType }, "inline");
8982
- rewritten = rewritten.split(uri).join(url);
9186
+ if (url !== null) rewritten = rewritten.split(uri).join(url);
8983
9187
  }
8984
9188
  const localAssetUrl = async (reference) => {
8985
9189
  if (reference.startsWith("/") || reference.startsWith("data:") || reference.includes("://")) {
@@ -9066,14 +9270,24 @@ function formatDate(iso) {
9066
9270
  year: "numeric"
9067
9271
  });
9068
9272
  }
9069
- function printExperiment(exp) {
9273
+ function printExperiment(exp, dashboardUrl = null) {
9070
9274
  console.log();
9071
9275
  console.log(source_default.bold(exp.name));
9276
+ if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
9072
9277
  console.log(` ID: ${source_default.dim(exp.id)}`);
9073
9278
  if (exp.slug) console.log(` Slug: ${source_default.cyan(exp.slug)}`);
9074
9279
  console.log(` Status: ${statusBadge(exp.status)}`);
9280
+ console.log(` Classification: ${exp.counting_type}`);
9075
9281
  console.log(` Traffic: ${(exp.traffic_allocation * 100).toFixed(0)}%`);
9282
+ if (exp.targeting !== void 0) {
9283
+ console.log(` Targeting: ${effectivePageTargetingLabel(exp.targeting)}`);
9284
+ }
9076
9285
  console.log(` Created: ${formatDate(exp.created_at)}`);
9286
+ if (exp.linked_idea) {
9287
+ const label = exp.linked_idea.key ? `${exp.linked_idea.key}: ` : "";
9288
+ const stage = exp.linked_idea.stage ? ` (${exp.linked_idea.stage})` : "";
9289
+ console.log(` Idea: ${label}${exp.linked_idea.title}${stage}`);
9290
+ }
9077
9291
  if (exp.variations && exp.variations.length > 0) {
9078
9292
  console.log(` Variations:`);
9079
9293
  for (const v of exp.variations) {
@@ -9122,6 +9336,21 @@ function printExperimentResults(results2, currency) {
9122
9336
  console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
9123
9337
  console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
9124
9338
  console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
9339
+ const resultsWeekdays = results2.results_weekdays;
9340
+ if (resultsWeekdays && Array.isArray(resultsWeekdays.days)) {
9341
+ const labels = {
9342
+ monday: "Mon",
9343
+ tuesday: "Tue",
9344
+ wednesday: "Wed",
9345
+ thursday: "Thu",
9346
+ friday: "Fri",
9347
+ saturday: "Sat",
9348
+ sunday: "Sun"
9349
+ };
9350
+ console.log(
9351
+ ` Judged on: ${resultsWeekdays.days.map((day) => labels[String(day)] ?? day).join(", ")} (${String(resultsWeekdays.timezone)})`
9352
+ );
9353
+ }
9125
9354
  console.log(` Statistics: ${statisticsStateLabel(statisticsState)}`);
9126
9355
  if (decision.treatmentName) {
9127
9356
  console.log(` Treatment: ${String(decision.treatmentName)}`);
@@ -9275,21 +9504,26 @@ async function rewriteExperimentFilesystemAssets(input) {
9275
9504
  baseUrl: input.baseUrl,
9276
9505
  enabled: input.enabled,
9277
9506
  upload: async (file) => {
9507
+ const hash = import_node_crypto4.default.createHash("sha256").update(file.bytes).digest("hex");
9508
+ const cached = input.assets[hash];
9509
+ if (cached) return cached;
9510
+ if (input.upload === null) return null;
9278
9511
  const response = await input.upload(input.credentials, [file]);
9279
9512
  const asset = response.data?.assets?.[0];
9280
9513
  if (!response.ok || !asset?.id) {
9281
9514
  throw new Error(response.error ?? `Failed to upload ${file.name}.`);
9282
9515
  }
9283
- return {
9516
+ const uploaded = {
9284
9517
  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
9518
+ url: typeof asset.url === "string" ? asset.url : `${new URL(input.baseUrl).origin}/api/assets/${encodeURIComponent(asset.id)}/file`,
9519
+ deliveryUrl: typeof asset.deliveryUrl === "string" ? asset.deliveryUrl : typeof asset.delivery_url === "string" ? asset.delivery_url : void 0
9288
9520
  };
9289
- },
9290
- onUpload: ({ name, url }) => {
9291
- process.stderr.write(`Uploaded asset ${name} -> ${url}
9292
- `);
9521
+ input.assets[hash] = uploaded;
9522
+ process.stderr.write(
9523
+ `Uploaded asset ${file.name} -> ${uploaded.deliveryUrl ?? uploaded.url}
9524
+ `
9525
+ );
9526
+ return uploaded;
9293
9527
  }
9294
9528
  });
9295
9529
  return Promise.all(
@@ -9335,6 +9569,7 @@ function printFilesystemPlan(plan) {
9335
9569
  }
9336
9570
  console.log();
9337
9571
  console.log(`QA invalidated: ${plan.invalidatesQa ? source_default.yellow("yes") : "no"}`);
9572
+ console.log(`Acknowledgment: ${plan.invalidatesQa ? "--accept-qa-invalidation" : "none"}`);
9338
9573
  console.log(`Precompile: ${plan.precompileRequired ? "yes" : "no"}`);
9339
9574
  }
9340
9575
  function blockingIssueSummary(issues) {
@@ -9343,11 +9578,21 @@ function blockingIssueSummary(issues) {
9343
9578
  return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
9344
9579
  }
9345
9580
  async function getQaPreviewUrls(creds, experimentId) {
9581
+ const fallbackDashboardUrl = buildDashboardUrl(
9582
+ creds.baseUrl,
9583
+ creds.resolvedContext?.shopId,
9584
+ experimentId
9585
+ );
9346
9586
  try {
9347
9587
  const res = await getExperiment(creds, experimentId);
9588
+ const dashboardUrl = buildDashboardUrl(
9589
+ creds.baseUrl,
9590
+ res.data?.shop_id ?? creds.resolvedContext?.shopId,
9591
+ experimentId
9592
+ );
9348
9593
  const storeUrl = res.data?.store_url;
9349
9594
  if (!res.ok || !res.data || !storeUrl) {
9350
- return { previewUrls: [], unavailable: true };
9595
+ return { previewUrls: [], unavailable: true, dashboardUrl };
9351
9596
  }
9352
9597
  const experiment = res.data;
9353
9598
  const previewUrls = (experiment.variations ?? []).filter((variation) => !variation.is_control).map((variation) => ({
@@ -9355,9 +9600,9 @@ async function getQaPreviewUrls(creds, experimentId) {
9355
9600
  variationName: variation.name,
9356
9601
  url: buildQaPreviewUrl2(storeUrl, experiment.id, variation.id).toString()
9357
9602
  }));
9358
- return { previewUrls, unavailable: false };
9603
+ return { previewUrls, unavailable: false, dashboardUrl };
9359
9604
  } catch {
9360
- return { previewUrls: [], unavailable: true };
9605
+ return { previewUrls: [], unavailable: true, dashboardUrl: fallbackDashboardUrl };
9361
9606
  }
9362
9607
  }
9363
9608
  function printQaPreviewUrls(experimentId, result) {
@@ -9430,8 +9675,29 @@ function parseEvidenceInput(value) {
9430
9675
  }
9431
9676
  return parsed;
9432
9677
  }
9433
- function isQaCheckStatus(value) {
9434
- return value === "pending" || value === "pass" || value === "warning" || value === "fail" || value === "accepted" || value === "skipped";
9678
+ function qaChoice(flags, description, choices) {
9679
+ return new Option(flags, description).choices([...choices]).argParser((value) => value);
9680
+ }
9681
+ function validateQaChoice(command, option, value, allowed2) {
9682
+ if (value !== void 0 && !allowed2.includes(value)) {
9683
+ fail(command, `Invalid ${option}: ${value}`, EXIT_CODES.validation, {
9684
+ option,
9685
+ allowedValues: [...allowed2]
9686
+ });
9687
+ }
9688
+ }
9689
+ function qaSummary(run3, changed) {
9690
+ return {
9691
+ run: {
9692
+ id: run3.id,
9693
+ status: run3.status,
9694
+ summary: run3.summary,
9695
+ approvedAt: run3.approvedAt,
9696
+ updatedAt: run3.updatedAt
9697
+ },
9698
+ changed,
9699
+ validationErrors: []
9700
+ };
9435
9701
  }
9436
9702
  function tryReadFilesystemSession(folder) {
9437
9703
  try {
@@ -9455,13 +9721,15 @@ async function resolveExperimentForFilesystemOrId(value) {
9455
9721
  return { experimentId: exp.id, folder: null, label: exp.name };
9456
9722
  }
9457
9723
  async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experimentId) {
9458
- if (!folder) return;
9724
+ if (!folder) return false;
9459
9725
  try {
9460
9726
  const refreshed = await getExperimentFilesystem(creds, experimentId);
9461
9727
  if (refreshed.ok && refreshed.data) {
9728
+ const assets = tryReadFilesystemSession(folder)?.assets;
9462
9729
  writeExperimentFilesystemFiles(folder, refreshed.data.files);
9463
9730
  writeFilesystemSession(folder, {
9464
9731
  schemaVersion: 1,
9732
+ ...assets ? { assets } : {},
9465
9733
  baseUrl: creds.baseUrl,
9466
9734
  experimentId,
9467
9735
  pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9469,11 +9737,13 @@ async function refreshExperimentFilesystemFolderIfNeeded(creds, folder, experime
9469
9737
  refreshed.data.files.map((file) => [file.path, { sha256: file.sha256 }])
9470
9738
  )
9471
9739
  });
9740
+ return true;
9472
9741
  }
9473
9742
  } catch {
9474
9743
  }
9744
+ return false;
9475
9745
  }
9476
- async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
9746
+ async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId, updateManifest = true) {
9477
9747
  if (!folder) return;
9478
9748
  try {
9479
9749
  const refreshed = await getExperimentFilesystem(creds, experimentId);
@@ -9484,23 +9754,25 @@ async function refreshServerOwnedFilesIfNeeded(creds, folder, experimentId) {
9484
9754
  if (serverOwned.length === 0) return;
9485
9755
  const session = readFilesystemSession(folder);
9486
9756
  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
- });
9757
+ if (updateManifest)
9758
+ writeFilesystemSession(folder, {
9759
+ ...session,
9760
+ files: {
9761
+ ...session.files,
9762
+ ...Object.fromEntries(serverOwned.map((file) => [file.path, { sha256: file.sha256 }]))
9763
+ }
9764
+ });
9494
9765
  } catch {
9495
9766
  }
9496
9767
  }
9768
+ var EXPERIMENT_REFERENCE_DESCRIPTION = "Experiment ID, slug, or test key (for example BIO-085)";
9497
9769
  async function resolveExperiment(idOrSlug) {
9498
9770
  const creds = requireCredentials();
9499
- if (/^[0-9a-f-]{36}$/i.test(idOrSlug)) {
9771
+ if (/^[0-9a-f-]{36}$/i.test(idOrSlug) || /^[1-9][0-9]{9}$/.test(idOrSlug) || isTestKeyReference(idOrSlug)) {
9500
9772
  const res2 = await getExperiment(creds, idOrSlug);
9501
9773
  if (res2.ok) return res2.data;
9502
9774
  throw new CliError(
9503
- res2.error ?? `Experiment "${idOrSlug}" not found`,
9775
+ res2.status === 404 && isTestKeyReference(idOrSlug) ? `Experiment "${idOrSlug}" not found in selected shop ${creds.resolvedContext?.shopId ?? "(API key shop)"}. Test keys resolve within the selected shop; use --shop <shop-id> to change it.` : res2.error ?? `Experiment "${idOrSlug}" not found`,
9504
9776
  classifyStatus(res2.status),
9505
9777
  res2.errorDetails
9506
9778
  );
@@ -9554,16 +9826,59 @@ var list = new Command("list").alias("ls").description("List experiments (archiv
9554
9826
  }
9555
9827
  });
9556
9828
  });
9557
- var get = new Command("get").description("Get experiment details by ID or slug").argument("<id-or-slug>", "Experiment ID or slug").action(async function action4(idOrSlug) {
9829
+ var get = new Command("get").description("Get experiment details by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action4(idOrSlug) {
9558
9830
  const spinner = await createSpinner("Fetching experiment...");
9559
9831
  const exp = await resolveExperiment(idOrSlug);
9560
9832
  spinner?.stop();
9561
9833
  if (!exp) {
9562
9834
  fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
9563
9835
  }
9564
- output(this, { experiment: exp }, () => printExperiment(exp));
9836
+ const creds = requireCredentials();
9837
+ const dashboardUrl = buildDashboardUrl(
9838
+ creds.baseUrl,
9839
+ exp.shop_id ?? creds.resolvedContext?.shopId,
9840
+ exp.id
9841
+ );
9842
+ output(
9843
+ this,
9844
+ { experiment: exp, dashboard_url: dashboardUrl },
9845
+ () => printExperiment(exp, dashboardUrl)
9846
+ );
9565
9847
  });
9566
- var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", "Experiment ID or slug").option("--currency <code>", "Currency code for revenue formatting").action(async function action5(idOrSlug, opts) {
9848
+ var attachIdea = new Command("attach-idea").description("Attach an experiment to an idea").argument("<experiment>", EXPERIMENT_REFERENCE_DESCRIPTION).requiredOption("--idea <key>", "Idea UUID or Apex backlog key").option("--follow-up", "Attach as a follow-up test").action(async function action5(experimentRef, opts) {
9849
+ const experiment = await resolveExperiment(experimentRef);
9850
+ if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
9851
+ const classified = classifyIdeaReference(opts.idea);
9852
+ if (classified.kind === "invalid") {
9853
+ fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
9854
+ }
9855
+ const res = await attachExperimentIdea(requireCredentials(), experiment.id, {
9856
+ ...classified.kind === "uuid" ? { backlog_item_id: classified.value } : { idea_key: classified.value },
9857
+ ...opts.followUp ? { as_follow_up: true } : {}
9858
+ });
9859
+ if (!res.ok)
9860
+ fail(
9861
+ this,
9862
+ res.error ?? "Failed to attach idea",
9863
+ classifyStatus(res.status),
9864
+ res.errorDetails
9865
+ );
9866
+ output(this, res.data, () => printSuccess("Idea attached."));
9867
+ });
9868
+ var detachIdea = new Command("detach-idea").description("Detach an experiment from its idea").argument("<experiment>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action6(experimentRef) {
9869
+ const experiment = await resolveExperiment(experimentRef);
9870
+ if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
9871
+ const res = await detachExperimentIdea(requireCredentials(), experiment.id);
9872
+ if (!res.ok)
9873
+ fail(
9874
+ this,
9875
+ res.error ?? "Failed to detach idea",
9876
+ classifyStatus(res.status),
9877
+ res.errorDetails
9878
+ );
9879
+ output(this, res.data, () => printSuccess("Idea detached."));
9880
+ });
9881
+ var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").action(async function action7(idOrSlug, opts) {
9567
9882
  const creds = requireCredentials();
9568
9883
  const spinner = await createSpinner("Fetching experiment results...");
9569
9884
  try {
@@ -9594,6 +9909,62 @@ var results = new Command("results").description("Read experiment results and ti
9594
9909
  fail(this, error instanceof Error ? error.message : String(error));
9595
9910
  }
9596
9911
  });
9912
+ var DAY_NAMES = {
9913
+ mon: "monday",
9914
+ monday: "monday",
9915
+ tue: "tuesday",
9916
+ tuesday: "tuesday",
9917
+ wed: "wednesday",
9918
+ wednesday: "wednesday",
9919
+ thu: "thursday",
9920
+ thursday: "thursday",
9921
+ fri: "friday",
9922
+ friday: "friday",
9923
+ sat: "saturday",
9924
+ saturday: "saturday",
9925
+ sun: "sunday",
9926
+ sunday: "sunday"
9927
+ };
9928
+ var countingTypes = ["standard", "setup_test", "aa_test", "non_program"];
9929
+ var update = new Command("update").description("Update an experiment").addOption(
9930
+ new Option("--counting-type <type>", "Draft experiment classification").choices(countingTypes)
9931
+ ).argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--results-weekdays <days>", "Comma-separated weekdays").option("--results-timezone <timezone>", "IANA timezone for the results window").option("--clear-results-weekdays", "Clear the results weekday window").action(async function action8(reference, opts) {
9932
+ if (opts.clearResultsWeekdays && (opts.resultsWeekdays || opts.resultsTimezone)) {
9933
+ fail(
9934
+ this,
9935
+ "--clear-results-weekdays cannot be combined with window options",
9936
+ EXIT_CODES.validation
9937
+ );
9938
+ }
9939
+ const hasWindowOption = opts.resultsWeekdays !== void 0 || opts.resultsTimezone !== void 0;
9940
+ if (!opts.clearResultsWeekdays && (!opts.countingType && !hasWindowOption || hasWindowOption && (!opts.resultsWeekdays || !opts.resultsTimezone))) {
9941
+ fail(
9942
+ this,
9943
+ "--results-weekdays and --results-timezone are required together",
9944
+ EXIT_CODES.validation
9945
+ );
9946
+ }
9947
+ const days = opts.resultsWeekdays?.split(",").map((day) => DAY_NAMES[day.trim().toLowerCase()]);
9948
+ if (days?.some((day) => !day)) fail(this, "Invalid weekday token", EXIT_CODES.validation);
9949
+ const exp = await resolveExperiment(reference);
9950
+ if (!exp) fail(this, `Experiment "${reference}" not found.`, EXIT_CODES.validation);
9951
+ const results_weekdays = opts.clearResultsWeekdays ? null : { days, timezone: opts.resultsTimezone };
9952
+ const hasWindow = opts.clearResultsWeekdays || opts.resultsWeekdays !== void 0;
9953
+ const res = await updateExperiment(requireCredentials(), exp.id, {
9954
+ ...hasWindow ? { results_weekdays } : {},
9955
+ ...opts.countingType !== void 0 ? { counting_type: opts.countingType } : {}
9956
+ });
9957
+ if (!res.ok) fail(this, res.error ?? "Failed to update experiment", classifyStatus(res.status));
9958
+ const stored = res.data?.results_weekdays ?? null;
9959
+ output(this, res.data, () => {
9960
+ if (opts.countingType !== void 0)
9961
+ console.log(`Classification: ${res.data.counting_type}`);
9962
+ if (hasWindow)
9963
+ console.log(
9964
+ stored?.days?.length ? `Judged on: ${stored.days.map((day) => day.slice(0, 3).replace(/^./, (c) => c.toUpperCase())).join(", ")} (${stored.timezone})` : "Results weekday window cleared: judged on all days."
9965
+ );
9966
+ });
9967
+ });
9597
9968
  function collectCreateOption(value, previous) {
9598
9969
  return [...previous, value];
9599
9970
  }
@@ -9610,30 +9981,238 @@ function parseHttpDestination(value) {
9610
9981
  }
9611
9982
  return trimmed;
9612
9983
  }
9613
- function sourceStoreUrl(sourceRules) {
9614
- for (const rule of sourceRules) {
9615
- if (rule.includes("*")) continue;
9616
- try {
9617
- const url = new URL(rule);
9618
- if (url.protocol === "http:" || url.protocol === "https:") {
9619
- return rule;
9620
- }
9621
- } catch {
9984
+ function readCreateJson(command, reference) {
9985
+ const file = reference.startsWith("@") ? reference.slice(1) : reference;
9986
+ try {
9987
+ const parsed = JSON.parse(import_node_fs7.default.readFileSync(import_node_path5.default.resolve(process.cwd(), file), "utf8"));
9988
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
9989
+ throw new Error("expected an object");
9990
+ return parsed;
9991
+ } catch (error) {
9992
+ fail(
9993
+ command,
9994
+ `Invalid audience JSON ${reference}: ${error instanceof Error ? error.message : String(error)}`,
9995
+ EXIT_CODES.validation
9996
+ );
9997
+ }
9998
+ }
9999
+ var GOAL_TYPES = ["click", "pageview", "custom", "revenue"];
10000
+ var GOAL_MEASUREMENTS = ["conversion", "value"];
10001
+ var GOAL_VALUE_UNITS = ["currency", "number", "duration_seconds", "percent"];
10002
+ var INLINE_GOAL_FIELDS = [
10003
+ "name",
10004
+ "type",
10005
+ "measurement",
10006
+ "value_property",
10007
+ "value_unit",
10008
+ "selector",
10009
+ "text_match",
10010
+ "url_pattern",
10011
+ "page_id",
10012
+ "event_name",
10013
+ "count_once"
10014
+ ];
10015
+ var GOAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10016
+ var GOAL_EVENT_NAME = /^[a-zA-Z0-9_.:-]{1,120}$/;
10017
+ var GOAL_VALUE_PROPERTY = /^[A-Za-z0-9_.-]{1,120}$/;
10018
+ function normalizeNewGoalKey(key) {
10019
+ const normalized = key.trim().toLowerCase().replace(/-/g, "_");
10020
+ return normalized === "page" ? "page_id" : normalized;
10021
+ }
10022
+ function validateNewGoal(command, value, body5) {
10023
+ const invalid = (reason) => fail(command, `Invalid --goal "new:${value}"; ${reason}`, EXIT_CODES.validation);
10024
+ const text = (field) => {
10025
+ const raw = body5[field];
10026
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
10027
+ };
10028
+ for (const key of Object.keys(body5)) {
10029
+ if (INLINE_GOAL_FIELDS.includes(key)) continue;
10030
+ invalid(
10031
+ key === "capture" || key === "filter" ? `${key} is not expressible inline; create the goal with \`apex goals create\` and pass its id` : `unknown field "${key}"; inline fields are ${INLINE_GOAL_FIELDS.join(", ")}`
10032
+ );
10033
+ }
10034
+ const type = text("type");
10035
+ const name = text("name");
10036
+ if (!type || !name) invalid("type and name are required");
10037
+ if (!GOAL_TYPES.includes(type)) invalid(`type must be one of ${GOAL_TYPES.join(", ")}`);
10038
+ if (name.length > 255) invalid("name must be 255 characters or fewer");
10039
+ const measurement = text("measurement") ?? "conversion";
10040
+ if (!GOAL_MEASUREMENTS.includes(measurement))
10041
+ invalid(`measurement must be one of ${GOAL_MEASUREMENTS.join(", ")}`);
10042
+ if (body5.count_once !== void 0) {
10043
+ const flag = String(body5.count_once).toLowerCase();
10044
+ if (flag !== "true" && flag !== "false") invalid("count-once must be true or false");
10045
+ body5.count_once = flag === "true";
10046
+ }
10047
+ if (type === "click" && !text("selector") && !text("text_match"))
10048
+ invalid("click goals need selector=<css> or text-match=<text>");
10049
+ if (type === "pageview" && !text("page_id") && !text("url_pattern"))
10050
+ invalid("pageview goals need page=<page-id> or url-pattern=<pattern>");
10051
+ if (type === "custom" && !text("event_name")) invalid("custom goals need event-name=<name>");
10052
+ const eventName = text("event_name");
10053
+ if (eventName && !GOAL_EVENT_NAME.test(eventName))
10054
+ invalid("event-name must be 1-120 letters, numbers, dots, colons, underscores, or hyphens");
10055
+ if ((type === "custom" || type === "revenue") && text("page_id"))
10056
+ invalid(`page is not supported for ${type} goals`);
10057
+ const pageId = text("page_id");
10058
+ if (pageId && !GOAL_UUID.test(pageId))
10059
+ invalid("page must be a page UUID (list them with `apex pages list`)");
10060
+ const valueProperty = text("value_property");
10061
+ const valueUnit = text("value_unit");
10062
+ if (measurement !== "value" && (valueProperty || valueUnit))
10063
+ invalid("value-property and value-unit need measurement=value");
10064
+ if (type === "revenue" && valueProperty)
10065
+ invalid("revenue goals read the order revenue; value-property is not supported");
10066
+ if (valueProperty && !GOAL_VALUE_PROPERTY.test(valueProperty))
10067
+ invalid("value-property must be 1-120 letters, numbers, dots, underscores, or hyphens");
10068
+ if (valueUnit && !GOAL_VALUE_UNITS.includes(valueUnit))
10069
+ invalid(`value-unit must be one of ${GOAL_VALUE_UNITS.join(", ")}`);
10070
+ }
10071
+ function parseNewGoal(command, value) {
10072
+ const body5 = {};
10073
+ for (const field of value.split(",")) {
10074
+ const separator = field.indexOf("=");
10075
+ if (separator <= 0 || separator === field.length - 1) {
10076
+ fail(
10077
+ command,
10078
+ `Invalid --goal "new:${value}"; use new:type=<type>,selector=<css>,name=<text>`,
10079
+ EXIT_CODES.validation
10080
+ );
9622
10081
  }
10082
+ body5[normalizeNewGoalKey(field.slice(0, separator))] = field.slice(separator + 1).trim();
9623
10083
  }
9624
- return null;
10084
+ validateNewGoal(command, value, body5);
10085
+ return body5;
9625
10086
  }
9626
- var create = new Command("create").description("Create a new experiment").requiredOption("--name <name>", "Experiment name").option("--slug <slug>", "URL-safe slug for GitOps").option("--description <desc>", "Description").option("--type <type>", "Experiment type: mutation or redirect", "mutation").option(
10087
+ async function resolveNewGoalPages(command, creds, goals, interactive) {
10088
+ const pending = goals.filter((goal) => goal.body.type === "click" && !goal.body.page_id);
10089
+ if (pending.length === 0) return;
10090
+ if (!interactive) {
10091
+ fail(
10092
+ command,
10093
+ `Invalid --goal "${pending[0].flagValue}"; click goals are page-scoped in v1 \u2014 add page=<page-id> (see \`apex pages list\`)`,
10094
+ EXIT_CODES.validation
10095
+ );
10096
+ }
10097
+ const listed = await requestTargetingResource(creds, "GET", "pages", void 0, void 0, 100);
10098
+ if (!listed.ok || !listed.data)
10099
+ fail(command, listed.error ?? "Failed to list pages", classifyStatus(listed.status));
10100
+ const rows = listed.data.data;
10101
+ if (rows.length === 0)
10102
+ fail(
10103
+ command,
10104
+ "Click goals are page-scoped in v1 and this shop has no pages yet; create one with `apex pages create`.",
10105
+ EXIT_CODES.validation
10106
+ );
10107
+ printChoices("Pages (enter id/number)", rows);
10108
+ for (const goal of pending) {
10109
+ const answer = await askCreate(`Page for click goal "${String(goal.body.name)}": `);
10110
+ if (!answer)
10111
+ fail(
10112
+ command,
10113
+ `Invalid --goal "${goal.flagValue}"; click goals need a page`,
10114
+ EXIT_CODES.validation
10115
+ );
10116
+ const chosen = /^\d+$/.test(answer) && rows[Number(answer) - 1] ? rows[Number(answer) - 1].id : answer;
10117
+ if (!GOAL_UUID.test(chosen))
10118
+ fail(
10119
+ command,
10120
+ `Invalid page "${chosen}"; pick a listed number or a page UUID`,
10121
+ EXIT_CODES.validation
10122
+ );
10123
+ goal.body.page_id = chosen;
10124
+ }
10125
+ }
10126
+ async function askCreate(question) {
10127
+ const readline3 = await import("node:readline");
10128
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
10129
+ return new Promise(
10130
+ (resolve2) => rl.question(question, (answer) => {
10131
+ rl.close();
10132
+ resolve2(answer.trim());
10133
+ })
10134
+ );
10135
+ }
10136
+ function printChoices(kind5, rows) {
10137
+ process.stderr.write(
10138
+ `${kind5}:
10139
+ ${rows.map((row, index) => ` ${index + 1}. ${row.name} (${row.id})`).join("\n")}
10140
+ `
10141
+ );
10142
+ }
10143
+ var create = new Command("create").description("Create a new experiment").addOption(
10144
+ new Option("--counting-type <type>", "Experiment classification").choices(countingTypes).default("standard")
10145
+ ).option("--name <name>", "Experiment name").option("--slug <slug>", "URL-safe slug for GitOps").option("--description <desc>", "Description").option("--type <type>", "Experiment type: mutation or redirect", "mutation").option(
9627
10146
  "--source-url <pattern>",
9628
10147
  "Positive source URL rule (repeatable; accepts exact URLs, paths, or wildcards)",
9629
10148
  collectCreateOption,
9630
10149
  []
10150
+ ).option(
10151
+ "--page-url <pattern>",
10152
+ "Page URL targeting pattern (repeatable)",
10153
+ collectCreateOption,
10154
+ []
10155
+ ).option(
10156
+ "--audience <segment-id|new:@file.json>",
10157
+ "Audience segment (repeatable)",
10158
+ collectCreateOption,
10159
+ []
10160
+ ).option(
10161
+ "--goal <goal-id|new:fields>",
10162
+ "Goal id or new:type=...,selector=...,name=... (repeatable)",
10163
+ collectCreateOption,
10164
+ []
9631
10165
  ).option(
9632
10166
  "--destination <url>",
9633
10167
  "Redirect treatment destination (repeatable; creates one treatment per URL)",
9634
10168
  collectCreateOption,
9635
10169
  []
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) {
10170
+ ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--idea <id>", "Idea UUID or Apex backlog key").option("--backlog-item <id>", "Alias for --idea").option("--preview", "Print a QA preview URL for the first treatment").option("--yes", "Do not prompt for missing values").action(async function action9(opts) {
10171
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !opts.yes);
10172
+ if (!opts.name && interactive) opts.name = await askCreate("Experiment name: ");
10173
+ if (!opts.name?.trim()) fail(this, "Missing required value: --name", EXIT_CODES.validation);
10174
+ if (opts.pageUrl.length === 0 && opts.sourceUrl.length === 0 && interactive) {
10175
+ const answer = await askCreate("Page URL (--page-url): ");
10176
+ if (answer) opts.pageUrl.push(answer);
10177
+ }
10178
+ const creds = requireCredentials();
10179
+ if (opts.audience.length === 0 && interactive) {
10180
+ const listed = await requestTargetingResource(
10181
+ creds,
10182
+ "GET",
10183
+ "segments",
10184
+ void 0,
10185
+ void 0,
10186
+ 100
10187
+ );
10188
+ if (!listed.ok || !listed.data)
10189
+ fail(this, listed.error ?? "Failed to list segments", classifyStatus(listed.status));
10190
+ const rows = listed.data.data;
10191
+ printChoices("Audiences (enter id/number, or none)", rows);
10192
+ const answer = await askCreate("Audience: ");
10193
+ if (answer && answer.toLowerCase() !== "none")
10194
+ opts.audience.push(
10195
+ /^\d+$/.test(answer) && rows[Number(answer) - 1] ? rows[Number(answer) - 1].id : answer
10196
+ );
10197
+ }
10198
+ if (opts.goal.length === 0 && interactive) {
10199
+ const listed = await listGoals(creds, { limit: 100 });
10200
+ if (!listed.ok || !listed.data)
10201
+ fail(this, listed.error ?? "Failed to list goals", classifyStatus(listed.status));
10202
+ const rows = listed.data.data;
10203
+ printChoices("Goals (enter ids/numbers separated by commas, or none)", rows);
10204
+ const answer = await askCreate("Goals: ");
10205
+ if (answer && answer.toLowerCase() !== "none")
10206
+ opts.goal.push(
10207
+ ...answer.split(",").map((part) => part.trim()).filter(Boolean).map(
10208
+ (part) => /^\d+$/.test(part) && rows[Number(part) - 1] ? rows[Number(part) - 1].id : part
10209
+ )
10210
+ );
10211
+ }
10212
+ if (this.getOptionValueSource("traffic") === "default" && interactive) {
10213
+ const answer = await askCreate("Traffic percentage [100]: ");
10214
+ if (answer) opts.traffic = answer;
10215
+ }
9637
10216
  if (opts.type !== "mutation" && opts.type !== "redirect") {
9638
10217
  fail(this, "--type must be mutation or redirect", EXIT_CODES.validation);
9639
10218
  }
@@ -9641,9 +10220,9 @@ var create = new Command("create").description("Create a new experiment").requir
9641
10220
  if (!Number.isFinite(traffic) || traffic < 0 || traffic > 100) {
9642
10221
  fail(this, "--traffic must be a number from 0 to 100", EXIT_CODES.validation);
9643
10222
  }
9644
- const sourceRules = opts.sourceUrl.map((value) => value.trim());
10223
+ const sourceRules = [...opts.sourceUrl, ...opts.pageUrl].map((value) => value.trim());
9645
10224
  if (sourceRules.some((value) => value.length === 0)) {
9646
- fail(this, "--source-url cannot be empty", EXIT_CODES.validation);
10225
+ fail(this, "--source-url/--page-url cannot be empty", EXIT_CODES.validation);
9647
10226
  }
9648
10227
  if (opts.type !== "redirect" && opts.destination.length > 0) {
9649
10228
  fail(this, "--destination is only valid with --type redirect", EXIT_CODES.validation);
@@ -9663,94 +10242,180 @@ var create = new Command("create").description("Create a new experiment").requir
9663
10242
  );
9664
10243
  }
9665
10244
  const destinations = opts.destination.map(parseHttpDestination);
10245
+ const previewStoreUrl = deriveStoreUrl(sourceRules);
10246
+ if (opts.preview && !previewStoreUrl) {
10247
+ fail(
10248
+ this,
10249
+ "--preview needs an absolute page URL: pass --page-url https://store.example/path (wildcard and path-only rules cannot seed the preview link), or create the experiment without --preview and run `apex preview <id> --store-url <url>`",
10250
+ EXIT_CODES.validation
10251
+ );
10252
+ }
9666
10253
  const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
9667
- const creds = requireCredentials();
9668
10254
  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;
10255
+ let ideaKey;
10256
+ if (opts.idea !== void 0 && opts.backlogItem !== void 0 && opts.idea.trim().toUpperCase() !== opts.backlogItem.trim().toUpperCase()) {
10257
+ fail(
10258
+ this,
10259
+ "--idea and --backlog-item name the same idea; pass only one",
10260
+ EXIT_CODES.validation
10261
+ );
10262
+ }
10263
+ const ideaReference = opts.idea ?? opts.backlogItem;
10264
+ if (ideaReference !== void 0) {
10265
+ const classified = classifyIdeaReference(ideaReference);
10266
+ if (classified.kind === "uuid") {
10267
+ backlogItemId = classified.value;
10268
+ } else if (classified.kind === "key") {
10269
+ ideaKey = classified.value;
9711
10270
  } else {
9712
- fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
10271
+ fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
9713
10272
  }
9714
10273
  }
10274
+ const newGoals = /* @__PURE__ */ new Map();
10275
+ for (const [index, goal] of opts.goal.entries()) {
10276
+ if (goal.startsWith("new:")) newGoals.set(index, parseNewGoal(this, goal.slice(4)));
10277
+ }
10278
+ await resolveNewGoalPages(
10279
+ this,
10280
+ creds,
10281
+ [...newGoals].map(([index, body5]) => ({ flagValue: opts.goal[index], body: body5 })),
10282
+ interactive
10283
+ );
10284
+ const createdSegments = [];
10285
+ const createdGoals = [];
10286
+ const segmentIds = [];
10287
+ const goalIds = [];
10288
+ const partial = () => [
10289
+ ...createdSegments.map((item) => `segment ${item.id}`),
10290
+ ...createdGoals.map((item) => `goal ${item.id}`)
10291
+ ];
10292
+ for (const audience of opts.audience) {
10293
+ if (!audience.startsWith("new:")) {
10294
+ segmentIds.push(audience);
10295
+ continue;
10296
+ }
10297
+ const reference = audience.slice(4);
10298
+ if (!reference.startsWith("@"))
10299
+ fail(
10300
+ this,
10301
+ `Invalid --audience "${audience}"; new audiences use new:@file.json`,
10302
+ EXIT_CODES.validation
10303
+ );
10304
+ const response = await requestTargetingResource(
10305
+ creds,
10306
+ "POST",
10307
+ "segments",
10308
+ void 0,
10309
+ readCreateJson(this, reference)
10310
+ );
10311
+ if (!response.ok || !response.data)
10312
+ fail(
10313
+ this,
10314
+ `${response.error ?? "Failed to create audience"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
10315
+ classifyStatus(response.status)
10316
+ );
10317
+ const segment = response.data;
10318
+ createdSegments.push(segment);
10319
+ segmentIds.push(segment.id);
10320
+ }
10321
+ for (const [index, goal] of opts.goal.entries()) {
10322
+ const body5 = newGoals.get(index);
10323
+ if (!body5) {
10324
+ goalIds.push(goal);
10325
+ continue;
10326
+ }
10327
+ const response = await createGoal(creds, body5);
10328
+ if (!response.ok || !response.data)
10329
+ fail(
10330
+ this,
10331
+ `${response.error ?? "Failed to create goal"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
10332
+ classifyStatus(response.status)
10333
+ );
10334
+ createdGoals.push(response.data);
10335
+ goalIds.push(response.data.id);
10336
+ }
9715
10337
  const spinner = await createSpinner("Creating experiment...");
9716
- const res = await createExperiment(creds, {
9717
- name: opts.name,
9718
- slug: opts.slug ?? null,
9719
- description: opts.description ?? null,
9720
- delivery_type: opts.type,
9721
- traffic_allocation: traffic / 100,
9722
- ...backlogItemId ? { backlog_item_id: backlogItemId } : {},
9723
- ...sourceRules.length > 0 ? {
9724
- store_url: sourceStoreUrl(sourceRules),
9725
- targeting: {
9726
- url: sourceRules.map((pattern) => ({
9727
- type: "simple",
9728
- pattern,
9729
- include: true
9730
- }))
9731
- }
9732
- } : {},
9733
- ...armWeight !== null ? {
9734
- variations: [
9735
- {
9736
- name: "Control",
9737
- weight: armWeight,
9738
- is_control: true
9739
- },
9740
- ...destinations.map((redirectUrl, index) => ({
9741
- name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
9742
- weight: armWeight,
9743
- is_control: false,
9744
- redirect_url: redirectUrl
9745
- }))
9746
- ]
9747
- } : {}
9748
- });
10338
+ let res;
10339
+ if (segmentIds.length === 0 && goalIds.length === 0) {
10340
+ res = await createExperiment(creds, {
10341
+ name: opts.name,
10342
+ slug: opts.slug ?? null,
10343
+ description: opts.description ?? null,
10344
+ delivery_type: opts.type,
10345
+ counting_type: opts.countingType,
10346
+ traffic_allocation: traffic / 100,
10347
+ ...backlogItemId ? { backlog_item_id: backlogItemId } : {},
10348
+ ...ideaKey ? { idea_key: ideaKey } : {},
10349
+ ...sourceRules.length > 0 ? {
10350
+ store_url: previewStoreUrl,
10351
+ targeting: {
10352
+ url: sourceRules.map((pattern) => ({
10353
+ type: "simple",
10354
+ pattern,
10355
+ include: true
10356
+ }))
10357
+ }
10358
+ } : {},
10359
+ ...armWeight !== null ? {
10360
+ variations: [
10361
+ {
10362
+ name: "Control",
10363
+ weight: armWeight,
10364
+ is_control: true
10365
+ },
10366
+ ...destinations.map((redirectUrl, index) => ({
10367
+ name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
10368
+ weight: armWeight,
10369
+ is_control: false,
10370
+ redirect_url: redirectUrl
10371
+ }))
10372
+ ]
10373
+ } : {}
10374
+ });
10375
+ } else {
10376
+ res = await createExperiment(creds, {
10377
+ name: opts.name,
10378
+ slug: opts.slug ?? null,
10379
+ description: opts.description ?? null,
10380
+ delivery_type: opts.type,
10381
+ counting_type: opts.countingType,
10382
+ traffic_allocation: traffic / 100,
10383
+ ...backlogItemId ? { backlog_item_id: backlogItemId } : {},
10384
+ ...ideaKey ? { idea_key: ideaKey } : {},
10385
+ ...sourceRules.length > 0 ? {
10386
+ store_url: previewStoreUrl,
10387
+ targeting: {
10388
+ url: sourceRules.map((pattern) => ({
10389
+ type: "simple",
10390
+ pattern,
10391
+ include: true
10392
+ })),
10393
+ ...segmentIds.length ? { segments: segmentIds.map((id) => ({ id, include: true })) } : {}
10394
+ }
10395
+ } : segmentIds.length ? { targeting: { segments: segmentIds.map((id) => ({ id, include: true })) } } : {},
10396
+ ...goalIds.length ? { goal_ids: goalIds, primary_goal_id: goalIds[0] } : {},
10397
+ ...armWeight !== null ? {
10398
+ variations: [
10399
+ {
10400
+ name: "Control",
10401
+ weight: armWeight,
10402
+ is_control: true
10403
+ },
10404
+ ...destinations.map((redirectUrl, index) => ({
10405
+ name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
10406
+ weight: armWeight,
10407
+ is_control: false,
10408
+ redirect_url: redirectUrl
10409
+ }))
10410
+ ]
10411
+ } : {}
10412
+ });
10413
+ }
9749
10414
  if (!res.ok || !res.data) {
9750
10415
  spinner?.fail("Failed to create experiment");
9751
10416
  fail(
9752
10417
  this,
9753
- res.error ?? "Failed to create experiment",
10418
+ `${res.error ?? "Failed to create experiment"}${partial().length ? `; already created: ${partial().join(", ")}` : ""}`,
9754
10419
  classifyStatus(res.status),
9755
10420
  res.errorDetails
9756
10421
  );
@@ -9759,13 +10424,49 @@ var create = new Command("create").description("Create a new experiment").requir
9759
10424
  spinner?.succeed(
9760
10425
  outcome === "created" ? "Experiment created" : `Already linked \u2014 returning existing experiment ${res.data.test_key ?? res.data.slug ?? res.data.id}`
9761
10426
  );
10427
+ let previewUrl = null;
10428
+ let previewNote = null;
10429
+ if (opts.preview) {
10430
+ const variation = res.data.variations?.find((item) => !item.is_control);
10431
+ const storeUrl = res.data.store_url ?? previewStoreUrl;
10432
+ if (storeUrl && variation) {
10433
+ previewUrl = buildQaPreviewUrl2(storeUrl, res.data.id, variation.id).toString();
10434
+ } else {
10435
+ previewNote = `No preview link: the experiment has no ${variation ? "store URL" : "treatment variation"} yet. Add one, then run \`apex preview ${res.data.id}\`.`;
10436
+ }
10437
+ }
9762
10438
  output(
9763
10439
  this,
9764
- { outcome, backlog_item_id: backlogItemId ?? null, experiment: res.data },
9765
- () => printExperiment(res.data)
10440
+ {
10441
+ outcome,
10442
+ backlog_item_id: backlogItemId ?? null,
10443
+ idea_key: ideaKey ?? null,
10444
+ experiment: res.data,
10445
+ segments: createdSegments,
10446
+ goals: createdGoals,
10447
+ dashboard_url: buildDashboardUrl(
10448
+ creds.baseUrl,
10449
+ res.data?.shop_id ?? creds.resolvedContext?.shopId,
10450
+ res.data.id
10451
+ ),
10452
+ preview_url: previewUrl,
10453
+ preview_note: previewNote
10454
+ },
10455
+ () => {
10456
+ printExperiment(
10457
+ res.data,
10458
+ buildDashboardUrl(
10459
+ creds.baseUrl,
10460
+ res.data?.shop_id ?? creds.resolvedContext?.shopId,
10461
+ res.data.id
10462
+ )
10463
+ );
10464
+ if (previewUrl) console.log(`Preview: ${previewUrl}`);
10465
+ if (previewNote) console.log(previewNote);
10466
+ }
9766
10467
  );
9767
10468
  });
9768
- var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
10469
+ var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action10(idOrSlug) {
9769
10470
  const creds = requireCredentials();
9770
10471
  const spinner = await createSpinner("Pausing experiment...");
9771
10472
  const exp = await resolveExperiment(idOrSlug);
@@ -9787,7 +10488,7 @@ var pause = new Command("pause").description("Pause a running experiment").argum
9787
10488
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
9788
10489
  });
9789
10490
  });
9790
- var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
10491
+ var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).action(async function action11(idOrSlug) {
9791
10492
  const creds = requireCredentials();
9792
10493
  const spinner = await createSpinner("Completing experiment...");
9793
10494
  const exp = await resolveExperiment(idOrSlug);
@@ -9809,7 +10510,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
9809
10510
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
9810
10511
  });
9811
10512
  });
9812
- var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", "Experiment ID or slug").option("-f, --force", "Skip confirmation").action(async function action9(idOrSlug, opts) {
10513
+ var del = new Command("delete").alias("rm").description("Delete an experiment").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("-f, --force", "Skip confirmation").action(async function action12(idOrSlug, opts) {
9813
10514
  const creds = requireCredentials();
9814
10515
  const exp = await resolveExperiment(idOrSlug);
9815
10516
  if (!exp) {
@@ -9854,7 +10555,7 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9854
10555
  () => spinner.succeed(`Experiment "${exp.name}" deleted.`)
9855
10556
  );
9856
10557
  });
9857
- var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", "Experiment ID or slug").option("--out <path>", "Target folder").action(async function action10(idOrSlug, opts) {
10558
+ var pullFilesystem = new Command("pull").description("Pull an experiment as an editable filesystem").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--out <path>", "Target folder").action(async function action13(idOrSlug, opts) {
9858
10559
  const creds = requireCredentials();
9859
10560
  const spinner = await createSpinner("Pulling experiment filesystem...");
9860
10561
  try {
@@ -9895,12 +10596,20 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9895
10596
  throw error;
9896
10597
  }
9897
10598
  });
9898
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action11(folderArg) {
10599
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action14(folderArg) {
9899
10600
  const creds = requireCredentials();
9900
10601
  try {
9901
10602
  const folder = folderPath(folderArg);
9902
10603
  const session = readFilesystemSession(folder);
9903
- const files = listExperimentFilesystemFiles(folder, session);
10604
+ const files = await rewriteExperimentFilesystemAssets({
10605
+ files: listExperimentFilesystemFiles(folder, session),
10606
+ folder,
10607
+ baseUrl: creds.baseUrl,
10608
+ enabled: true,
10609
+ upload: null,
10610
+ assets: session.assets ?? {},
10611
+ credentials: creds
10612
+ });
9904
10613
  const res = await planExperimentFilesystem(creds, session.experimentId, { files });
9905
10614
  if (!res.ok || !res.data) {
9906
10615
  fail(
@@ -9920,7 +10629,12 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9920
10629
  await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId);
9921
10630
  output(
9922
10631
  this,
9923
- { folder, experimentId: session.experimentId, plan: res.data },
10632
+ {
10633
+ folder,
10634
+ experimentId: session.experimentId,
10635
+ plan: res.data,
10636
+ requiredAcknowledgment: res.data.invalidatesQa ? "accept-qa-invalidation" : null
10637
+ },
9924
10638
  () => printFilesystemPlan(res.data)
9925
10639
  );
9926
10640
  } catch (error) {
@@ -9928,19 +10642,27 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9928
10642
  fail(this, error instanceof Error ? error.message : String(error));
9929
10643
  }
9930
10644
  });
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) {
10645
+ var applyFilesystem = new Command("apply").option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("--accept-qa-invalidation", "Acknowledge that applying changes invalidates QA approval").addOption(new Option("-f, --force", "Deprecated alias for --accept-qa-invalidation").hideHelp()).option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action15(folderArg, opts) {
9932
10646
  const creds = requireCredentials();
9933
10647
  try {
9934
10648
  const folder = folderPath(folderArg);
9935
10649
  const session = readFilesystemSession(folder);
10650
+ if (opts.force)
10651
+ process.stderr.write("--force is deprecated; use --accept-qa-invalidation instead.\n");
10652
+ const assets = session.assets ?? {};
9936
10653
  const files = await rewriteExperimentFilesystemAssets({
9937
10654
  files: listExperimentFilesystemFiles(folder, session),
9938
10655
  folder,
9939
10656
  baseUrl: creds.baseUrl,
9940
10657
  enabled: opts.assetUpload !== false,
9941
10658
  upload: uploadAssets,
10659
+ assets,
9942
10660
  credentials: creds
9943
10661
  });
10662
+ if (Object.keys(assets).length > 0) {
10663
+ session.assets = assets;
10664
+ writeFilesystemSession(folder, session);
10665
+ }
9944
10666
  const planRes = await planExperimentFilesystem(creds, session.experimentId, { files });
9945
10667
  if (!planRes.ok || !planRes.data) {
9946
10668
  fail(
@@ -9960,30 +10682,43 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9960
10682
  }
9961
10683
  fail(this, blockingIssueSummary(planRes.data.blockingIssues), EXIT_CODES.validation, {
9962
10684
  errors: planRes.data.errors,
9963
- plan: planRes.data
10685
+ plan: planRes.data,
10686
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null
9964
10687
  });
9965
10688
  }
9966
10689
  if (planRes.data.changes.length === 0) {
10690
+ const synced2 = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
10691
+ if (!synced2)
10692
+ await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId, true);
10693
+ const { dashboardUrl: dashboardUrl2 } = await getQaPreviewUrls(creds, session.experimentId);
9967
10694
  output(
9968
10695
  this,
9969
10696
  {
9970
10697
  folder,
9971
10698
  experimentId: session.experimentId,
9972
10699
  applied: false,
10700
+ synced: synced2,
10701
+ dashboardUrl: dashboardUrl2,
9973
10702
  plan: planRes.data,
10703
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
9974
10704
  message: "No changes."
9975
10705
  },
9976
- () => printFilesystemPlan(planRes.data)
10706
+ () => {
10707
+ printFilesystemPlan(planRes.data);
10708
+ console.log(
10709
+ synced2 ? "Local files were synced." : opts.sync ? "Local file sync failed; retry --sync or apex experiments pull." : "Local authored files were left unchanged; use --sync or apex experiments pull to refresh."
10710
+ );
10711
+ }
9977
10712
  );
9978
10713
  return;
9979
10714
  }
9980
- if (planRes.data.invalidatesQa && !opts.force) {
10715
+ if (planRes.data.invalidatesQa && !opts.force && !opts.acceptQaInvalidation) {
9981
10716
  const ok = await confirmApply("This apply invalidates QA approval. Continue?");
9982
10717
  if (ok === null) {
9983
10718
  confirmationRequired(
9984
10719
  this,
9985
10720
  "Applying these changes invalidates QA approval and requires confirmation.",
9986
- `apex experiments apply ${folderArg} --force`
10721
+ `apex experiments apply ${folderArg} --accept-qa-invalidation`
9987
10722
  );
9988
10723
  }
9989
10724
  if (!ok) {
@@ -10001,9 +10736,29 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10001
10736
  });
10002
10737
  }
10003
10738
  spinner?.succeed("Applied experiment filesystem");
10004
- await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId);
10739
+ const synced = opts.sync ? await refreshExperimentFilesystemFolderIfNeeded(creds, folder, session.experimentId) : false;
10740
+ if (!synced) {
10741
+ await refreshServerOwnedFilesIfNeeded(
10742
+ creds,
10743
+ folder,
10744
+ session.experimentId,
10745
+ !res.data?.receipt?.files
10746
+ );
10747
+ if (res.data?.receipt?.files) {
10748
+ writeFilesystemSession(folder, {
10749
+ ...session,
10750
+ files: {
10751
+ ...session.files,
10752
+ ...Object.fromEntries(
10753
+ res.data.receipt.files.map((file) => [file.path, { sha256: file.sha256 }])
10754
+ )
10755
+ }
10756
+ });
10757
+ }
10758
+ }
10759
+ const syncMessage = synced ? "Local files were synced." : opts.sync ? "Local file sync failed; retry --sync or apex experiments pull." : "Local authored files were left unchanged; use --sync or apex experiments pull to refresh." + (res.data?.receipt?.files ? "" : " No receipt file digests; authored-file manifest entries left unchanged.");
10005
10760
  const previewResult = await getQaPreviewUrls(creds, session.experimentId);
10006
- const { previewUrls } = previewResult;
10761
+ const { previewUrls, dashboardUrl } = previewResult;
10007
10762
  output(
10008
10763
  this,
10009
10764
  {
@@ -10011,12 +10766,37 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10011
10766
  experimentId: session.experimentId,
10012
10767
  applied: true,
10013
10768
  plan: planRes.data,
10769
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
10014
10770
  result: res.data,
10771
+ synced,
10772
+ dashboardUrl,
10773
+ receipt: res.data?.receipt ?? null,
10015
10774
  previewUrls
10016
10775
  },
10017
10776
  () => {
10018
10777
  printFilesystemPlan(planRes.data);
10778
+ const receipt = res.data?.receipt;
10779
+ if (receipt) {
10780
+ console.log("\nReceipt");
10781
+ if (dashboardUrl) console.log(` Dashboard: ${dashboardUrl}`);
10782
+ console.log(` Status: ${receipt.status}`);
10783
+ console.log(` Classification: ${receipt.delivery_type} / ${receipt.counting_type}`);
10784
+ console.log(` Traffic: ${(receipt.traffic_allocation * 100).toFixed(0)}%`);
10785
+ console.log(
10786
+ ` Schedule: ${receipt.schedule.scheduled_start_at ?? "none"} \u2192 ${receipt.schedule.scheduled_end_at ?? "none"}`
10787
+ );
10788
+ console.log(` Goals: ${receipt.goal_count}`);
10789
+ console.log(
10790
+ ` QA: ${receipt.qa ? `${receipt.qa.status} (${receipt.qa.run_id}), approved: ${receipt.qa.approved_at ?? "none"}` : "none"}`
10791
+ );
10792
+ for (const variation of receipt.variations) {
10793
+ console.log(
10794
+ ` ${variation.name} (${variation.slug}): ${Object.entries(variation.digests).map(([key, value]) => `${key}=${value ?? "null"}`).join(" ")}`
10795
+ );
10796
+ }
10797
+ }
10019
10798
  printQaPreviewUrls(session.experimentId, previewResult);
10799
+ console.log(syncMessage);
10020
10800
  }
10021
10801
  );
10022
10802
  } catch (error) {
@@ -10024,7 +10804,11 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10024
10804
  fail(this, error instanceof Error ? error.message : String(error));
10025
10805
  }
10026
10806
  });
10027
- var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--json", "Print the QA run in the standard JSON envelope").action(async function action13(target) {
10807
+ var qaStatus = new Command("status").description("Show or create the latest experiment QA run").argument(
10808
+ "[folder-or-id]",
10809
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
10810
+ "."
10811
+ ).option("--json", "Print the QA run in the standard JSON envelope").action(async function action16(target) {
10028
10812
  const creds = requireCredentials();
10029
10813
  const spinner = await createSpinner("Fetching QA run...");
10030
10814
  try {
@@ -10048,15 +10832,17 @@ var qaStatus = new Command("status").description("Show or create the latest expe
10048
10832
  throw error;
10049
10833
  }
10050
10834
  });
10051
- var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").requiredOption("--item <id>", "Checklist item id").requiredOption("--status <status>", "pending|pass|warning|fail|accepted|skipped").option("--notes <text>", "Checklist notes").option("--evidence <json-or-path>", "Evidence JSON object or @path/to/evidence.json").option("--json", "Print raw QA run JSON").action(async function action14(target, opts) {
10835
+ var qaCheck = new Command("check").description("Update one QA checklist item with status, notes, and evidence").argument(
10836
+ "[folder-or-id]",
10837
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
10838
+ "."
10839
+ ).requiredOption("--item <id>", "Checklist item id").addOption(
10840
+ qaChoice("--status <status>", "Checklist status", checklistStatuses).makeOptionMandatory()
10841
+ ).option("--notes <text>", "Checklist notes").option("--evidence <json-or-path>", "Evidence JSON object or @path/to/evidence.json").option("--summary", "Print a compact run and changed values").option("--json", "Print raw QA run JSON").action(async function action17(target, opts) {
10842
+ validateQaChoice(this, "--status", opts.status, checklistStatuses);
10052
10843
  const creds = requireCredentials();
10053
10844
  const spinner = await createSpinner("Updating QA checklist...");
10054
10845
  try {
10055
- if (!isQaCheckStatus(opts.status)) {
10056
- throw new Error(
10057
- "Invalid QA status. Use pending, pass, warning, fail, accepted, or skipped."
10058
- );
10059
- }
10060
10846
  const evidence = parseEvidenceInput(opts.evidence);
10061
10847
  const resolved = await resolveExperimentForFilesystemOrId(target);
10062
10848
  const current = await getExperimentQaRun(creds, resolved.experimentId);
@@ -10082,16 +10868,23 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
10082
10868
  resolved.folder,
10083
10869
  resolved.experimentId
10084
10870
  );
10085
- output(this, { run: run3 }, () => {
10871
+ const data = opts.summary ? qaSummary(run3, run3.checklist.find((item) => item.id === opts.item) ?? null) : { run: run3 };
10872
+ output(this, data, () => {
10086
10873
  spinner.succeed("Updated QA checklist");
10087
- printQaRun(run3);
10874
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
10875
+ `);
10876
+ else printQaRun(run3);
10088
10877
  });
10089
10878
  } catch (error) {
10090
10879
  spinner.fail("Failed to update QA checklist");
10091
10880
  throw error;
10092
10881
  }
10093
10882
  });
10094
- var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").option("--notes <text>", "QA run notes").option("--acceptance <text>", "Acceptance criteria summary").option("--json", "Print raw QA run JSON").action(async function action15(target, opts) {
10883
+ var qaNotes = new Command("notes").description("Update QA run notes or acceptance criteria summary").argument(
10884
+ "[folder-or-id]",
10885
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
10886
+ "."
10887
+ ).option("--notes <text>", "QA run notes").option("--acceptance <text>", "Acceptance criteria summary").option("--summary", "Print a compact run and changed values").option("--json", "Print raw QA run JSON").action(async function action18(target, opts) {
10095
10888
  const creds = requireCredentials();
10096
10889
  const spinner = await createSpinner("Updating QA notes...");
10097
10890
  try {
@@ -10103,9 +10896,10 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10103
10896
  if (!current.ok || !current.data) {
10104
10897
  throw new Error(current.error ?? "Failed to fetch QA run");
10105
10898
  }
10106
- const body5 = opts.notes !== void 0 ? { runId: current.data.run.id, runNotes: opts.notes } : {
10899
+ const body5 = {
10107
10900
  runId: current.data.run.id,
10108
- acceptanceCriteriaSummary: opts.acceptance ?? null
10901
+ ...opts.notes !== void 0 ? { runNotes: opts.notes } : {},
10902
+ ...opts.acceptance !== void 0 ? { acceptanceCriteriaSummary: opts.acceptance } : {}
10109
10903
  };
10110
10904
  const res = await updateExperimentQaRun(creds, resolved.experimentId, body5);
10111
10905
  if (!res.ok || !res.data)
@@ -10120,20 +10914,32 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10120
10914
  resolved.folder,
10121
10915
  resolved.experimentId
10122
10916
  );
10123
- output(this, { run: run3 }, () => {
10917
+ const data = opts.summary ? qaSummary(run3, {
10918
+ notes: run3.notes,
10919
+ acceptanceCriteriaSummary: run3.acceptanceCriteriaSummary
10920
+ }) : { run: run3 };
10921
+ output(this, data, () => {
10124
10922
  spinner.succeed("Updated QA notes");
10125
- printQaRun(run3);
10923
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
10924
+ `);
10925
+ else printQaRun(run3);
10126
10926
  });
10127
10927
  } catch (error) {
10128
10928
  spinner.fail("Failed to update QA notes");
10129
10929
  throw error;
10130
10930
  }
10131
10931
  });
10132
- var qaApprove = new Command("approve").description("Mark QA done after the checklist gates are ready").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action16(target) {
10932
+ var qaApprove = new Command("approve").description("Approve QA and move the experiment to ready_to_launch; qa reopen returns it to QA").argument(
10933
+ "[folder-or-id]",
10934
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
10935
+ "."
10936
+ ).action(async function action19(target) {
10133
10937
  const creds = requireCredentials();
10134
10938
  const spinner = await createSpinner("Approving QA...");
10135
10939
  try {
10136
10940
  const resolved = await resolveExperimentForFilesystemOrId(target);
10941
+ const before = await resolveExperiment(resolved.experimentId);
10942
+ if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
10137
10943
  const res = await approveExperimentQa(creds, resolved.experimentId);
10138
10944
  if (!res.ok)
10139
10945
  throw new CliError(
@@ -10146,17 +10952,35 @@ var qaApprove = new Command("approve").description("Mark QA done after the check
10146
10952
  resolved.folder,
10147
10953
  resolved.experimentId
10148
10954
  );
10955
+ if (!res.data?.status) fail(this, "QA approval response is missing experiment status.");
10956
+ const transition = {
10957
+ from: before.status,
10958
+ to: res.data.status,
10959
+ reopen: `apex experiments qa reopen ${resolved.experimentId}`
10960
+ };
10149
10961
  output(
10150
10962
  this,
10151
- { experimentId: resolved.experimentId, qaStatus: "done" },
10152
- () => spinner.succeed(`QA marked done for ${resolved.label}`)
10963
+ {
10964
+ experimentId: resolved.experimentId,
10965
+ qaStatus: "done",
10966
+ experiment_status_before: before.status,
10967
+ experiment_status_after: res.data.status,
10968
+ transition
10969
+ },
10970
+ () => spinner.succeed(
10971
+ `QA approved: ${transition.from} \u2192 ${transition.to}; reopen: ${transition.reopen}`
10972
+ )
10153
10973
  );
10154
10974
  } catch (error) {
10155
10975
  spinner.fail("Failed to approve QA");
10156
10976
  throw error;
10157
10977
  }
10158
10978
  });
10159
- var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action17(target) {
10979
+ var qaReopen = new Command("reopen").description("Reopen QA and clear scheduled launch state").argument(
10980
+ "[folder-or-id]",
10981
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
10982
+ "."
10983
+ ).action(async function action20(target) {
10160
10984
  const creds = requireCredentials();
10161
10985
  const spinner = await createSpinner("Reopening QA...");
10162
10986
  try {
@@ -10214,7 +11038,13 @@ function resolveEvidenceVariation(experiment, ref) {
10214
11038
  }
10215
11039
  throw new Error(`Unknown --variation value: ${ref}`);
10216
11040
  }
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) {
11041
+ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or upload typed QA evidence").argument(
11042
+ "[folder-or-id]",
11043
+ "Experiment filesystem folder, ID, slug, or test key (for example BIO-085)",
11044
+ "."
11045
+ ).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").addOption(qaChoice("--status <status>", "Evidence status", evidenceStatuses)).addOption(qaChoice("--kind <kind>", "Evidence kind", evidenceKinds).default("screenshot")).option("--records <json-or-@path>", "Typed evidence record array").option("--summary", "Print a compact run and newly added evidence").option("--json", "Print the QA run in the standard JSON envelope").action(async function action21(target, opts) {
11046
+ validateQaChoice(this, "--status", opts.status, evidenceStatuses);
11047
+ validateQaChoice(this, "--kind", opts.kind, evidenceKinds);
10218
11048
  const modes = Number(Boolean(opts.job)) + Number(opts.file.length > 0) + Number(Boolean(opts.records));
10219
11049
  if (modes !== 1) {
10220
11050
  fail(this, "Provide exactly one of --job, --file, or --records.", EXIT_CODES.validation);
@@ -10223,6 +11053,17 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
10223
11053
  const spinner = await createSpinner("Writing QA evidence...");
10224
11054
  try {
10225
11055
  const resolved = await resolveExperimentForFilesystemOrId(target);
11056
+ const previousIds = /* @__PURE__ */ new Set();
11057
+ if (opts.summary) {
11058
+ const current = await getExperimentQaRun(creds, resolved.experimentId);
11059
+ if (!current.ok || !current.data)
11060
+ throw new CliError(
11061
+ current.error ?? "Failed to fetch QA run",
11062
+ classifyStatus(current.status),
11063
+ current.errorDetails
11064
+ );
11065
+ for (const record2 of current.data.run.evidenceRecords) previousIds.add(record2.id);
11066
+ }
10226
11067
  let response;
10227
11068
  if (opts.job) {
10228
11069
  response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
@@ -10263,7 +11104,16 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
10263
11104
  );
10264
11105
  }
10265
11106
  spinner.stop();
10266
- output(this, { run: response.data.run }, () => printQaRun(response.data.run));
11107
+ const run3 = response.data.run;
11108
+ const data = opts.summary ? qaSummary(
11109
+ run3,
11110
+ run3.evidenceRecords.filter((record2) => !previousIds.has(record2.id))
11111
+ ) : { run: run3 };
11112
+ output(this, data, () => {
11113
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
11114
+ `);
11115
+ else printQaRun(run3);
11116
+ });
10267
11117
  } catch (error) {
10268
11118
  spinner.fail("Failed to write QA evidence");
10269
11119
  throw error;
@@ -10279,102 +11129,255 @@ var SCREENSHOT_DEVICES = [
10279
11129
  ];
10280
11130
  var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
10281
11131
  var commaList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
11132
+ function shellQuote(value) {
11133
+ if (value.length > 0 && /^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
11134
+ return `'${value.replaceAll("'", "'\\''")}'`;
11135
+ }
11136
+ function isTransientResumeStatus(status2) {
11137
+ return status2 === 429 || status2 !== void 0 && status2 >= 500;
11138
+ }
11139
+ function screenshotTimeoutMessage(job, experimentRef, out, seconds, flags = {}) {
11140
+ const progress = job.progress ? ` Last progress: ${job.progress.captured}/${job.progress.total} captured.` : "";
11141
+ const carried = [
11142
+ flags.attachQa === false ? " --no-attach-qa" : "",
11143
+ flags.labelPairs === false ? " --no-label-pairs" : "",
11144
+ flags.json ? " --json" : ""
11145
+ ].join("");
11146
+ const resume = `apex experiments screenshots ${shellQuote(experimentRef)} --job ${shellQuote(job.job_id)} --out ${shellQuote(out)}${carried}`;
11147
+ return `Screenshot job ${job.job_id} timed out after ${seconds}s.${progress} Resume with: ${resume}`;
11148
+ }
10282
11149
  var screenshots = new Command("screenshots").description(
10283
11150
  "Force-assign every capture, including control, to its variation server-side for deterministic screenshots"
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(
11151
+ ).argument("<experimentRef>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--url <target>", "Target URL override (not supported by the current v1 API)").option("--devices <list>", `Comma-separated devices (default: ${SCREENSHOT_DEVICES.join(",")})`).option(
10285
11152
  "--variations <list>",
10286
11153
  "Comma-separated treatment IDs, slugs, or names; control is always included and force-assigned"
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(
11154
+ ).option("--out <dir>", "Download directory", ".").option("--job <id>", "Resume an existing screenshot job instead of creating one").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("--no-label-pairs", "Keep legacy screenshot filenames without before/after labels").option(
10288
11155
  "--timeout <seconds>",
10289
11156
  "Maximum wait for completion; finished results are always downloaded",
10290
11157
  "300"
10291
- ).action(async function action19(experimentRef, opts) {
11158
+ ).action(async function action22(experimentRef, opts) {
10292
11159
  if (opts.url)
10293
11160
  fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
10294
11161
  const timeoutSeconds = Number(opts.timeout);
11162
+ const explicitJson = process.argv.includes("--json");
10295
11163
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
10296
11164
  fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
10297
11165
  const experiment = await resolveExperiment(experimentRef);
10298
11166
  if (!experiment?.variations?.length)
10299
11167
  fail(this, "Experiment has no variations to capture.", EXIT_CODES.validation);
10300
- const devices = opts.devices ? commaList(opts.devices) : SCREENSHOT_DEVICES;
10301
- if (!devices.length)
10302
- fail(this, "--devices must contain at least one device.", EXIT_CODES.validation);
10303
- const unknownDevices = devices.filter((device) => !SCREENSHOT_DEVICES.includes(device));
10304
- if (unknownDevices.length)
10305
- fail(
10306
- this,
10307
- `Unknown screenshot device(s): ${unknownDevices.join(", ")}`,
10308
- EXIT_CODES.validation
10309
- );
10310
- const requested = opts.variations ? commaList(opts.variations) : [];
10311
- const control = experiment.variations.find((variation) => variation.is_control);
10312
- if (!control) fail(this, "Experiment has no control variation.", EXIT_CODES.validation);
10313
- const matchedById = /* @__PURE__ */ new Map();
10314
- const unmatched = [];
10315
- for (const ref of requested) {
10316
- const exactMatch = experiment.variations.find(
10317
- (candidate) => ref === candidate.id || ref === candidate.slug
10318
- );
10319
- const nameMatches = exactMatch ? [] : experiment.variations.filter(
10320
- (candidate) => ref.toLowerCase() === candidate.name.toLowerCase()
10321
- );
10322
- if (nameMatches.length > 1) {
10323
- const matches = nameMatches.map(
10324
- (candidate) => `${candidate.name} (ID: ${candidate.id}${candidate.slug ? `, slug: ${candidate.slug}` : ""})`
11168
+ const creds = requireCredentials();
11169
+ let devices;
11170
+ let variationIds;
11171
+ let job;
11172
+ let deadlineStart = null;
11173
+ if (opts.job) {
11174
+ const ignoredFlags = [
11175
+ opts.devices ? "--devices" : null,
11176
+ opts.variations ? "--variations" : null
11177
+ ].filter((flag) => flag !== null);
11178
+ if (ignoredFlags.length)
11179
+ console.error(
11180
+ source_default.yellow(
11181
+ `Warning: ${ignoredFlags.join(" and ")} ${ignoredFlags.length > 1 ? "are" : "is"} ignored with --job; screenshot job ${opts.job} keeps its stored devices and variations.`
11182
+ )
10325
11183
  );
11184
+ deadlineStart = screenshotNow();
11185
+ const resumeDeadline = deadlineStart + timeoutSeconds * 1e3;
11186
+ let existing;
11187
+ let resumeDelayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
11188
+ for (let attempt = 1; ; attempt += 1) {
11189
+ try {
11190
+ existing = await getScreenshotJob(
11191
+ creds,
11192
+ opts.job,
11193
+ Math.max(1, resumeDeadline - screenshotNow())
11194
+ );
11195
+ } catch (error) {
11196
+ if (attempt > 3 || screenshotNow() >= resumeDeadline) throw error;
11197
+ await new Promise(
11198
+ (resolve2) => setTimeout(
11199
+ resolve2,
11200
+ Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
11201
+ )
11202
+ );
11203
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
11204
+ continue;
11205
+ }
11206
+ if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
11207
+ await new Promise(
11208
+ (resolve2) => setTimeout(
11209
+ resolve2,
11210
+ Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
11211
+ )
11212
+ );
11213
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
11214
+ continue;
11215
+ }
11216
+ break;
11217
+ }
11218
+ if (!existing || !existing.ok || !existing.data)
10326
11219
  fail(
10327
11220
  this,
10328
- `Ambiguous --variations value "${ref}" matches: ${matches.join("; ")}. Use a variation ID or slug.`,
11221
+ existing?.error ?? "Failed to resume screenshot job",
11222
+ classifyStatus(existing?.status),
11223
+ existing?.errorDetails
11224
+ );
11225
+ job = existing.data;
11226
+ if (!job.experiment_id)
11227
+ fail(
11228
+ this,
11229
+ `Screenshot job ${job.job_id} names no experiment; refusing to resume it under ${experimentRef} (${experiment.id}).`,
10329
11230
  EXIT_CODES.validation
10330
11231
  );
11232
+ if (job.experiment_id !== experiment.id)
11233
+ fail(
11234
+ this,
11235
+ `Screenshot job ${job.job_id} belongs to experiment ${job.experiment_id}, not ${experimentRef} (${experiment.id}).`,
11236
+ EXIT_CODES.validation
11237
+ );
11238
+ const storedDevices = job.devices ?? [];
11239
+ for (const device of storedDevices.filter((device2) => !SCREENSHOT_DEVICES.includes(device2)))
11240
+ console.error(
11241
+ source_default.yellow(
11242
+ `Warning: screenshot job ${job.job_id} names device ${device}, which this CLI does not know; skipping it.`
11243
+ )
11244
+ );
11245
+ devices = storedDevices.filter((device) => SCREENSHOT_DEVICES.includes(device));
11246
+ if (!devices.length)
11247
+ fail(
11248
+ this,
11249
+ storedDevices.length ? `Screenshot job ${job.job_id} names no device this CLI knows (${storedDevices.join(", ")}).` : `Screenshot job ${job.job_id} names no devices; it cannot be resumed.`,
11250
+ EXIT_CODES.validation
11251
+ );
11252
+ if (Array.isArray(job.results))
11253
+ fail(
11254
+ this,
11255
+ `Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
11256
+ EXIT_CODES.validation
11257
+ );
11258
+ const knownVariationIds = new Set(experiment.variations.map((variation) => variation.id));
11259
+ const storedVariationIds = job.variation_ids ?? [];
11260
+ for (const id of storedVariationIds.filter((id2) => !knownVariationIds.has(id2)))
11261
+ console.error(
11262
+ source_default.yellow(
11263
+ `Warning: screenshot job ${job.job_id} names variation ${id}, which is not part of this experiment; skipping it.`
11264
+ )
11265
+ );
11266
+ variationIds = storedVariationIds.filter((id) => knownVariationIds.has(id));
11267
+ if (!variationIds.length)
11268
+ fail(
11269
+ this,
11270
+ `Screenshot job ${job.job_id} names no variation that belongs to experiment ${experimentRef}.`,
11271
+ EXIT_CODES.validation
11272
+ );
11273
+ } else {
11274
+ devices = opts.devices ? commaList(opts.devices) : SCREENSHOT_DEVICES;
11275
+ if (!devices.length)
11276
+ fail(this, "--devices must contain at least one device.", EXIT_CODES.validation);
11277
+ const unknownDevices = devices.filter((device) => !SCREENSHOT_DEVICES.includes(device));
11278
+ if (unknownDevices.length)
11279
+ fail(
11280
+ this,
11281
+ `Unknown screenshot device(s): ${unknownDevices.join(", ")}`,
11282
+ EXIT_CODES.validation
11283
+ );
11284
+ const requested = opts.variations ? commaList(opts.variations) : [];
11285
+ const control = experiment.variations.find((variation) => variation.is_control);
11286
+ if (!control) fail(this, "Experiment has no control variation.", EXIT_CODES.validation);
11287
+ const matchedById = /* @__PURE__ */ new Map();
11288
+ const unmatched = [];
11289
+ for (const ref of requested) {
11290
+ const exactMatch = experiment.variations.find(
11291
+ (candidate) => ref === candidate.id || ref === candidate.slug
11292
+ );
11293
+ const nameMatches = exactMatch ? [] : experiment.variations.filter(
11294
+ (candidate) => ref.toLowerCase() === candidate.name.toLowerCase()
11295
+ );
11296
+ if (nameMatches.length > 1) {
11297
+ const matches = nameMatches.map(
11298
+ (candidate) => `${candidate.name} (ID: ${candidate.id}${candidate.slug ? `, slug: ${candidate.slug}` : ""})`
11299
+ );
11300
+ fail(
11301
+ this,
11302
+ `Ambiguous --variations value "${ref}" matches: ${matches.join("; ")}. Use a variation ID or slug.`,
11303
+ EXIT_CODES.validation
11304
+ );
11305
+ }
11306
+ const variation = exactMatch ?? nameMatches[0];
11307
+ if (variation) matchedById.set(variation.id, variation);
11308
+ else unmatched.push(ref);
10331
11309
  }
10332
- const variation = exactMatch ?? nameMatches[0];
10333
- if (variation) matchedById.set(variation.id, variation);
10334
- else unmatched.push(ref);
10335
- }
10336
- if (unmatched.length)
10337
- fail(this, `Unknown --variations value(s): ${unmatched.join(", ")}`, EXIT_CODES.validation);
10338
- const selected2 = requested.length ? [...matchedById.values()] : experiment.variations;
10339
- const variationIds = [.../* @__PURE__ */ new Set([control.id, ...selected2.map((variation) => variation.id)])];
10340
- const creds = requireCredentials();
10341
- const created = await createExperimentScreenshots(creds, experiment.id, {
10342
- devices,
10343
- variation_ids: variationIds
10344
- });
10345
- if (!created.ok || !created.data)
10346
- fail(
10347
- this,
10348
- created.error ?? "Failed to create screenshot job",
10349
- classifyStatus(created.status),
10350
- created.errorDetails
10351
- );
10352
- let job = created.data;
10353
- const deadline = screenshotNow() + timeoutSeconds * 1e3;
11310
+ if (unmatched.length)
11311
+ fail(this, `Unknown --variations value(s): ${unmatched.join(", ")}`, EXIT_CODES.validation);
11312
+ const selected2 = requested.length ? [...matchedById.values()] : experiment.variations;
11313
+ variationIds = [.../* @__PURE__ */ new Set([control.id, ...selected2.map((variation) => variation.id)])];
11314
+ const created = await createExperimentScreenshots(creds, experiment.id, {
11315
+ devices,
11316
+ variation_ids: variationIds
11317
+ });
11318
+ if (!created.ok || !created.data)
11319
+ fail(
11320
+ this,
11321
+ created.error ?? "Failed to create screenshot job",
11322
+ classifyStatus(created.status),
11323
+ created.errorDetails
11324
+ );
11325
+ job = created.data;
11326
+ }
11327
+ const deadline = (deadlineStart ?? screenshotNow()) + timeoutSeconds * 1e3;
10354
11328
  let delayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
10355
11329
  let consecutiveTransientFailures = 0;
10356
11330
  while (job.status === "queued" || job.status === "processing") {
10357
11331
  if (screenshotNow() >= deadline)
10358
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
11332
+ fail(
11333
+ this,
11334
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
11335
+ labelPairs: opts.labelPairs,
11336
+ json: explicitJson,
11337
+ attachQa: opts.attachQa
11338
+ })
11339
+ );
10359
11340
  await new Promise(
10360
11341
  (resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
10361
11342
  );
10362
11343
  if (screenshotNow() >= deadline)
10363
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
11344
+ fail(
11345
+ this,
11346
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
11347
+ labelPairs: opts.labelPairs,
11348
+ json: explicitJson,
11349
+ attachQa: opts.attachQa
11350
+ })
11351
+ );
10364
11352
  let polled;
10365
11353
  try {
10366
11354
  polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - screenshotNow()));
10367
11355
  } catch (error) {
10368
11356
  if (screenshotNow() >= deadline)
10369
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
11357
+ fail(
11358
+ this,
11359
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
11360
+ labelPairs: opts.labelPairs,
11361
+ json: explicitJson,
11362
+ attachQa: opts.attachQa
11363
+ })
11364
+ );
10370
11365
  consecutiveTransientFailures += 1;
10371
11366
  if (consecutiveTransientFailures > 3) throw error;
10372
11367
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10373
11368
  continue;
10374
11369
  }
10375
11370
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
11371
+ if (polled.ok && polled.data) job = polled.data;
10376
11372
  if (!polledToTerminalState && screenshotNow() >= deadline) {
10377
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
11373
+ fail(
11374
+ this,
11375
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
11376
+ labelPairs: opts.labelPairs,
11377
+ json: explicitJson,
11378
+ attachQa: opts.attachQa
11379
+ })
11380
+ );
10378
11381
  }
10379
11382
  if (!polled.ok || !polled.data) {
10380
11383
  const transient = polled.status === 429 || polled.status >= 500;
@@ -10390,7 +11393,6 @@ var screenshots = new Command("screenshots").description(
10390
11393
  continue;
10391
11394
  }
10392
11395
  consecutiveTransientFailures = 0;
10393
- job = polled.data;
10394
11396
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10395
11397
  }
10396
11398
  if (opts.attachQa !== false) {
@@ -10421,16 +11423,32 @@ var screenshots = new Command("screenshots").description(
10421
11423
  const rows = [];
10422
11424
  const errors = [];
10423
11425
  const usedFilenames = /* @__PURE__ */ new Set();
11426
+ const hasControlVariation = experiment.variations.some((item) => item.is_control);
10424
11427
  const downloads = [];
11428
+ if (Array.isArray(job.results))
11429
+ fail(
11430
+ this,
11431
+ `Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
11432
+ EXIT_CODES.validation
11433
+ );
10425
11434
  for (const variationId of variationIds) {
10426
11435
  const variation = experiment.variations.find((item) => item.id === variationId);
11436
+ if (!variation) {
11437
+ console.error(
11438
+ source_default.yellow(
11439
+ `Warning: variation ${variationId} is not part of this experiment; skipping it.`
11440
+ )
11441
+ );
11442
+ continue;
11443
+ }
10427
11444
  for (const device of devices) {
10428
11445
  const url = job.results?.[variationId]?.[device];
10429
11446
  if (!url) {
10430
11447
  downloads.push({ variation, device, url, filename: "" });
10431
11448
  continue;
10432
11449
  }
10433
- const baseFilename = `${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
11450
+ const pairPrefix = opts.labelPairs === false || !hasControlVariation ? "" : variation.is_control ? "before-" : "after-";
11451
+ const baseFilename = `${pairPrefix}${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
10434
11452
  let filename = `${baseFilename}.png`;
10435
11453
  if (usedFilenames.has(filename)) {
10436
11454
  const suffix = variation.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
@@ -10487,7 +11505,7 @@ var screenshots = new Command("screenshots").description(
10487
11505
  printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
10488
11506
  });
10489
11507
  });
10490
- var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(create).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText(
11508
+ var experimentsCommand = new Command("experiments").alias("exp").description("Manage experiments").addCommand(list).addCommand(get).addCommand(results).addCommand(update).addCommand(create).addCommand(attachIdea).addCommand(detachIdea).addCommand(pause).addCommand(complete).addCommand(del).addCommand(pullFilesystem).addCommand(planFilesystem).addCommand(applyFilesystem).addCommand(screenshots).addCommand(qa).addHelpText(
10491
11509
  "after",
10492
11510
  `
10493
11511
  Examples:
@@ -10581,10 +11599,10 @@ async function importConfigModule(absPath) {
10581
11599
  }
10582
11600
  throw new Error(`Unsupported config extension "${ext}". Use .drip.config.ts or .drip.config.js.`);
10583
11601
  }
10584
- function isRecord(value) {
11602
+ function isRecord2(value) {
10585
11603
  return !!value && typeof value === "object" && !Array.isArray(value);
10586
11604
  }
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) {
11605
+ 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 action23(opts) {
10588
11606
  const creds = requireCredentials();
10589
11607
  const configPath = opts.config ?? findConfigPath();
10590
11608
  if (!configPath) {
@@ -10599,12 +11617,12 @@ var pushCommand = new Command("push").description("Push experiment definitions f
10599
11617
  let experiments;
10600
11618
  try {
10601
11619
  const mod = await importConfigModule(absoluteConfigPath);
10602
- const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
11620
+ const exported = isRecord2(mod) && "default" in mod ? mod.default : mod;
10603
11621
  if (Array.isArray(exported)) {
10604
11622
  experiments = exported;
10605
- } else if (isRecord(exported) && Array.isArray(exported.experiments)) {
11623
+ } else if (isRecord2(exported) && Array.isArray(exported.experiments)) {
10606
11624
  experiments = exported.experiments;
10607
- } else if (isRecord(exported) && typeof exported.slug === "string" && Array.isArray(exported.variations)) {
11625
+ } else if (isRecord2(exported) && typeof exported.slug === "string" && Array.isArray(exported.variations)) {
10608
11626
  experiments = [exported];
10609
11627
  } else {
10610
11628
  spinner?.fail("Invalid config format");
@@ -11028,7 +12046,7 @@ async function createSession(folder) {
11028
12046
  var harnessCommand = new Command("harness").description(
11029
12047
  "Develop AB Test Harness projects through Apex live QA sessions"
11030
12048
  );
11031
- harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action21(folderArg, opts) {
12049
+ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action24(folderArg, opts) {
11032
12050
  const folder = resolveFolder(folderArg);
11033
12051
  const targetPath = apexConfigPath(folder);
11034
12052
  if (import_node_fs9.default.existsSync(targetPath) && !opts.force) {
@@ -11048,7 +12066,7 @@ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment
11048
12066
  () => printSuccess(`Created ${targetPath}`)
11049
12067
  );
11050
12068
  });
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) {
12069
+ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment folder").option("--no-open", "Print preview URL instead of opening it").action(async function action25(folderArg, opts) {
11052
12070
  const folder = resolveFolder(folderArg);
11053
12071
  const created = await createSession(folder);
11054
12072
  await runAbHarness(["build", folder], folder);
@@ -11115,7 +12133,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11115
12133
  process.exitCode = 0;
11116
12134
  });
11117
12135
  });
11118
- harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
12136
+ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action26(folderArg) {
11119
12137
  const folder = resolveFolder(folderArg);
11120
12138
  const created = await createSession(folder);
11121
12139
  await runAbHarness(["verify", folder], folder);
@@ -11490,7 +12508,7 @@ Examples:
11490
12508
  apex workspace publish ./workspace
11491
12509
  `
11492
12510
  );
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) {
12511
+ 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 action27(experimentId, opts) {
11494
12512
  try {
11495
12513
  const creds = requireCredentials();
11496
12514
  const expRes = await getExperiment(creds, experimentId);
@@ -11540,7 +12558,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11540
12558
  throw error;
11541
12559
  }
11542
12560
  });
11543
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
12561
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action28(folderArg) {
11544
12562
  try {
11545
12563
  const folder = folderPath2(folderArg);
11546
12564
  const session = readSession(folder);
@@ -11572,7 +12590,7 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
11572
12590
  fail(this, error instanceof Error ? error.message : String(error));
11573
12591
  }
11574
12592
  });
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) {
12593
+ 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 action29(folderArg, opts) {
11576
12594
  try {
11577
12595
  const folder = folderPath2(folderArg);
11578
12596
  const session = readSession(folder);
@@ -11651,7 +12669,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11651
12669
  throw error;
11652
12670
  }
11653
12671
  });
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) {
12672
+ 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 action30(folderArg, opts) {
11655
12673
  try {
11656
12674
  const result = await runPublish(folderArg, opts);
11657
12675
  const build = result.build;
@@ -11695,7 +12713,7 @@ function briefLine(item) {
11695
12713
  ].filter(Boolean);
11696
12714
  return parts.join("\n");
11697
12715
  }
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) {
12716
+ 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 action31(opts) {
11699
12717
  const creds = requireCredentials();
11700
12718
  const limit = parseInt(opts.limit, 10);
11701
12719
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -11749,7 +12767,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
11749
12767
  }
11750
12768
  );
11751
12769
  });
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) {
12770
+ 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 action32(id, opts) {
11753
12771
  if (opts.output !== void 0) {
11754
12772
  process.stderr.write(
11755
12773
  `Warning: --output is deprecated for apex roadmap handoff; use --out instead. --output will be removed in v${LEGACY_OUTPUT_REMOVAL_VERSION}.
@@ -11968,7 +12986,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
11968
12986
  console.log(`${index + 1}. ${check}`);
11969
12987
  });
11970
12988
  });
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) {
12989
+ 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 action33(opts) {
11972
12990
  const creds = requireCredentials();
11973
12991
  const res = await verifyTracking(creds, {
11974
12992
  minutes: Number.parseInt(opts.minutes, 10),
@@ -12225,7 +13243,7 @@ function printGoal(goal) {
12225
13243
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
12226
13244
  console.log();
12227
13245
  }
12228
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
13246
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action34(opts) {
12229
13247
  const creds = requireCredentials();
12230
13248
  const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
12231
13249
  if (!res.ok || !res.data) {
@@ -12249,7 +13267,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
12249
13267
  );
12250
13268
  });
12251
13269
  });
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) {
13270
+ var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value", "conversion").option("--value-property <property>", "Numeric event property for value goals").option("--value-unit <unit>", "currency|number|duration_seconds|percent").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action35(opts) {
12253
13271
  const creds = requireCredentials();
12254
13272
  const body5 = cleanBody({
12255
13273
  name: opts.name,
@@ -12275,10 +13293,10 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
12275
13293
  }
12276
13294
  output(this, { goal: res.data }, () => printGoal(res.data));
12277
13295
  });
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(
13296
+ var update2 = new Command("update").description("Update a goal").argument("<id>", "Goal ID").option("--name <name>", "Goal name").option("--type <type>", "click|pageview|custom|revenue").option("--measurement <measurement>", "conversion|value").option(
12279
13297
  "--value-property <property>",
12280
13298
  "Numeric event property for value goals; pass null to clear"
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) {
13299
+ ).option("--value-unit <unit>", "currency|number|duration_seconds|percent; pass null to clear").option("--selector <selector>", "CSS selector for click goals").option("--text-match <text>", "Text match for click goals").option("--url-pattern <pattern>", "URL pattern for pageview goals").option("--event-name <name>", "Event name for custom goals").option("--count-once", "Count only once per visitor").option("--no-count-once", "Do not count only once per visitor").option("--capture <json-or-path>", "Capture JSON array or @path").action(async function action36(id, opts) {
12282
13300
  const creds = requireCredentials();
12283
13301
  const body5 = cleanBody({
12284
13302
  name: opts.name,
@@ -12310,7 +13328,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
12310
13328
  var test = new Command("test").description("Dry run a goal URL pattern against traffic").option(
12311
13329
  "--pattern <regex>",
12312
13330
  "URL regex pattern to test (matched against the full URL, as the SDK matches pageview goals)"
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) {
13331
+ ).option("--goal <id>", "Existing goal ID whose URL pattern should be tested").option("--days <n>", "Traffic lookback days", "14").action(async function action37(opts) {
12314
13332
  if (!opts.pattern && !opts.goal)
12315
13333
  fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
12316
13334
  if (opts.pattern && opts.goal)
@@ -12392,7 +13410,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
12392
13410
  }
12393
13411
  );
12394
13412
  });
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) {
13413
+ 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 action38(opts) {
12396
13414
  const creds = requireCredentials();
12397
13415
  const res = await suggestGoals(creds);
12398
13416
  if (!res.ok || !res.data)
@@ -12440,7 +13458,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
12440
13458
  );
12441
13459
  }
12442
13460
  });
12443
- var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update).addCommand(test).addCommand(suggest).addHelpText(
13461
+ var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update2).addCommand(test).addCommand(suggest).addHelpText(
12444
13462
  "after",
12445
13463
  `
12446
13464
  Examples:
@@ -12482,7 +13500,7 @@ function printFunnel(funnel) {
12482
13500
  console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
12483
13501
  console.log();
12484
13502
  }
12485
- var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
13503
+ var list3 = new Command("list").alias("ls").description("List funnels").action(async function action39() {
12486
13504
  const res = await listFunnels(requireCredentials());
12487
13505
  if (!res.ok || !res.data)
12488
13506
  fail(
@@ -12501,7 +13519,7 @@ var list3 = new Command("list").alias("ls").description("List funnels").action(a
12501
13519
  )
12502
13520
  );
12503
13521
  });
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) {
13522
+ 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 action40(opts) {
12505
13523
  const res = await createFunnel(requireCredentials(), {
12506
13524
  name: opts.name,
12507
13525
  steps: readSteps(this, opts.stepsFile)
@@ -12515,7 +13533,7 @@ var create3 = new Command("create").description("Create a funnel").requiredOptio
12515
13533
  );
12516
13534
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12517
13535
  });
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) {
13536
+ var update3 = 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 action41(id, opts) {
12519
13537
  const res = await updateFunnel(requireCredentials(), id, {
12520
13538
  steps: readSteps(this, opts.stepsFile)
12521
13539
  });
@@ -12528,7 +13546,7 @@ var update2 = new Command("update").description("Replace a funnel's steps").argu
12528
13546
  );
12529
13547
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12530
13548
  });
12531
- var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
13549
+ var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action42(id) {
12532
13550
  const res = await deleteFunnel(requireCredentials(), id);
12533
13551
  if (!res.ok)
12534
13552
  fail(
@@ -12539,7 +13557,7 @@ var del2 = new Command("delete").description("Delete a funnel").argument("<id>",
12539
13557
  );
12540
13558
  output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
12541
13559
  });
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) {
13560
+ var run = new Command("run").description("Run funnel analysis").argument("<id>", "Funnel ID").option("--days <n>", "Analysis window", "30").action(async function action43(id, opts) {
12543
13561
  if (!/^[1-9]\d*$/.test(opts.days))
12544
13562
  fail(this, "--days must be a positive integer", EXIT_CODES.validation);
12545
13563
  const days = Number.parseInt(opts.days, 10);
@@ -12563,11 +13581,11 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
12563
13581
  )
12564
13582
  );
12565
13583
  });
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) {
13584
+ var validate2 = new Command("validate").description("Validate funnel steps without writing").requiredOption("--steps-file <file>", "JSON array of { label, pattern }").action(function action44(opts) {
12567
13585
  const steps = readSteps(this, opts.stepsFile);
12568
13586
  output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
12569
13587
  });
12570
- var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update2).addCommand(del2).addCommand(run).addCommand(validate2);
13588
+ var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update3).addCommand(del2).addCommand(run).addCommand(validate2);
12571
13589
 
12572
13590
  // src/commands/environments.ts
12573
13591
  init_source();
@@ -12624,11 +13642,11 @@ async function load2(command) {
12624
13642
  }
12625
13643
  return result.data;
12626
13644
  }
12627
- var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
13645
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action45() {
12628
13646
  const { environments } = await load2(this);
12629
13647
  output(this, { environments }, () => printEnvironments(environments.items));
12630
13648
  });
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) {
13649
+ 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 action46(domain, opts) {
12632
13650
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12633
13651
  if (!normalizedDomain) {
12634
13652
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12675,7 +13693,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
12675
13693
  () => printEnvironments(result.data.environments.items)
12676
13694
  );
12677
13695
  });
12678
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
13696
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action47(domain) {
12679
13697
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12680
13698
  if (!normalizedDomain) {
12681
13699
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12739,11 +13757,11 @@ function printConsent(data) {
12739
13757
  `Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`
12740
13758
  );
12741
13759
  }
12742
- var get2 = new Command("get").description("Show consent settings").action(async function action45() {
13760
+ var get2 = new Command("get").description("Show consent settings").action(async function action48() {
12743
13761
  const data = await load3(this);
12744
13762
  output(this, data, () => printConsent(data));
12745
13763
  });
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) {
13764
+ 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 action49(opts) {
12747
13765
  const consentMode = {
12748
13766
  enabled: true,
12749
13767
  provider: opts.provider,
@@ -12764,7 +13782,7 @@ var set = new Command("set").description("Enable and configure consent gating").
12764
13782
  warn(this, result.data.precompile);
12765
13783
  output(this, result.data, () => printConsent(result.data));
12766
13784
  });
12767
- var off = new Command("off").description("Disable consent gating").action(async function action47() {
13785
+ var off = new Command("off").description("Disable consent gating").action(async function action50() {
12768
13786
  const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
12769
13787
  if (!result.ok || !result.data)
12770
13788
  fail(
@@ -13078,8 +14096,8 @@ function score2(path24, patterns) {
13078
14096
  0
13079
14097
  );
13080
14098
  }
13081
- function best(fields2, patterns, numeric2 = false) {
13082
- const ranked = fields2.filter(
14099
+ function best(fields3, patterns, numeric2 = false) {
14100
+ const ranked = fields3.filter(
13083
14101
  (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
13084
14102
  ).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));
13085
14103
  if (!ranked.length) return [];
@@ -13104,15 +14122,15 @@ function proposeTriggers(report) {
13104
14122
  continue;
13105
14123
  const descriptors = candidate.fields ?? candidate.paths;
13106
14124
  const payload = candidate.properties ?? candidate.payload ?? candidate;
13107
- const fields2 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
14125
+ const fields3 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
13108
14126
  const revenues = best(
13109
- fields2,
14127
+ fields3,
13110
14128
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
13111
14129
  true
13112
14130
  );
13113
- const orderIds = best(fields2, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
13114
- const currencies = best(fields2, [/currency/, /(^|\.)cur(rency)?$/]);
13115
- const items = best(fields2, [/(^|\.)((line_?)?items|products)$/]);
14131
+ const orderIds = best(fields3, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
14132
+ const currencies = best(fields3, [/currency/, /(^|\.)cur(rency)?$/]);
14133
+ const items = best(fields3, [/(^|\.)((line_?)?items|products)$/]);
13116
14134
  if (!revenues.length || !orderIds.length || !currencies.length) continue;
13117
14135
  for (const revenue of revenues)
13118
14136
  for (const orderId of orderIds)
@@ -13662,11 +14680,11 @@ async function patchWithRetry(command, build) {
13662
14680
  warn2(command, result.data.precompile);
13663
14681
  return result.data;
13664
14682
  }
13665
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
14683
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action51() {
13666
14684
  const data = await load4(this);
13667
14685
  output(this, data, () => printDomains(data));
13668
14686
  });
13669
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
14687
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action52(domain) {
13670
14688
  const added = domain.map(normalizeDomain);
13671
14689
  const data = await patchWithRetry(
13672
14690
  this,
@@ -13674,7 +14692,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
13674
14692
  );
13675
14693
  output(this, data, () => printDomains(data));
13676
14694
  });
13677
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
14695
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action53(domain) {
13678
14696
  const removeSet = new Set(domain.map(normalizeDomain));
13679
14697
  const data = await patchWithRetry(
13680
14698
  this,
@@ -13682,7 +14700,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
13682
14700
  );
13683
14701
  output(this, data, () => printDomains(data));
13684
14702
  });
13685
- var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
14703
+ var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action54(domain) {
13686
14704
  const replacement = domain.map(normalizeDomain);
13687
14705
  const data = await patchWithRetry(this, () => replacement);
13688
14706
  output(this, data, () => printDomains(data));
@@ -13774,14 +14792,14 @@ async function patchWithRetry2(command, build) {
13774
14792
  );
13775
14793
  return result.data;
13776
14794
  }
13777
- var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
14795
+ var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action55() {
13778
14796
  const data = await load5(this);
13779
14797
  output(this, data, () => printSettings(data.externalTracking));
13780
14798
  });
13781
14799
  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(
13782
14800
  "--signed",
13783
14801
  "Require signed handover tokens (the default; unsigning is a platform-support action)"
13784
- ).action(async function action53(opts) {
14802
+ ).action(async function action56(opts) {
13785
14803
  const domains = opts.domain.map(normalizeDomain2);
13786
14804
  if (domains.some((domain) => !domain))
13787
14805
  fail(
@@ -13801,14 +14819,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
13801
14819
  }));
13802
14820
  output(this, data, () => printSettings(data.externalTracking));
13803
14821
  });
13804
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
14822
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action57() {
13805
14823
  const data = await patchWithRetry2(this, (current) => ({
13806
14824
  externalTracking: { domains: [], signed: true },
13807
14825
  ifRevision: current.revision
13808
14826
  }));
13809
14827
  output(this, data, () => printSettings(data.externalTracking));
13810
14828
  });
13811
- var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action55(opts) {
14829
+ 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 action58(opts) {
13812
14830
  const config = await load5(this);
13813
14831
  const target = normalizeDomain2(opts.toDomain);
13814
14832
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
@@ -14851,7 +15869,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
14851
15869
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
14852
15870
  "--worker <url>",
14853
15871
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
14854
- ).action(async function action56(options) {
15872
+ ).action(async function action59(options) {
14855
15873
  const data = await runDoctor(options);
14856
15874
  if (options.check && data.checks.length === 0)
14857
15875
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
@@ -14873,7 +15891,7 @@ function printDoctorReport(checks) {
14873
15891
  var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
14874
15892
  "--since <date>",
14875
15893
  "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
14876
- ).action(async function action57(opts) {
15894
+ ).action(async function action60(opts) {
14877
15895
  if (opts.since !== void 0 && parseDay(opts.since) === null)
14878
15896
  fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
14879
15897
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
@@ -14933,7 +15951,7 @@ function analyticsSubcommand(name, description, defaultDays) {
14933
15951
  command.option("--planning-days <n>", "Planning window in days", "180");
14934
15952
  command.option("--limit <n>", "Maximum page rows", "50");
14935
15953
  }
14936
- return command.action(async function action71(opts) {
15954
+ return command.action(async function action74(opts) {
14937
15955
  const result = await getAnalytics(requireCredentials(), name, {
14938
15956
  days: positiveInteger(this, opts.days, "--days"),
14939
15957
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -14953,7 +15971,7 @@ function urlsCommand() {
14953
15971
  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(
14954
15972
  "--match <regex>",
14955
15973
  "Client-side regex filter; exhausts inventory pages before filtering"
14956
- ).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
15974
+ ).option("--limit <n>", "Maximum rows to print", "50").action(async function action74(opts) {
14957
15975
  const creds = requireCredentials();
14958
15976
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
14959
15977
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -15147,10 +16165,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
15147
16165
  const chunks = [];
15148
16166
  let bytes = 0;
15149
16167
  let settled = false;
15150
- const settle = (action71) => {
16168
+ const settle = (action74) => {
15151
16169
  if (settled) return;
15152
16170
  settled = true;
15153
- action71();
16171
+ action74();
15154
16172
  };
15155
16173
  stream.on("data", (chunk) => {
15156
16174
  if (settled) return;
@@ -15221,11 +16239,11 @@ function pinnedFetch(url, addresses, limits) {
15221
16239
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
15222
16240
  };
15223
16241
  let settled = false;
15224
- const settle = (action71) => {
16242
+ const settle = (action74) => {
15225
16243
  if (settled) return;
15226
16244
  settled = true;
15227
16245
  clearTimeout(deadline);
15228
- action71();
16246
+ action74();
15229
16247
  };
15230
16248
  const req = send3(
15231
16249
  url,
@@ -15560,7 +16578,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
15560
16578
  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(
15561
16579
  "--worker-url <url>",
15562
16580
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
15563
- ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action58(opts) {
16581
+ ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action61(opts) {
15564
16582
  const creds = requireCredentials();
15565
16583
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
15566
16584
  if (!shopId)
@@ -15860,11 +16878,11 @@ function write(name) {
15860
16878
  });
15861
16879
  return c;
15862
16880
  }
15863
- function lifecycle(action71) {
15864
- return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
15865
- const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
16881
+ function lifecycle(action74) {
16882
+ return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a flag`).argument("<id>").action(async function(id) {
16883
+ const r = await transitionManagedDelivery(requireCredentials(), kind, id, action74);
15866
16884
  if (!r.ok || !r.data)
15867
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
16885
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
15868
16886
  output(this, { flag: r.data }, () => detail(r.data));
15869
16887
  });
15870
16888
  }
@@ -15902,13 +16920,74 @@ function parse(command, value) {
15902
16920
  );
15903
16921
  }
15904
16922
  }
15905
- function detail2(x) {
16923
+ function collect2(value, previous) {
16924
+ return [...previous, value];
16925
+ }
16926
+ function customFieldsRecord(value) {
16927
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
16928
+ }
16929
+ function typedFieldValue(definition, raw) {
16930
+ if (raw === "null") return null;
16931
+ if (raw.trim() === "") return null;
16932
+ switch (definition?.type) {
16933
+ case "number": {
16934
+ const parsed = Number(raw.trim());
16935
+ return Number.isFinite(parsed) ? parsed : raw;
16936
+ }
16937
+ case "boolean": {
16938
+ const trimmed = raw.trim().toLowerCase();
16939
+ if (["true", "yes", "y", "1"].includes(trimmed)) return true;
16940
+ if (["false", "no", "n", "0"].includes(trimmed)) return false;
16941
+ return raw;
16942
+ }
16943
+ default:
16944
+ return raw;
16945
+ }
16946
+ }
16947
+ async function customFieldsFromPairs(command, pairs) {
16948
+ if (pairs.length === 0) return void 0;
16949
+ const parsedPairs = pairs.map((pair) => {
16950
+ const separator = pair.indexOf("=");
16951
+ if (separator <= 0)
16952
+ fail(command, `Invalid --field "${pair}": use key=value.`, EXIT_CODES.validation);
16953
+ return { key: pair.slice(0, separator), raw: pair.slice(separator + 1) };
16954
+ });
16955
+ const fields3 = await listBacklogFields(requireCredentials());
16956
+ if (!fields3.ok || !fields3.data) {
16957
+ process.stderr.write(
16958
+ `Could not read the shop's field definitions (${fields3.error ?? `HTTP ${fields3.status}`}). Sending every --field value as text, so a number or yes/no field will answer 422. Add the backlog:read scope to this key to type them.
16959
+ `
16960
+ );
16961
+ }
16962
+ const byKey = new Map(
16963
+ (fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field])
16964
+ );
16965
+ const values = {};
16966
+ for (const { key, raw } of parsedPairs) values[key] = typedFieldValue(byKey.get(key), raw);
16967
+ return values;
16968
+ }
16969
+ function detail2(x, labels) {
15906
16970
  console.log();
15907
16971
  console.log(source_default.bold(String(x.title ?? x.label ?? x.key ?? x.id)));
15908
16972
  console.log(` ID: ${source_default.dim(x.id)}`);
15909
16973
  if (x.status) console.log(` State: ${x.status}`);
16974
+ const custom = customFieldsRecord(x.custom_fields);
16975
+ if (Object.keys(custom).length > 0) {
16976
+ console.log();
16977
+ for (const [key, value] of Object.entries(custom)) {
16978
+ console.log(` ${labels.get(key) ?? key}: ${value === null ? "-" : String(value)}`);
16979
+ }
16980
+ }
15910
16981
  console.log();
15911
16982
  }
16983
+ async function fieldLabels(command, item) {
16984
+ if (shouldUseJson(command)) return /* @__PURE__ */ new Map();
16985
+ if (Object.keys(customFieldsRecord(item.custom_fields)).length === 0) return /* @__PURE__ */ new Map();
16986
+ const fields3 = await listBacklogFields(requireCredentials());
16987
+ return new Map(
16988
+ (fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field.label])
16989
+ );
16990
+ }
15912
16991
  var list7 = new Command("list").alias("ls").description("List backlog").option("-n, --limit <n>", "Max results", "50").option("--cursor <cursor>", "Continue from cursor").option("--include-archived", "Include archived backlog items").action(async function(o) {
15913
16992
  const r = await listInsights(
15914
16993
  requireCredentials(),
@@ -15937,27 +17016,35 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
15937
17016
  const r = await getInsight(requireCredentials(), kind2, id);
15938
17017
  if (!r.ok || !r.data)
15939
17018
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15940
- output(this, { backlog_item: r.data }, () => detail2(r.data));
17019
+ const labels = await fieldLabels(this, r.data);
17020
+ output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
15941
17021
  });
15942
- function write2(action71) {
15943
- const c = new Command(action71).description(
15944
- `${action71 === "create" ? "Create" : "Update"} a ${singular}`
17022
+ function write2(action74) {
17023
+ const c = new Command(action74).description(
17024
+ `${action74 === "create" ? "Create" : "Update"} a ${singular}`
15945
17025
  );
15946
- if (action71 === "update") c.argument("<id>", `${singular} ID`);
15947
- c.argument("[json]", "Inline JSON, @file, or file path").option(
15948
- "--file <path>",
15949
- "JSON body file"
17026
+ if (action74 === "update") c.argument("<id>", `${singular} ID`);
17027
+ c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").option(
17028
+ "--field <key=value>",
17029
+ "Set one of the shop's own fields; repeatable. Use key=null to clear it.",
17030
+ collect2,
17031
+ []
15950
17032
  );
15951
17033
  c.action(async function(...args) {
15952
17034
  const o = this.opts();
15953
- const id = action71 === "update" ? String(args[0]) : void 0;
15954
- const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
17035
+ const id = action74 === "update" ? String(args[0]) : void 0;
17036
+ const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
15955
17037
  const body5 = parse(this, raw);
17038
+ const customFields = await customFieldsFromPairs(this, o.field);
17039
+ if (customFields) {
17040
+ body5.custom_fields = { ...customFieldsRecord(body5.custom_fields), ...customFields };
17041
+ }
15956
17042
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
15957
- const r = action71 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
17043
+ const r = action74 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
15958
17044
  if (!r.ok || !r.data)
15959
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15960
- output(this, { backlog_item: r.data }, () => detail2(r.data));
17045
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
17046
+ const labels = await fieldLabels(this, r.data);
17047
+ output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
15961
17048
  });
15962
17049
  return c;
15963
17050
  }
@@ -16002,7 +17089,99 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
16002
17089
  fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
16003
17090
  output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
16004
17091
  });
16005
- var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
17092
+ function fieldDetail(field) {
17093
+ console.log();
17094
+ console.log(source_default.bold(field.label));
17095
+ console.log(` ID: ${source_default.dim(field.id)}`);
17096
+ console.log(` Key: ${field.key}`);
17097
+ console.log(` Type: ${field.type}`);
17098
+ if (field.options.length > 0) console.log(` Choices: ${field.options.join(", ")}`);
17099
+ console.log(` Required: ${field.required ? "yes" : "no"}`);
17100
+ console.log(` Visible to clients: ${field.client_visible ? "yes" : "no"}`);
17101
+ if (field.archived_at) console.log(` Archived: ${field.archived_at}`);
17102
+ console.log();
17103
+ }
17104
+ function boolOption(command, flag, value) {
17105
+ if (value === void 0) return void 0;
17106
+ const trimmed = value.trim().toLowerCase();
17107
+ if (["true", "yes", "y", "1"].includes(trimmed)) return true;
17108
+ if (["false", "no", "n", "0"].includes(trimmed)) return false;
17109
+ fail(command, `Invalid ${flag} "${value}": use true or false.`, EXIT_CODES.validation);
17110
+ }
17111
+ var fieldsList = new Command("list").alias("ls").description("List the shop's idea fields").option("--include-archived", "Include archived fields").action(async function(o) {
17112
+ const r = await listBacklogFields(requireCredentials(), o.includeArchived);
17113
+ if (!r.ok || !r.data)
17114
+ fail(this, r.error ?? "Failed to list fields", classifyStatus(r.status), r.errorDetails);
17115
+ output(
17116
+ this,
17117
+ { fields: r.data.data },
17118
+ () => printTable(
17119
+ ["KEY", "LABEL", "TYPE", "REQUIRED", "CLIENT"],
17120
+ r.data.data.map((field) => [
17121
+ field.key,
17122
+ field.label,
17123
+ field.type,
17124
+ field.required ? "yes" : "no",
17125
+ field.client_visible ? "yes" : "no"
17126
+ ]),
17127
+ [24, 28, 14, 10, 8]
17128
+ )
17129
+ );
17130
+ });
17131
+ var fieldsCreate = new Command("create").description("Add an idea field").requiredOption("--label <text>", "What the field is called").requiredOption(
17132
+ "--type <type>",
17133
+ "short_text, long_text, number, boolean, single_choice, date, url or person"
17134
+ ).option("--key <key>", "Field key; derived from the label when omitted").option(
17135
+ "--option <value>",
17136
+ "A choice for a single_choice field; repeatable",
17137
+ collect2,
17138
+ []
17139
+ ).option("--required <true|false>", "Every idea must carry a value").option("--client-visible <true|false>", "Share the value with the client").action(async function(o) {
17140
+ const required = boolOption(this, "--required", o.required);
17141
+ const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
17142
+ const r = await createBacklogField(requireCredentials(), {
17143
+ label: o.label,
17144
+ type: o.type,
17145
+ ...o.key ? { key: o.key } : {},
17146
+ ...o.option.length > 0 ? { options: o.option } : {},
17147
+ ...required === void 0 ? {} : { required },
17148
+ ...clientVisible === void 0 ? {} : { client_visible: clientVisible }
17149
+ });
17150
+ if (!r.ok || !r.data)
17151
+ fail(this, r.error ?? "Failed to create field", classifyStatus(r.status), r.errorDetails);
17152
+ output(this, { field: r.data }, () => fieldDetail(r.data));
17153
+ });
17154
+ var fieldsUpdate = new Command("update").description("Update an idea field").argument("<id>", "field ID").option("--label <text>", "What the field is called").option(
17155
+ "--option <value>",
17156
+ "Replace the choices of a single_choice field; repeatable",
17157
+ collect2,
17158
+ []
17159
+ ).option("--required <true|false>", "Every idea must carry a value").option("--client-visible <true|false>", "Share the value with the client").option("--archived <true|false>", "Archive or restore the field").action(async function(id, o) {
17160
+ const required = boolOption(this, "--required", o.required);
17161
+ const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
17162
+ const archived = boolOption(this, "--archived", o.archived);
17163
+ const body5 = {
17164
+ ...o.label ? { label: o.label } : {},
17165
+ ...o.option.length > 0 ? { options: o.option } : {},
17166
+ ...required === void 0 ? {} : { required },
17167
+ ...clientVisible === void 0 ? {} : { client_visible: clientVisible },
17168
+ ...archived === void 0 ? {} : { archived }
17169
+ };
17170
+ if (!Object.keys(body5).length)
17171
+ fail(this, "Provide at least one field option to change.", EXIT_CODES.validation);
17172
+ const r = await updateBacklogField(requireCredentials(), id, body5);
17173
+ if (!r.ok || !r.data)
17174
+ fail(this, r.error ?? "Failed to update field", classifyStatus(r.status), r.errorDetails);
17175
+ output(this, { field: r.data }, () => fieldDetail(r.data));
17176
+ });
17177
+ var fieldsArchive = new Command("archive").description("Archive an idea field; values already on ideas are kept").argument("<id>", "field ID").action(async function(id) {
17178
+ const r = await archiveBacklogField(requireCredentials(), id);
17179
+ if (!r.ok || !r.data)
17180
+ fail(this, r.error ?? "Failed to archive field", classifyStatus(r.status), r.errorDetails);
17181
+ output(this, { field: r.data }, () => fieldDetail(r.data));
17182
+ });
17183
+ var fields2 = new Command("fields").description("Manage the shop's own idea fields").addCommand(fieldsList).addCommand(fieldsCreate).addCommand(fieldsUpdate).addCommand(fieldsArchive);
17184
+ var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2).addCommand(fields2);
16006
17185
 
16007
17186
  // src/commands/signals.ts
16008
17187
  var import_node_fs19 = __toESM(require("node:fs"));
@@ -16064,24 +17243,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
16064
17243
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
16065
17244
  output(this, { signal: r.data }, () => detail3(r.data));
16066
17245
  });
16067
- function write3(action71) {
16068
- const c = new Command(action71).description(
16069
- `${action71 === "create" ? "Create" : "Update"} a ${singular2}`
17246
+ function write3(action74) {
17247
+ const c = new Command(action74).description(
17248
+ `${action74 === "create" ? "Create" : "Update"} a ${singular2}`
16070
17249
  );
16071
- if (action71 === "update") c.argument("<id>", `${singular2} ID`);
17250
+ if (action74 === "update") c.argument("<id>", `${singular2} ID`);
16072
17251
  c.argument("[json]", "Inline JSON, @file, or file path").option(
16073
17252
  "--file <path>",
16074
17253
  "JSON body file"
16075
17254
  );
16076
17255
  c.action(async function(...args) {
16077
17256
  const o = this.opts();
16078
- const id = action71 === "update" ? String(args[0]) : void 0;
16079
- const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
17257
+ const id = action74 === "update" ? String(args[0]) : void 0;
17258
+ const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
16080
17259
  const body5 = parse2(this, raw);
16081
17260
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
16082
- const r = action71 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
17261
+ const r = action74 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
16083
17262
  if (!r.ok || !r.data)
16084
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
17263
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
16085
17264
  output(this, { signal: r.data }, () => detail3(r.data));
16086
17265
  });
16087
17266
  return c;
@@ -16173,11 +17352,11 @@ function write4(name) {
16173
17352
  });
16174
17353
  return c;
16175
17354
  }
16176
- function lifecycle2(action71) {
16177
- return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
16178
- const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
17355
+ function lifecycle2(action74) {
17356
+ return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a personalization`).argument("<id>").action(async function(id) {
17357
+ const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action74);
16179
17358
  if (!r.ok || !r.data)
16180
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
17359
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
16181
17360
  output(this, { personalization: r.data }, () => detail4(r.data));
16182
17361
  });
16183
17362
  }
@@ -16259,21 +17438,21 @@ function targetingResourceCommand(kind5) {
16259
17438
  );
16260
17439
  })
16261
17440
  );
16262
- for (const action71 of ["create", "update"]) {
16263
- const command = new Command(action71).description(
16264
- `${action71 === "create" ? "Create" : "Update"} a ${singular3}`
17441
+ for (const action74 of ["create", "update"]) {
17442
+ const command = new Command(action74).description(
17443
+ `${action74 === "create" ? "Create" : "Update"} a ${singular3}`
16265
17444
  );
16266
- if (action71 === "update") command.argument("<id>");
17445
+ if (action74 === "update") command.argument("<id>");
16267
17446
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
16268
17447
  const opts = this.opts();
16269
- const id = action71 === "update" ? String(args[0]) : void 0;
16270
- const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
17448
+ const id = action74 === "update" ? String(args[0]) : void 0;
17449
+ const value = opts.file ? `@${opts.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
16271
17450
  const requestBody = body3(this, value);
16272
- if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
17451
+ if (action74 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
16273
17452
  fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
16274
17453
  const response = await requestTargetingResource(
16275
17454
  requireCredentials(),
16276
- action71 === "create" ? "POST" : "PATCH",
17455
+ action74 === "create" ? "POST" : "PATCH",
16277
17456
  kind5,
16278
17457
  id,
16279
17458
  requestBody
@@ -16281,7 +17460,7 @@ function targetingResourceCommand(kind5) {
16281
17460
  if (!response.ok || !response.data)
16282
17461
  fail(
16283
17462
  this,
16284
- response.error ?? `Failed to ${action71}`,
17463
+ response.error ?? `Failed to ${action74}`,
16285
17464
  classifyStatus(response.status),
16286
17465
  response.errorDetails
16287
17466
  );
@@ -16290,7 +17469,7 @@ function targetingResourceCommand(kind5) {
16290
17469
  {
16291
17470
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
16292
17471
  },
16293
- () => console.log(source_default.green(`${action71}d ${singular3}`))
17472
+ () => console.log(source_default.green(`${action74}d ${singular3}`))
16294
17473
  );
16295
17474
  });
16296
17475
  root.addCommand(command);
@@ -16364,7 +17543,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
16364
17543
  // src/commands/brand.ts
16365
17544
  init_source();
16366
17545
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
16367
- var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action59() {
17546
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action62() {
16368
17547
  const creds = requireCredentials();
16369
17548
  const res = await getBrandContext(creds);
16370
17549
  if (res.status === 404) {
@@ -16397,7 +17576,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
16397
17576
 
16398
17577
  // src/commands/targeting.ts
16399
17578
  init_source();
16400
- var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action60(url, opts) {
17579
+ 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 action63(url, opts) {
16401
17580
  const creds = requireCredentials();
16402
17581
  const res = await testTargeting(creds, url, { status: opts.status });
16403
17582
  if (!res.ok || !res.data) {
@@ -16482,7 +17661,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16482
17661
  "--type <type>",
16483
17662
  "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
16484
17663
  "qa"
16485
- ).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action61(descriptionArg, opts) {
17664
+ ).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 action64(descriptionArg, opts) {
16486
17665
  if (!SOS_TYPES.has(opts.type)) {
16487
17666
  fail(
16488
17667
  this,
@@ -16509,7 +17688,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16509
17688
  }
16510
17689
  output(this, res.data, () => printCreatedTicket(res.data));
16511
17690
  });
16512
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
17691
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action65() {
16513
17692
  const creds = requireCredentials();
16514
17693
  const res = await listMySos(creds);
16515
17694
  if (!res.ok || !res.data) {
@@ -16658,7 +17837,7 @@ var create5 = new Command("create").description("Create a shop").argument("[json
16658
17837
  const r = await createShop(requireUserCredentials(), payload);
16659
17838
  done(this, r, "create shop");
16660
17839
  });
16661
- var update3 = new Command("update").description("Update a shop").argument("<id>").argument("[json]", "Inline JSON, @file, or file path").option("--holdout-pct <percent>", "Platform-operator global holdout percent (0-10)").option("--holdout-enabled", "Enable the platform-operator global holdout v1 setting").option("--holdout-disabled", "Disable the platform-operator global holdout v1 setting").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
17840
+ var update4 = new Command("update").description("Update a shop").argument("<id>").argument("[json]", "Inline JSON, @file, or file path").option("--holdout-pct <percent>", "Platform-operator global holdout percent (0-10)").option("--holdout-enabled", "Enable the platform-operator global holdout v1 setting").option("--holdout-disabled", "Disable the platform-operator global holdout v1 setting").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
16662
17841
  const b = raw === void 0 ? {} : body4(this, raw);
16663
17842
  if (o.holdoutEnabled && o.holdoutDisabled)
16664
17843
  fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
@@ -16791,7 +17970,7 @@ shopware.addCommand(
16791
17970
  );
16792
17971
  })
16793
17972
  );
16794
- var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(update3).addCommand(doctorCommand).addCommand(
17973
+ var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(update4).addCommand(doctorCommand).addCommand(
16795
17974
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
16796
17975
  ).addCommand(settings("testing-defaults", "Read or update testing defaults")).addCommand(settings("guardian", "Read or update guardian defaults", { on: "on", off: "off" })).addCommand(profile).addCommand(shopware);
16797
17976
 
@@ -16799,6 +17978,7 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
16799
17978
  function optionSchema(option) {
16800
17979
  return {
16801
17980
  flags: option.flags,
17981
+ ...option.argChoices ? { choices: option.argChoices } : {},
16802
17982
  description: option.description,
16803
17983
  required: option.required,
16804
17984
  optional: option.optional,
@@ -16824,7 +18004,7 @@ function commandSchema(command) {
16824
18004
  };
16825
18005
  }
16826
18006
  function createSchemaCommand(program3) {
16827
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action71() {
18007
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action74() {
16828
18008
  const schema = commandSchema(program3);
16829
18009
  output(this, schema, () => {
16830
18010
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -17239,7 +18419,7 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
17239
18419
  return `${lines.join("\n")}
17240
18420
  `;
17241
18421
  }
17242
- var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function action63(opts) {
18422
+ 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 action66(opts) {
17243
18423
  if (opts.gtm && opts.gtmUrl)
17244
18424
  fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
17245
18425
  if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
@@ -17442,7 +18622,7 @@ function installSkill(paths, targetRoot) {
17442
18622
  import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
17443
18623
  return target;
17444
18624
  }
17445
- var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action64(opts) {
18625
+ 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 action67(opts) {
17446
18626
  let paths;
17447
18627
  try {
17448
18628
  paths = skillPaths();
@@ -18066,7 +19246,7 @@ function selectedEvents(opts) {
18066
19246
  }
18067
19247
  var list14 = new Command("list").alias("ls").description(
18068
19248
  "List webhooks. JSON includes availableEvents; secrets are never returned after create."
18069
- ).action(async function action65() {
19249
+ ).action(async function action68() {
18070
19250
  const data = await loadWebhookIndex(this);
18071
19251
  output(this, data, () => printWebhooks(data.data));
18072
19252
  });
@@ -18075,7 +19255,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
18075
19255
  "Webhook event to subscribe to; repeat for multiple events",
18076
19256
  (value, previous = []) => [...previous, value],
18077
19257
  []
18078
- ).action(async function action66(opts) {
19258
+ ).action(async function action69(opts) {
18079
19259
  const events = selectedEvents(opts);
18080
19260
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
18081
19261
  const index = await loadWebhookIndex(this);
@@ -18107,7 +19287,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
18107
19287
  }
18108
19288
  );
18109
19289
  });
18110
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
19290
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
18111
19291
  requireShopSecretKey(this);
18112
19292
  const result = handleResult(
18113
19293
  this,
@@ -18116,7 +19296,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
18116
19296
  );
18117
19297
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
18118
19298
  });
18119
- var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action68(id, opts) {
19299
+ 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 action71(id, opts) {
18120
19300
  const limit = Number.parseInt(opts.limit, 10);
18121
19301
  if (!Number.isInteger(limit) || limit < 1)
18122
19302
  fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
@@ -18128,12 +19308,12 @@ var deliveries = new Command("deliveries").description("List webhook deliveries
18128
19308
  );
18129
19309
  output(this, result, () => printDeliveries(result.data, result.webhook));
18130
19310
  });
18131
- var update4 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option(
19311
+ var update5 = new Command("update").description("Update a webhook URL, events, or active state").argument("<id>", "Webhook id").option("--url <url>", "HTTPS callback URL").option(
18132
19312
  "--event <event>",
18133
19313
  "Replacement event; repeat for multiple events",
18134
19314
  (value, previous = []) => [...previous, value],
18135
19315
  []
18136
- ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
19316
+ ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action72(id, opts) {
18137
19317
  if (opts.active && opts.inactive)
18138
19318
  fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
18139
19319
  const events = selectedEvents(opts);
@@ -18166,7 +19346,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
18166
19346
  );
18167
19347
  output(this, { webhook: result }, () => printWebhooks([result]));
18168
19348
  });
18169
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
19349
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action73(id) {
18170
19350
  requireShopSecretKey(this);
18171
19351
  const result = handleResult(
18172
19352
  this,
@@ -18181,7 +19361,7 @@ var test3 = new Command("test").description("Send a signed test event to a webho
18181
19361
  )
18182
19362
  );
18183
19363
  });
18184
- var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update4).addCommand(remove4).addCommand(deliveries).addCommand(test3);
19364
+ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update5).addCommand(remove4).addCommand(deliveries).addCommand(test3);
18185
19365
 
18186
19366
  // src/commands/simulate.ts
18187
19367
  init_source();