@drip-apex/apex-cli 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/AGENTS.md +9 -1
  2. package/README.md +44 -1
  3. package/dist/cli.js +937 -263
  4. package/package.json +1 -1
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.7.0",
7116
7116
  license: "MIT",
7117
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7118
7118
  private: false,
@@ -7754,6 +7754,15 @@ function upsertExperimentBySlug(creds, slug, body5) {
7754
7754
  function createExperiment(creds, body5) {
7755
7755
  return request(creds, "POST", "/api/v1/experiments", body5);
7756
7756
  }
7757
+ function updateExperiment(creds, id, body5) {
7758
+ return request(creds, "PATCH", `/api/v1/experiments/${encodeURIComponent(id)}`, body5);
7759
+ }
7760
+ function attachExperimentIdea(creds, id, body5) {
7761
+ return request(creds, "POST", `/api/v1/experiments/${encodeURIComponent(id)}/idea`, body5);
7762
+ }
7763
+ function detachExperimentIdea(creds, id) {
7764
+ return request(creds, "DELETE", `/api/v1/experiments/${encodeURIComponent(id)}/idea`);
7765
+ }
7757
7766
  function pauseExperiment(creds, id) {
7758
7767
  return request(creds, "POST", `/api/v1/experiments/${id}/pause`);
7759
7768
  }
@@ -7781,7 +7790,13 @@ function createExperimentScreenshots(creds, id, body5) {
7781
7790
  }
7782
7791
  async function getScreenshotJob(creds, jobId, timeoutMs) {
7783
7792
  await testScreenshotDelay?.(timeoutMs);
7784
- return request(creds, "GET", `/api/v1/screenshots/${jobId}`, void 0, timeoutMs);
7793
+ return request(
7794
+ creds,
7795
+ "GET",
7796
+ `/api/v1/screenshots/${encodeURIComponent(jobId)}`,
7797
+ void 0,
7798
+ timeoutMs
7799
+ );
7785
7800
  }
7786
7801
  function planExperimentFilesystem(creds, id, body5) {
7787
7802
  return request(
@@ -8122,8 +8137,8 @@ function requestTargetingResource(creds, method, kind5, id, body5, limit, cursor
8122
8137
  const path24 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8123
8138
  return request(creds, method, path24, body5);
8124
8139
  }
8125
- function managedPath(kind5, id, action71) {
8126
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action71 ? `/${action71}` : ""}`;
8140
+ function managedPath(kind5, id, action74) {
8141
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action74 ? `/${action74}` : ""}`;
8127
8142
  }
8128
8143
  function listManagedDeliveries(creds, kind5, limit) {
8129
8144
  const q = limit ? `?limit=${limit}` : "";
@@ -8141,8 +8156,8 @@ function createManagedDelivery(creds, kind5, body5) {
8141
8156
  function updateManagedDelivery(creds, kind5, id, body5) {
8142
8157
  return request(creds, "PATCH", managedPath(kind5, id), body5);
8143
8158
  }
8144
- function transitionManagedDelivery(creds, kind5, id, action71) {
8145
- return request(creds, "POST", managedPath(kind5, id, action71));
8159
+ function transitionManagedDelivery(creds, kind5, id, action74) {
8160
+ return request(creds, "POST", managedPath(kind5, id, action74));
8146
8161
  }
8147
8162
  function listConfigResources(creds, family, opts) {
8148
8163
  const params = new URLSearchParams();
@@ -8301,6 +8316,28 @@ function restoreBacklogInsight(creds, id) {
8301
8316
  function deleteInsight(creds, kind5, id) {
8302
8317
  return request(creds, "DELETE", insightPath(kind5, id));
8303
8318
  }
8319
+ function listBacklogFields(creds, includeArchived) {
8320
+ const query = includeArchived ? "?include_archived=true" : "";
8321
+ return request(creds, "GET", `/api/v1/backlog/fields${query}`);
8322
+ }
8323
+ function createBacklogField(creds, body5) {
8324
+ return request(creds, "POST", "/api/v1/backlog/fields", body5);
8325
+ }
8326
+ function updateBacklogField(creds, id, body5) {
8327
+ return request(
8328
+ creds,
8329
+ "PATCH",
8330
+ `/api/v1/backlog/fields/${encodeURIComponent(id)}`,
8331
+ body5
8332
+ );
8333
+ }
8334
+ function archiveBacklogField(creds, id) {
8335
+ return request(
8336
+ creds,
8337
+ "DELETE",
8338
+ `/api/v1/backlog/fields/${encodeURIComponent(id)}`
8339
+ );
8340
+ }
8304
8341
  var listShops = (c) => userRequest(c, "GET", "/api/v1/shops");
8305
8342
  var getShop = (c, id) => userRequest(c, "GET", `/api/v1/shops/${id}`);
8306
8343
  var createShop = (c, body5) => userRequest(c, "POST", "/api/v1/shops", body5);
@@ -8835,6 +8872,34 @@ var import_node_crypto4 = __toESM(require("node:crypto"));
8835
8872
  var import_node_fs7 = __toESM(require("node:fs"));
8836
8873
  var import_node_path5 = __toESM(require("node:path"));
8837
8874
 
8875
+ // src/lib/qa-vocabulary.ts
8876
+ var checklistStatuses = [
8877
+ "pending",
8878
+ "pass",
8879
+ "warning",
8880
+ "fail",
8881
+ "accepted",
8882
+ "skipped"
8883
+ ];
8884
+ var evidenceStatuses = [
8885
+ "captured",
8886
+ "passed",
8887
+ "warning",
8888
+ "failed",
8889
+ "pending",
8890
+ "not_run"
8891
+ ];
8892
+ var evidenceKinds = [
8893
+ "screenshot",
8894
+ "goal",
8895
+ "summary",
8896
+ "launch_capture",
8897
+ "selector_audit",
8898
+ "console_network",
8899
+ "qa_audit",
8900
+ "readiness"
8901
+ ];
8902
+
8838
8903
  // ../api-client/src/qa-preview-url.ts
8839
8904
  var FORCE_ALIAS_PARAM_NAMES = ["drip_experiment_id", "drip_experiment", "drip_variation_id", "drip_variation"];
8840
8905
  function applyQaPreviewParams(url, params) {
@@ -8872,6 +8937,92 @@ function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
8872
8937
  return url;
8873
8938
  }
8874
8939
 
8940
+ // src/lib/effective-page-targeting.ts
8941
+ var ALL_PAGES_LABEL = "All pages";
8942
+ var CHECKOUT_LABEL = "Checkout";
8943
+ var UNKNOWN_PAGE_NAME = "Saved Page";
8944
+ function isRecord(value) {
8945
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8946
+ }
8947
+ function arrayOf(value) {
8948
+ if (Array.isArray(value)) return value;
8949
+ if (value === void 0 || value === null) return [];
8950
+ return [value];
8951
+ }
8952
+ function ruleKey(rule) {
8953
+ if (typeof rule === "string") return JSON.stringify(["include", "simple", rule.trim(), ""]);
8954
+ if (!isRecord(rule)) return "";
8955
+ const candidate = rule;
8956
+ return JSON.stringify([
8957
+ candidate.include === false ? "exclude" : "include",
8958
+ candidate.type ?? "simple",
8959
+ typeof candidate.pattern === "string" ? candidate.pattern : "",
8960
+ candidate.matchType ?? candidate.match_type ?? ""
8961
+ ]);
8962
+ }
8963
+ function authoredUrlRules(raw) {
8964
+ if (Array.isArray(raw.authorUrl)) return raw.authorUrl;
8965
+ const url = [...arrayOf(raw.url), ...arrayOf(raw.urls), ...arrayOf(raw.rules)];
8966
+ if (url.length === 0) return [];
8967
+ const derived = /* @__PURE__ */ new Set();
8968
+ for (const candidate of arrayOf(raw.pageRules)) {
8969
+ if (!isRecord(candidate)) continue;
8970
+ for (const rule of [...arrayOf(candidate.includeRules), ...arrayOf(candidate.excludeRules)]) {
8971
+ derived.add(ruleKey(rule));
8972
+ }
8973
+ }
8974
+ return url.filter((rule) => !derived.has(ruleKey(rule)));
8975
+ }
8976
+ function pageEntries(raw) {
8977
+ const byId = /* @__PURE__ */ new Map();
8978
+ for (const candidate of arrayOf(raw.pageRules)) {
8979
+ if (!isRecord(candidate) || typeof candidate.id !== "string" || !candidate.id) continue;
8980
+ byId.set(candidate.id, {
8981
+ name: typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : null,
8982
+ include: candidate.include !== false
8983
+ });
8984
+ }
8985
+ for (const candidate of arrayOf(raw.pages)) {
8986
+ const id = typeof candidate === "string" ? candidate : isRecord(candidate) && typeof candidate.id === "string" ? candidate.id : "";
8987
+ if (!id) continue;
8988
+ byId.set(id, {
8989
+ name: byId.get(id)?.name ?? null,
8990
+ include: isRecord(candidate) && candidate.include === false ? false : true
8991
+ });
8992
+ }
8993
+ return [...byId.values()];
8994
+ }
8995
+ function urlRuleClause(count) {
8996
+ return count === 1 ? "1 URL rule" : `${count} URL rules`;
8997
+ }
8998
+ function effectivePageTargetingLabel(targeting) {
8999
+ const raw = isRecord(targeting) ? targeting : {};
9000
+ if (raw.surface === "checkout" || isRecord(raw.checkoutBlock) && raw.checkoutBlock.enabled === true) {
9001
+ return CHECKOUT_LABEL;
9002
+ }
9003
+ const pages = pageEntries(raw);
9004
+ const rules = authoredUrlRules(raw);
9005
+ const pagesLabel = pages.map((page) => {
9006
+ const readable = page.name ?? UNKNOWN_PAGE_NAME;
9007
+ return page.include ? readable : `Exclude ${readable}`;
9008
+ }).join(", ");
9009
+ if (pages.length && rules.length) return `${pagesLabel} and ${urlRuleClause(rules.length)}`;
9010
+ if (pages.length) return pagesLabel;
9011
+ if (rules.length) return `${urlRuleClause(rules.length)} (no saved Page)`;
9012
+ return ALL_PAGES_LABEL;
9013
+ }
9014
+
9015
+ // ../api-client/src/idea-reference.ts
9016
+ 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;
9017
+ var IDEA_KEY = /^[A-Za-z0-9]{2,8}-\d+$/;
9018
+ function classifyIdeaReference(reference) {
9019
+ const trimmed = reference.trim();
9020
+ if (IDEA_UUID.test(trimmed)) return { kind: "uuid", value: trimmed };
9021
+ if (IDEA_KEY.test(trimmed))
9022
+ return { kind: "key", value: trimmed.toUpperCase() };
9023
+ return { kind: "invalid", value: trimmed };
9024
+ }
9025
+
8875
9026
  // src/lib/asset-upload.ts
8876
9027
  var import_node_crypto3 = __toESM(require("node:crypto"));
8877
9028
  var import_node_fs6 = __toESM(require("node:fs"));
@@ -9072,8 +9223,17 @@ function printExperiment(exp) {
9072
9223
  console.log(` ID: ${source_default.dim(exp.id)}`);
9073
9224
  if (exp.slug) console.log(` Slug: ${source_default.cyan(exp.slug)}`);
9074
9225
  console.log(` Status: ${statusBadge(exp.status)}`);
9226
+ console.log(` Classification: ${exp.counting_type}`);
9075
9227
  console.log(` Traffic: ${(exp.traffic_allocation * 100).toFixed(0)}%`);
9228
+ if (exp.targeting !== void 0) {
9229
+ console.log(` Targeting: ${effectivePageTargetingLabel(exp.targeting)}`);
9230
+ }
9076
9231
  console.log(` Created: ${formatDate(exp.created_at)}`);
9232
+ if (exp.linked_idea) {
9233
+ const label = exp.linked_idea.key ? `${exp.linked_idea.key}: ` : "";
9234
+ const stage = exp.linked_idea.stage ? ` (${exp.linked_idea.stage})` : "";
9235
+ console.log(` Idea: ${label}${exp.linked_idea.title}${stage}`);
9236
+ }
9077
9237
  if (exp.variations && exp.variations.length > 0) {
9078
9238
  console.log(` Variations:`);
9079
9239
  for (const v of exp.variations) {
@@ -9122,6 +9282,21 @@ function printExperimentResults(results2, currency) {
9122
9282
  console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
9123
9283
  console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
9124
9284
  console.log(` Outcome: ${statisticsStateLabel(presentedOutcome)}${outcomeProvenance}`);
9285
+ const resultsWeekdays = results2.results_weekdays;
9286
+ if (resultsWeekdays && Array.isArray(resultsWeekdays.days)) {
9287
+ const labels = {
9288
+ monday: "Mon",
9289
+ tuesday: "Tue",
9290
+ wednesday: "Wed",
9291
+ thursday: "Thu",
9292
+ friday: "Fri",
9293
+ saturday: "Sat",
9294
+ sunday: "Sun"
9295
+ };
9296
+ console.log(
9297
+ ` Judged on: ${resultsWeekdays.days.map((day) => labels[String(day)] ?? day).join(", ")} (${String(resultsWeekdays.timezone)})`
9298
+ );
9299
+ }
9125
9300
  console.log(` Statistics: ${statisticsStateLabel(statisticsState)}`);
9126
9301
  if (decision.treatmentName) {
9127
9302
  console.log(` Treatment: ${String(decision.treatmentName)}`);
@@ -9335,6 +9510,7 @@ function printFilesystemPlan(plan) {
9335
9510
  }
9336
9511
  console.log();
9337
9512
  console.log(`QA invalidated: ${plan.invalidatesQa ? source_default.yellow("yes") : "no"}`);
9513
+ console.log(`Acknowledgment: ${plan.invalidatesQa ? "--accept-qa-invalidation" : "none"}`);
9338
9514
  console.log(`Precompile: ${plan.precompileRequired ? "yes" : "no"}`);
9339
9515
  }
9340
9516
  function blockingIssueSummary(issues) {
@@ -9430,8 +9606,29 @@ function parseEvidenceInput(value) {
9430
9606
  }
9431
9607
  return parsed;
9432
9608
  }
9433
- function isQaCheckStatus(value) {
9434
- return value === "pending" || value === "pass" || value === "warning" || value === "fail" || value === "accepted" || value === "skipped";
9609
+ function qaChoice(flags, description, choices) {
9610
+ return new Option(flags, description).choices([...choices]).argParser((value) => value);
9611
+ }
9612
+ function validateQaChoice(command, option, value, allowed2) {
9613
+ if (value !== void 0 && !allowed2.includes(value)) {
9614
+ fail(command, `Invalid ${option}: ${value}`, EXIT_CODES.validation, {
9615
+ option,
9616
+ allowedValues: [...allowed2]
9617
+ });
9618
+ }
9619
+ }
9620
+ function qaSummary(run3, changed) {
9621
+ return {
9622
+ run: {
9623
+ id: run3.id,
9624
+ status: run3.status,
9625
+ summary: run3.summary,
9626
+ approvedAt: run3.approvedAt,
9627
+ updatedAt: run3.updatedAt
9628
+ },
9629
+ changed,
9630
+ validationErrors: []
9631
+ };
9435
9632
  }
9436
9633
  function tryReadFilesystemSession(folder) {
9437
9634
  try {
@@ -9563,7 +9760,40 @@ var get = new Command("get").description("Get experiment details by ID or slug")
9563
9760
  }
9564
9761
  output(this, { experiment: exp }, () => printExperiment(exp));
9565
9762
  });
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) {
9763
+ var attachIdea = new Command("attach-idea").description("Attach an experiment to an idea").argument("<experiment>", "Experiment ID or slug").requiredOption("--idea <key>", "Idea UUID or Apex backlog key").option("--follow-up", "Attach as a follow-up test").action(async function action5(experimentRef, opts) {
9764
+ const experiment = await resolveExperiment(experimentRef);
9765
+ if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
9766
+ const classified = classifyIdeaReference(opts.idea);
9767
+ if (classified.kind === "invalid") {
9768
+ fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
9769
+ }
9770
+ const res = await attachExperimentIdea(requireCredentials(), experiment.id, {
9771
+ ...classified.kind === "uuid" ? { backlog_item_id: classified.value } : { idea_key: classified.value },
9772
+ ...opts.followUp ? { as_follow_up: true } : {}
9773
+ });
9774
+ if (!res.ok)
9775
+ fail(
9776
+ this,
9777
+ res.error ?? "Failed to attach idea",
9778
+ classifyStatus(res.status),
9779
+ res.errorDetails
9780
+ );
9781
+ output(this, res.data, () => printSuccess("Idea attached."));
9782
+ });
9783
+ var detachIdea = new Command("detach-idea").description("Detach an experiment from its idea").argument("<experiment>", "Experiment ID or slug").action(async function action6(experimentRef) {
9784
+ const experiment = await resolveExperiment(experimentRef);
9785
+ if (!experiment) fail(this, `Experiment "${experimentRef}" not found.`, EXIT_CODES.validation);
9786
+ const res = await detachExperimentIdea(requireCredentials(), experiment.id);
9787
+ if (!res.ok)
9788
+ fail(
9789
+ this,
9790
+ res.error ?? "Failed to detach idea",
9791
+ classifyStatus(res.status),
9792
+ res.errorDetails
9793
+ );
9794
+ output(this, res.data, () => printSuccess("Idea detached."));
9795
+ });
9796
+ 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 action7(idOrSlug, opts) {
9567
9797
  const creds = requireCredentials();
9568
9798
  const spinner = await createSpinner("Fetching experiment results...");
9569
9799
  try {
@@ -9594,6 +9824,62 @@ var results = new Command("results").description("Read experiment results and ti
9594
9824
  fail(this, error instanceof Error ? error.message : String(error));
9595
9825
  }
9596
9826
  });
9827
+ var DAY_NAMES = {
9828
+ mon: "monday",
9829
+ monday: "monday",
9830
+ tue: "tuesday",
9831
+ tuesday: "tuesday",
9832
+ wed: "wednesday",
9833
+ wednesday: "wednesday",
9834
+ thu: "thursday",
9835
+ thursday: "thursday",
9836
+ fri: "friday",
9837
+ friday: "friday",
9838
+ sat: "saturday",
9839
+ saturday: "saturday",
9840
+ sun: "sunday",
9841
+ sunday: "sunday"
9842
+ };
9843
+ var countingTypes = ["standard", "setup_test", "aa_test", "non_program"];
9844
+ var update = new Command("update").description("Update an experiment").addOption(
9845
+ new Option("--counting-type <type>", "Draft experiment classification").choices(countingTypes)
9846
+ ).argument("<id-or-slug>", "Experiment ID or slug").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) {
9847
+ if (opts.clearResultsWeekdays && (opts.resultsWeekdays || opts.resultsTimezone)) {
9848
+ fail(
9849
+ this,
9850
+ "--clear-results-weekdays cannot be combined with window options",
9851
+ EXIT_CODES.validation
9852
+ );
9853
+ }
9854
+ const hasWindowOption = opts.resultsWeekdays !== void 0 || opts.resultsTimezone !== void 0;
9855
+ if (!opts.clearResultsWeekdays && (!opts.countingType && !hasWindowOption || hasWindowOption && (!opts.resultsWeekdays || !opts.resultsTimezone))) {
9856
+ fail(
9857
+ this,
9858
+ "--results-weekdays and --results-timezone are required together",
9859
+ EXIT_CODES.validation
9860
+ );
9861
+ }
9862
+ const days = opts.resultsWeekdays?.split(",").map((day) => DAY_NAMES[day.trim().toLowerCase()]);
9863
+ if (days?.some((day) => !day)) fail(this, "Invalid weekday token", EXIT_CODES.validation);
9864
+ const exp = await resolveExperiment(reference);
9865
+ if (!exp) fail(this, `Experiment "${reference}" not found.`, EXIT_CODES.validation);
9866
+ const results_weekdays = opts.clearResultsWeekdays ? null : { days, timezone: opts.resultsTimezone };
9867
+ const hasWindow = opts.clearResultsWeekdays || opts.resultsWeekdays !== void 0;
9868
+ const res = await updateExperiment(requireCredentials(), exp.id, {
9869
+ ...hasWindow ? { results_weekdays } : {},
9870
+ ...opts.countingType !== void 0 ? { counting_type: opts.countingType } : {}
9871
+ });
9872
+ if (!res.ok) fail(this, res.error ?? "Failed to update experiment", classifyStatus(res.status));
9873
+ const stored = res.data?.results_weekdays ?? null;
9874
+ output(this, res.data, () => {
9875
+ if (opts.countingType !== void 0)
9876
+ console.log(`Classification: ${res.data.counting_type}`);
9877
+ if (hasWindow)
9878
+ console.log(
9879
+ 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."
9880
+ );
9881
+ });
9882
+ });
9597
9883
  function collectCreateOption(value, previous) {
9598
9884
  return [...previous, value];
9599
9885
  }
@@ -9623,7 +9909,9 @@ function sourceStoreUrl(sourceRules) {
9623
9909
  }
9624
9910
  return null;
9625
9911
  }
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(
9912
+ var create = new Command("create").description("Create a new experiment").addOption(
9913
+ new Option("--counting-type <type>", "Experiment classification").choices(countingTypes).default("standard")
9914
+ ).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(
9627
9915
  "--source-url <pattern>",
9628
9916
  "Positive source URL rule (repeatable; accepts exact URLs, paths, or wildcards)",
9629
9917
  collectCreateOption,
@@ -9633,7 +9921,7 @@ var create = new Command("create").description("Create a new experiment").requir
9633
9921
  "Redirect treatment destination (repeatable; creates one treatment per URL)",
9634
9922
  collectCreateOption,
9635
9923
  []
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) {
9924
+ ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").option("--idea <id>", "Idea UUID or Apex backlog key").option("--backlog-item <id>", "Alias for --idea").action(async function action9(opts) {
9637
9925
  if (opts.type !== "mutation" && opts.type !== "redirect") {
9638
9926
  fail(this, "--type must be mutation or redirect", EXIT_CODES.validation);
9639
9927
  }
@@ -9666,50 +9954,23 @@ var create = new Command("create").description("Create a new experiment").requir
9666
9954
  const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
9667
9955
  const creds = requireCredentials();
9668
9956
  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;
9957
+ let ideaKey;
9958
+ if (opts.idea !== void 0 && opts.backlogItem !== void 0 && opts.idea.trim().toUpperCase() !== opts.backlogItem.trim().toUpperCase()) {
9959
+ fail(
9960
+ this,
9961
+ "--idea and --backlog-item name the same idea; pass only one",
9962
+ EXIT_CODES.validation
9963
+ );
9964
+ }
9965
+ const ideaReference = opts.idea ?? opts.backlogItem;
9966
+ if (ideaReference !== void 0) {
9967
+ const classified = classifyIdeaReference(ideaReference);
9968
+ if (classified.kind === "uuid") {
9969
+ backlogItemId = classified.value;
9970
+ } else if (classified.kind === "key") {
9971
+ ideaKey = classified.value;
9711
9972
  } else {
9712
- fail(this, "--backlog-item must be a UUID or Apex backlog key", EXIT_CODES.validation);
9973
+ fail(this, "--idea must be a UUID or Apex backlog key", EXIT_CODES.validation);
9713
9974
  }
9714
9975
  }
9715
9976
  const spinner = await createSpinner("Creating experiment...");
@@ -9718,8 +9979,10 @@ var create = new Command("create").description("Create a new experiment").requir
9718
9979
  slug: opts.slug ?? null,
9719
9980
  description: opts.description ?? null,
9720
9981
  delivery_type: opts.type,
9982
+ counting_type: opts.countingType,
9721
9983
  traffic_allocation: traffic / 100,
9722
9984
  ...backlogItemId ? { backlog_item_id: backlogItemId } : {},
9985
+ ...ideaKey ? { idea_key: ideaKey } : {},
9723
9986
  ...sourceRules.length > 0 ? {
9724
9987
  store_url: sourceStoreUrl(sourceRules),
9725
9988
  targeting: {
@@ -9761,11 +10024,16 @@ var create = new Command("create").description("Create a new experiment").requir
9761
10024
  );
9762
10025
  output(
9763
10026
  this,
9764
- { outcome, backlog_item_id: backlogItemId ?? null, experiment: res.data },
10027
+ {
10028
+ outcome,
10029
+ backlog_item_id: backlogItemId ?? null,
10030
+ idea_key: ideaKey ?? null,
10031
+ experiment: res.data
10032
+ },
9765
10033
  () => printExperiment(res.data)
9766
10034
  );
9767
10035
  });
9768
- var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action7(idOrSlug) {
10036
+ var pause = new Command("pause").description("Pause a running experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action10(idOrSlug) {
9769
10037
  const creds = requireCredentials();
9770
10038
  const spinner = await createSpinner("Pausing experiment...");
9771
10039
  const exp = await resolveExperiment(idOrSlug);
@@ -9787,7 +10055,7 @@ var pause = new Command("pause").description("Pause a running experiment").argum
9787
10055
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("paused")}`);
9788
10056
  });
9789
10057
  });
9790
- var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action8(idOrSlug) {
10058
+ var complete = new Command("complete").description("Complete/stop an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async function action11(idOrSlug) {
9791
10059
  const creds = requireCredentials();
9792
10060
  const spinner = await createSpinner("Completing experiment...");
9793
10061
  const exp = await resolveExperiment(idOrSlug);
@@ -9809,7 +10077,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
9809
10077
  spinner.succeed(`Experiment "${exp.name}" is now ${statusBadge("completed")}`);
9810
10078
  });
9811
10079
  });
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) {
10080
+ 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 action12(idOrSlug, opts) {
9813
10081
  const creds = requireCredentials();
9814
10082
  const exp = await resolveExperiment(idOrSlug);
9815
10083
  if (!exp) {
@@ -9854,7 +10122,7 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9854
10122
  () => spinner.succeed(`Experiment "${exp.name}" deleted.`)
9855
10123
  );
9856
10124
  });
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) {
10125
+ 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 action13(idOrSlug, opts) {
9858
10126
  const creds = requireCredentials();
9859
10127
  const spinner = await createSpinner("Pulling experiment filesystem...");
9860
10128
  try {
@@ -9895,7 +10163,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9895
10163
  throw error;
9896
10164
  }
9897
10165
  });
9898
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action11(folderArg) {
10166
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action14(folderArg) {
9899
10167
  const creds = requireCredentials();
9900
10168
  try {
9901
10169
  const folder = folderPath(folderArg);
@@ -9920,7 +10188,12 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9920
10188
  await refreshServerOwnedFilesIfNeeded(creds, folder, session.experimentId);
9921
10189
  output(
9922
10190
  this,
9923
- { folder, experimentId: session.experimentId, plan: res.data },
10191
+ {
10192
+ folder,
10193
+ experimentId: session.experimentId,
10194
+ plan: res.data,
10195
+ requiredAcknowledgment: res.data.invalidatesQa ? "accept-qa-invalidation" : null
10196
+ },
9924
10197
  () => printFilesystemPlan(res.data)
9925
10198
  );
9926
10199
  } catch (error) {
@@ -9928,11 +10201,13 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9928
10201
  fail(this, error instanceof Error ? error.message : String(error));
9929
10202
  }
9930
10203
  });
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) {
10204
+ var applyFilesystem = new Command("apply").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
10205
  const creds = requireCredentials();
9933
10206
  try {
9934
10207
  const folder = folderPath(folderArg);
9935
10208
  const session = readFilesystemSession(folder);
10209
+ if (opts.force)
10210
+ process.stderr.write("--force is deprecated; use --accept-qa-invalidation instead.\n");
9936
10211
  const files = await rewriteExperimentFilesystemAssets({
9937
10212
  files: listExperimentFilesystemFiles(folder, session),
9938
10213
  folder,
@@ -9960,7 +10235,8 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9960
10235
  }
9961
10236
  fail(this, blockingIssueSummary(planRes.data.blockingIssues), EXIT_CODES.validation, {
9962
10237
  errors: planRes.data.errors,
9963
- plan: planRes.data
10238
+ plan: planRes.data,
10239
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null
9964
10240
  });
9965
10241
  }
9966
10242
  if (planRes.data.changes.length === 0) {
@@ -9971,19 +10247,20 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
9971
10247
  experimentId: session.experimentId,
9972
10248
  applied: false,
9973
10249
  plan: planRes.data,
10250
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
9974
10251
  message: "No changes."
9975
10252
  },
9976
10253
  () => printFilesystemPlan(planRes.data)
9977
10254
  );
9978
10255
  return;
9979
10256
  }
9980
- if (planRes.data.invalidatesQa && !opts.force) {
10257
+ if (planRes.data.invalidatesQa && !opts.force && !opts.acceptQaInvalidation) {
9981
10258
  const ok = await confirmApply("This apply invalidates QA approval. Continue?");
9982
10259
  if (ok === null) {
9983
10260
  confirmationRequired(
9984
10261
  this,
9985
10262
  "Applying these changes invalidates QA approval and requires confirmation.",
9986
- `apex experiments apply ${folderArg} --force`
10263
+ `apex experiments apply ${folderArg} --accept-qa-invalidation`
9987
10264
  );
9988
10265
  }
9989
10266
  if (!ok) {
@@ -10011,11 +10288,32 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10011
10288
  experimentId: session.experimentId,
10012
10289
  applied: true,
10013
10290
  plan: planRes.data,
10291
+ requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
10014
10292
  result: res.data,
10293
+ receipt: res.data?.receipt ?? null,
10015
10294
  previewUrls
10016
10295
  },
10017
10296
  () => {
10018
10297
  printFilesystemPlan(planRes.data);
10298
+ const receipt = res.data?.receipt;
10299
+ if (receipt) {
10300
+ console.log("\nReceipt");
10301
+ console.log(` Status: ${receipt.status}`);
10302
+ console.log(` Classification: ${receipt.delivery_type} / ${receipt.counting_type}`);
10303
+ console.log(` Traffic: ${(receipt.traffic_allocation * 100).toFixed(0)}%`);
10304
+ console.log(
10305
+ ` Schedule: ${receipt.schedule.scheduled_start_at ?? "none"} \u2192 ${receipt.schedule.scheduled_end_at ?? "none"}`
10306
+ );
10307
+ console.log(` Goals: ${receipt.goal_count}`);
10308
+ console.log(
10309
+ ` QA: ${receipt.qa ? `${receipt.qa.status} (${receipt.qa.run_id}), approved: ${receipt.qa.approved_at ?? "none"}` : "none"}`
10310
+ );
10311
+ for (const variation of receipt.variations) {
10312
+ console.log(
10313
+ ` ${variation.name} (${variation.slug}): ${Object.entries(variation.digests).map(([key, value]) => `${key}=${value ?? "null"}`).join(" ")}`
10314
+ );
10315
+ }
10316
+ }
10019
10317
  printQaPreviewUrls(session.experimentId, previewResult);
10020
10318
  }
10021
10319
  );
@@ -10024,7 +10322,7 @@ var applyFilesystem = new Command("apply").description("Apply experiment filesys
10024
10322
  fail(this, error instanceof Error ? error.message : String(error));
10025
10323
  }
10026
10324
  });
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) {
10325
+ 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 action16(target) {
10028
10326
  const creds = requireCredentials();
10029
10327
  const spinner = await createSpinner("Fetching QA run...");
10030
10328
  try {
@@ -10048,15 +10346,13 @@ var qaStatus = new Command("status").description("Show or create the latest expe
10048
10346
  throw error;
10049
10347
  }
10050
10348
  });
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) {
10349
+ 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").addOption(
10350
+ qaChoice("--status <status>", "Checklist status", checklistStatuses).makeOptionMandatory()
10351
+ ).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) {
10352
+ validateQaChoice(this, "--status", opts.status, checklistStatuses);
10052
10353
  const creds = requireCredentials();
10053
10354
  const spinner = await createSpinner("Updating QA checklist...");
10054
10355
  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
10356
  const evidence = parseEvidenceInput(opts.evidence);
10061
10357
  const resolved = await resolveExperimentForFilesystemOrId(target);
10062
10358
  const current = await getExperimentQaRun(creds, resolved.experimentId);
@@ -10082,16 +10378,19 @@ var qaCheck = new Command("check").description("Update one QA checklist item wit
10082
10378
  resolved.folder,
10083
10379
  resolved.experimentId
10084
10380
  );
10085
- output(this, { run: run3 }, () => {
10381
+ const data = opts.summary ? qaSummary(run3, run3.checklist.find((item) => item.id === opts.item) ?? null) : { run: run3 };
10382
+ output(this, data, () => {
10086
10383
  spinner.succeed("Updated QA checklist");
10087
- printQaRun(run3);
10384
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
10385
+ `);
10386
+ else printQaRun(run3);
10088
10387
  });
10089
10388
  } catch (error) {
10090
10389
  spinner.fail("Failed to update QA checklist");
10091
10390
  throw error;
10092
10391
  }
10093
10392
  });
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) {
10393
+ 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("--summary", "Print a compact run and changed values").option("--json", "Print raw QA run JSON").action(async function action18(target, opts) {
10095
10394
  const creds = requireCredentials();
10096
10395
  const spinner = await createSpinner("Updating QA notes...");
10097
10396
  try {
@@ -10103,9 +10402,10 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10103
10402
  if (!current.ok || !current.data) {
10104
10403
  throw new Error(current.error ?? "Failed to fetch QA run");
10105
10404
  }
10106
- const body5 = opts.notes !== void 0 ? { runId: current.data.run.id, runNotes: opts.notes } : {
10405
+ const body5 = {
10107
10406
  runId: current.data.run.id,
10108
- acceptanceCriteriaSummary: opts.acceptance ?? null
10407
+ ...opts.notes !== void 0 ? { runNotes: opts.notes } : {},
10408
+ ...opts.acceptance !== void 0 ? { acceptanceCriteriaSummary: opts.acceptance } : {}
10109
10409
  };
10110
10410
  const res = await updateExperimentQaRun(creds, resolved.experimentId, body5);
10111
10411
  if (!res.ok || !res.data)
@@ -10120,20 +10420,28 @@ var qaNotes = new Command("notes").description("Update QA run notes or acceptanc
10120
10420
  resolved.folder,
10121
10421
  resolved.experimentId
10122
10422
  );
10123
- output(this, { run: run3 }, () => {
10423
+ const data = opts.summary ? qaSummary(run3, {
10424
+ notes: run3.notes,
10425
+ acceptanceCriteriaSummary: run3.acceptanceCriteriaSummary
10426
+ }) : { run: run3 };
10427
+ output(this, data, () => {
10124
10428
  spinner.succeed("Updated QA notes");
10125
- printQaRun(run3);
10429
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
10430
+ `);
10431
+ else printQaRun(run3);
10126
10432
  });
10127
10433
  } catch (error) {
10128
10434
  spinner.fail("Failed to update QA notes");
10129
10435
  throw error;
10130
10436
  }
10131
10437
  });
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) {
10438
+ var qaApprove = new Command("approve").description("Approve QA and move the experiment to ready_to_launch; qa reopen returns it to QA").argument("[folder-or-id]", "Experiment filesystem folder, ID, or slug", ".").action(async function action19(target) {
10133
10439
  const creds = requireCredentials();
10134
10440
  const spinner = await createSpinner("Approving QA...");
10135
10441
  try {
10136
10442
  const resolved = await resolveExperimentForFilesystemOrId(target);
10443
+ const before = await resolveExperiment(resolved.experimentId);
10444
+ if (!before) fail(this, "Experiment not found.", EXIT_CODES.validation);
10137
10445
  const res = await approveExperimentQa(creds, resolved.experimentId);
10138
10446
  if (!res.ok)
10139
10447
  throw new CliError(
@@ -10146,17 +10454,31 @@ var qaApprove = new Command("approve").description("Mark QA done after the check
10146
10454
  resolved.folder,
10147
10455
  resolved.experimentId
10148
10456
  );
10457
+ if (!res.data?.status) fail(this, "QA approval response is missing experiment status.");
10458
+ const transition = {
10459
+ from: before.status,
10460
+ to: res.data.status,
10461
+ reopen: `apex experiments qa reopen ${resolved.experimentId}`
10462
+ };
10149
10463
  output(
10150
10464
  this,
10151
- { experimentId: resolved.experimentId, qaStatus: "done" },
10152
- () => spinner.succeed(`QA marked done for ${resolved.label}`)
10465
+ {
10466
+ experimentId: resolved.experimentId,
10467
+ qaStatus: "done",
10468
+ experiment_status_before: before.status,
10469
+ experiment_status_after: res.data.status,
10470
+ transition
10471
+ },
10472
+ () => spinner.succeed(
10473
+ `QA approved: ${transition.from} \u2192 ${transition.to}; reopen: ${transition.reopen}`
10474
+ )
10153
10475
  );
10154
10476
  } catch (error) {
10155
10477
  spinner.fail("Failed to approve QA");
10156
10478
  throw error;
10157
10479
  }
10158
10480
  });
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) {
10481
+ 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 action20(target) {
10160
10482
  const creds = requireCredentials();
10161
10483
  const spinner = await createSpinner("Reopening QA...");
10162
10484
  try {
@@ -10214,7 +10536,9 @@ function resolveEvidenceVariation(experiment, ref) {
10214
10536
  }
10215
10537
  throw new Error(`Unknown --variation value: ${ref}`);
10216
10538
  }
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) {
10539
+ 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").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) {
10540
+ validateQaChoice(this, "--status", opts.status, evidenceStatuses);
10541
+ validateQaChoice(this, "--kind", opts.kind, evidenceKinds);
10218
10542
  const modes = Number(Boolean(opts.job)) + Number(opts.file.length > 0) + Number(Boolean(opts.records));
10219
10543
  if (modes !== 1) {
10220
10544
  fail(this, "Provide exactly one of --job, --file, or --records.", EXIT_CODES.validation);
@@ -10223,6 +10547,17 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
10223
10547
  const spinner = await createSpinner("Writing QA evidence...");
10224
10548
  try {
10225
10549
  const resolved = await resolveExperimentForFilesystemOrId(target);
10550
+ const previousIds = /* @__PURE__ */ new Set();
10551
+ if (opts.summary) {
10552
+ const current = await getExperimentQaRun(creds, resolved.experimentId);
10553
+ if (!current.ok || !current.data)
10554
+ throw new CliError(
10555
+ current.error ?? "Failed to fetch QA run",
10556
+ classifyStatus(current.status),
10557
+ current.errorDetails
10558
+ );
10559
+ for (const record2 of current.data.run.evidenceRecords) previousIds.add(record2.id);
10560
+ }
10226
10561
  let response;
10227
10562
  if (opts.job) {
10228
10563
  response = await attachExperimentQaEvidence(creds, resolved.experimentId, {
@@ -10263,7 +10598,16 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
10263
10598
  );
10264
10599
  }
10265
10600
  spinner.stop();
10266
- output(this, { run: response.data.run }, () => printQaRun(response.data.run));
10601
+ const run3 = response.data.run;
10602
+ const data = opts.summary ? qaSummary(
10603
+ run3,
10604
+ run3.evidenceRecords.filter((record2) => !previousIds.has(record2.id))
10605
+ ) : { run: run3 };
10606
+ output(this, data, () => {
10607
+ if (opts.summary) process.stdout.write(`${JSON.stringify(data, null, 2)}
10608
+ `);
10609
+ else printQaRun(run3);
10610
+ });
10267
10611
  } catch (error) {
10268
10612
  spinner.fail("Failed to write QA evidence");
10269
10613
  throw error;
@@ -10279,102 +10623,255 @@ var SCREENSHOT_DEVICES = [
10279
10623
  ];
10280
10624
  var safeFilenamePart = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
10281
10625
  var commaList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
10626
+ function shellQuote(value) {
10627
+ if (value.length > 0 && /^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
10628
+ return `'${value.replaceAll("'", "'\\''")}'`;
10629
+ }
10630
+ function isTransientResumeStatus(status2) {
10631
+ return status2 === 429 || status2 !== void 0 && status2 >= 500;
10632
+ }
10633
+ function screenshotTimeoutMessage(job, experimentRef, out, seconds, flags = {}) {
10634
+ const progress = job.progress ? ` Last progress: ${job.progress.captured}/${job.progress.total} captured.` : "";
10635
+ const carried = [
10636
+ flags.attachQa === false ? " --no-attach-qa" : "",
10637
+ flags.labelPairs === false ? " --no-label-pairs" : "",
10638
+ flags.json ? " --json" : ""
10639
+ ].join("");
10640
+ const resume = `apex experiments screenshots ${shellQuote(experimentRef)} --job ${shellQuote(job.job_id)} --out ${shellQuote(out)}${carried}`;
10641
+ return `Screenshot job ${job.job_id} timed out after ${seconds}s.${progress} Resume with: ${resume}`;
10642
+ }
10282
10643
  var screenshots = new Command("screenshots").description(
10283
10644
  "Force-assign every capture, including control, to its variation server-side for deterministic screenshots"
10284
10645
  ).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(
10285
10646
  "--variations <list>",
10286
10647
  "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(
10648
+ ).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
10649
  "--timeout <seconds>",
10289
10650
  "Maximum wait for completion; finished results are always downloaded",
10290
10651
  "300"
10291
- ).action(async function action19(experimentRef, opts) {
10652
+ ).action(async function action22(experimentRef, opts) {
10292
10653
  if (opts.url)
10293
10654
  fail(this, "--url is not supported by the current screenshot v1 API.", EXIT_CODES.validation);
10294
10655
  const timeoutSeconds = Number(opts.timeout);
10656
+ const explicitJson = process.argv.includes("--json");
10295
10657
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
10296
10658
  fail(this, "--timeout must be a positive number of seconds.", EXIT_CODES.validation);
10297
10659
  const experiment = await resolveExperiment(experimentRef);
10298
10660
  if (!experiment?.variations?.length)
10299
10661
  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}` : ""})`
10662
+ const creds = requireCredentials();
10663
+ let devices;
10664
+ let variationIds;
10665
+ let job;
10666
+ let deadlineStart = null;
10667
+ if (opts.job) {
10668
+ const ignoredFlags = [
10669
+ opts.devices ? "--devices" : null,
10670
+ opts.variations ? "--variations" : null
10671
+ ].filter((flag) => flag !== null);
10672
+ if (ignoredFlags.length)
10673
+ console.error(
10674
+ source_default.yellow(
10675
+ `Warning: ${ignoredFlags.join(" and ")} ${ignoredFlags.length > 1 ? "are" : "is"} ignored with --job; screenshot job ${opts.job} keeps its stored devices and variations.`
10676
+ )
10677
+ );
10678
+ deadlineStart = screenshotNow();
10679
+ const resumeDeadline = deadlineStart + timeoutSeconds * 1e3;
10680
+ let existing;
10681
+ let resumeDelayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
10682
+ for (let attempt = 1; ; attempt += 1) {
10683
+ try {
10684
+ existing = await getScreenshotJob(
10685
+ creds,
10686
+ opts.job,
10687
+ Math.max(1, resumeDeadline - screenshotNow())
10688
+ );
10689
+ } catch (error) {
10690
+ if (attempt > 3 || screenshotNow() >= resumeDeadline) throw error;
10691
+ await new Promise(
10692
+ (resolve2) => setTimeout(
10693
+ resolve2,
10694
+ Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
10695
+ )
10696
+ );
10697
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
10698
+ continue;
10699
+ }
10700
+ if (!existing.ok && isTransientResumeStatus(existing.status) && attempt <= 3 && screenshotNow() < resumeDeadline) {
10701
+ await new Promise(
10702
+ (resolve2) => setTimeout(
10703
+ resolve2,
10704
+ Math.min(resumeDelayMs, Math.max(0, resumeDeadline - screenshotNow()))
10705
+ )
10706
+ );
10707
+ resumeDelayMs = Math.min(Math.round(resumeDelayMs * 1.6), 1e4);
10708
+ continue;
10709
+ }
10710
+ break;
10711
+ }
10712
+ if (!existing || !existing.ok || !existing.data)
10713
+ fail(
10714
+ this,
10715
+ existing?.error ?? "Failed to resume screenshot job",
10716
+ classifyStatus(existing?.status),
10717
+ existing?.errorDetails
10718
+ );
10719
+ job = existing.data;
10720
+ if (!job.experiment_id)
10721
+ fail(
10722
+ this,
10723
+ `Screenshot job ${job.job_id} names no experiment; refusing to resume it under ${experimentRef} (${experiment.id}).`,
10724
+ EXIT_CODES.validation
10725
+ );
10726
+ if (job.experiment_id !== experiment.id)
10727
+ fail(
10728
+ this,
10729
+ `Screenshot job ${job.job_id} belongs to experiment ${job.experiment_id}, not ${experimentRef} (${experiment.id}).`,
10730
+ EXIT_CODES.validation
10731
+ );
10732
+ const storedDevices = job.devices ?? [];
10733
+ for (const device of storedDevices.filter((device2) => !SCREENSHOT_DEVICES.includes(device2)))
10734
+ console.error(
10735
+ source_default.yellow(
10736
+ `Warning: screenshot job ${job.job_id} names device ${device}, which this CLI does not know; skipping it.`
10737
+ )
10738
+ );
10739
+ devices = storedDevices.filter((device) => SCREENSHOT_DEVICES.includes(device));
10740
+ if (!devices.length)
10741
+ fail(
10742
+ this,
10743
+ 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.`,
10744
+ EXIT_CODES.validation
10745
+ );
10746
+ if (Array.isArray(job.results))
10747
+ fail(
10748
+ this,
10749
+ `Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
10750
+ EXIT_CODES.validation
10751
+ );
10752
+ const knownVariationIds = new Set(experiment.variations.map((variation) => variation.id));
10753
+ const storedVariationIds = job.variation_ids ?? [];
10754
+ for (const id of storedVariationIds.filter((id2) => !knownVariationIds.has(id2)))
10755
+ console.error(
10756
+ source_default.yellow(
10757
+ `Warning: screenshot job ${job.job_id} names variation ${id}, which is not part of this experiment; skipping it.`
10758
+ )
10325
10759
  );
10760
+ variationIds = storedVariationIds.filter((id) => knownVariationIds.has(id));
10761
+ if (!variationIds.length)
10326
10762
  fail(
10327
10763
  this,
10328
- `Ambiguous --variations value "${ref}" matches: ${matches.join("; ")}. Use a variation ID or slug.`,
10764
+ `Screenshot job ${job.job_id} names no variation that belongs to experiment ${experimentRef}.`,
10329
10765
  EXIT_CODES.validation
10330
10766
  );
10767
+ } else {
10768
+ devices = opts.devices ? commaList(opts.devices) : SCREENSHOT_DEVICES;
10769
+ if (!devices.length)
10770
+ fail(this, "--devices must contain at least one device.", EXIT_CODES.validation);
10771
+ const unknownDevices = devices.filter((device) => !SCREENSHOT_DEVICES.includes(device));
10772
+ if (unknownDevices.length)
10773
+ fail(
10774
+ this,
10775
+ `Unknown screenshot device(s): ${unknownDevices.join(", ")}`,
10776
+ EXIT_CODES.validation
10777
+ );
10778
+ const requested = opts.variations ? commaList(opts.variations) : [];
10779
+ const control = experiment.variations.find((variation) => variation.is_control);
10780
+ if (!control) fail(this, "Experiment has no control variation.", EXIT_CODES.validation);
10781
+ const matchedById = /* @__PURE__ */ new Map();
10782
+ const unmatched = [];
10783
+ for (const ref of requested) {
10784
+ const exactMatch = experiment.variations.find(
10785
+ (candidate) => ref === candidate.id || ref === candidate.slug
10786
+ );
10787
+ const nameMatches = exactMatch ? [] : experiment.variations.filter(
10788
+ (candidate) => ref.toLowerCase() === candidate.name.toLowerCase()
10789
+ );
10790
+ if (nameMatches.length > 1) {
10791
+ const matches = nameMatches.map(
10792
+ (candidate) => `${candidate.name} (ID: ${candidate.id}${candidate.slug ? `, slug: ${candidate.slug}` : ""})`
10793
+ );
10794
+ fail(
10795
+ this,
10796
+ `Ambiguous --variations value "${ref}" matches: ${matches.join("; ")}. Use a variation ID or slug.`,
10797
+ EXIT_CODES.validation
10798
+ );
10799
+ }
10800
+ const variation = exactMatch ?? nameMatches[0];
10801
+ if (variation) matchedById.set(variation.id, variation);
10802
+ else unmatched.push(ref);
10331
10803
  }
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;
10804
+ if (unmatched.length)
10805
+ fail(this, `Unknown --variations value(s): ${unmatched.join(", ")}`, EXIT_CODES.validation);
10806
+ const selected2 = requested.length ? [...matchedById.values()] : experiment.variations;
10807
+ variationIds = [.../* @__PURE__ */ new Set([control.id, ...selected2.map((variation) => variation.id)])];
10808
+ const created = await createExperimentScreenshots(creds, experiment.id, {
10809
+ devices,
10810
+ variation_ids: variationIds
10811
+ });
10812
+ if (!created.ok || !created.data)
10813
+ fail(
10814
+ this,
10815
+ created.error ?? "Failed to create screenshot job",
10816
+ classifyStatus(created.status),
10817
+ created.errorDetails
10818
+ );
10819
+ job = created.data;
10820
+ }
10821
+ const deadline = (deadlineStart ?? screenshotNow()) + timeoutSeconds * 1e3;
10354
10822
  let delayMs = cliTestSetting("API") === "1" ? 1 : 1e3;
10355
10823
  let consecutiveTransientFailures = 0;
10356
10824
  while (job.status === "queued" || job.status === "processing") {
10357
10825
  if (screenshotNow() >= deadline)
10358
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10826
+ fail(
10827
+ this,
10828
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
10829
+ labelPairs: opts.labelPairs,
10830
+ json: explicitJson,
10831
+ attachQa: opts.attachQa
10832
+ })
10833
+ );
10359
10834
  await new Promise(
10360
10835
  (resolve2) => setTimeout(resolve2, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
10361
10836
  );
10362
10837
  if (screenshotNow() >= deadline)
10363
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10838
+ fail(
10839
+ this,
10840
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
10841
+ labelPairs: opts.labelPairs,
10842
+ json: explicitJson,
10843
+ attachQa: opts.attachQa
10844
+ })
10845
+ );
10364
10846
  let polled;
10365
10847
  try {
10366
10848
  polled = await getScreenshotJob(creds, job.job_id, Math.max(1, deadline - screenshotNow()));
10367
10849
  } catch (error) {
10368
10850
  if (screenshotNow() >= deadline)
10369
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10851
+ fail(
10852
+ this,
10853
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
10854
+ labelPairs: opts.labelPairs,
10855
+ json: explicitJson,
10856
+ attachQa: opts.attachQa
10857
+ })
10858
+ );
10370
10859
  consecutiveTransientFailures += 1;
10371
10860
  if (consecutiveTransientFailures > 3) throw error;
10372
10861
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10373
10862
  continue;
10374
10863
  }
10375
10864
  const polledToTerminalState = polled.ok && polled.data && polled.data.status !== "queued" && polled.data.status !== "processing";
10865
+ if (polled.ok && polled.data) job = polled.data;
10376
10866
  if (!polledToTerminalState && screenshotNow() >= deadline) {
10377
- fail(this, `Screenshot job ${job.job_id} timed out after ${timeoutSeconds}s.`);
10867
+ fail(
10868
+ this,
10869
+ screenshotTimeoutMessage(job, experimentRef, opts.out, timeoutSeconds, {
10870
+ labelPairs: opts.labelPairs,
10871
+ json: explicitJson,
10872
+ attachQa: opts.attachQa
10873
+ })
10874
+ );
10378
10875
  }
10379
10876
  if (!polled.ok || !polled.data) {
10380
10877
  const transient = polled.status === 429 || polled.status >= 500;
@@ -10390,7 +10887,6 @@ var screenshots = new Command("screenshots").description(
10390
10887
  continue;
10391
10888
  }
10392
10889
  consecutiveTransientFailures = 0;
10393
- job = polled.data;
10394
10890
  delayMs = Math.min(Math.round(delayMs * 1.6), 1e4);
10395
10891
  }
10396
10892
  if (opts.attachQa !== false) {
@@ -10421,16 +10917,32 @@ var screenshots = new Command("screenshots").description(
10421
10917
  const rows = [];
10422
10918
  const errors = [];
10423
10919
  const usedFilenames = /* @__PURE__ */ new Set();
10920
+ const hasControlVariation = experiment.variations.some((item) => item.is_control);
10424
10921
  const downloads = [];
10922
+ if (Array.isArray(job.results))
10923
+ fail(
10924
+ this,
10925
+ `Screenshot job ${job.job_id} stores its results in a legacy shape this CLI cannot download; create a new job instead of resuming it.`,
10926
+ EXIT_CODES.validation
10927
+ );
10425
10928
  for (const variationId of variationIds) {
10426
10929
  const variation = experiment.variations.find((item) => item.id === variationId);
10930
+ if (!variation) {
10931
+ console.error(
10932
+ source_default.yellow(
10933
+ `Warning: variation ${variationId} is not part of this experiment; skipping it.`
10934
+ )
10935
+ );
10936
+ continue;
10937
+ }
10427
10938
  for (const device of devices) {
10428
10939
  const url = job.results?.[variationId]?.[device];
10429
10940
  if (!url) {
10430
10941
  downloads.push({ variation, device, url, filename: "" });
10431
10942
  continue;
10432
10943
  }
10433
- const baseFilename = `${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
10944
+ const pairPrefix = opts.labelPairs === false || !hasControlVariation ? "" : variation.is_control ? "before-" : "after-";
10945
+ const baseFilename = `${pairPrefix}${safeFilenamePart(experiment.slug ?? experiment.name)}-${safeFilenamePart(variation.slug ?? variation.name)}-${safeFilenamePart(device)}`;
10434
10946
  let filename = `${baseFilename}.png`;
10435
10947
  if (usedFilenames.has(filename)) {
10436
10948
  const suffix = variation.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
@@ -10487,7 +10999,7 @@ var screenshots = new Command("screenshots").description(
10487
10999
  printTable(["VARIATION", "DEVICE", "FILE"], rows, [24, 20, 48]);
10488
11000
  });
10489
11001
  });
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(
11002
+ 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
11003
  "after",
10492
11004
  `
10493
11005
  Examples:
@@ -10581,10 +11093,10 @@ async function importConfigModule(absPath) {
10581
11093
  }
10582
11094
  throw new Error(`Unsupported config extension "${ext}". Use .drip.config.ts or .drip.config.js.`);
10583
11095
  }
10584
- function isRecord(value) {
11096
+ function isRecord2(value) {
10585
11097
  return !!value && typeof value === "object" && !Array.isArray(value);
10586
11098
  }
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) {
11099
+ 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
11100
  const creds = requireCredentials();
10589
11101
  const configPath = opts.config ?? findConfigPath();
10590
11102
  if (!configPath) {
@@ -10599,12 +11111,12 @@ var pushCommand = new Command("push").description("Push experiment definitions f
10599
11111
  let experiments;
10600
11112
  try {
10601
11113
  const mod = await importConfigModule(absoluteConfigPath);
10602
- const exported = isRecord(mod) && "default" in mod ? mod.default : mod;
11114
+ const exported = isRecord2(mod) && "default" in mod ? mod.default : mod;
10603
11115
  if (Array.isArray(exported)) {
10604
11116
  experiments = exported;
10605
- } else if (isRecord(exported) && Array.isArray(exported.experiments)) {
11117
+ } else if (isRecord2(exported) && Array.isArray(exported.experiments)) {
10606
11118
  experiments = exported.experiments;
10607
- } else if (isRecord(exported) && typeof exported.slug === "string" && Array.isArray(exported.variations)) {
11119
+ } else if (isRecord2(exported) && typeof exported.slug === "string" && Array.isArray(exported.variations)) {
10608
11120
  experiments = [exported];
10609
11121
  } else {
10610
11122
  spinner?.fail("Invalid config format");
@@ -11028,7 +11540,7 @@ async function createSession(folder) {
11028
11540
  var harnessCommand = new Command("harness").description(
11029
11541
  "Develop AB Test Harness projects through Apex live QA sessions"
11030
11542
  );
11031
- harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action21(folderArg, opts) {
11543
+ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment folder").option("--force", "Overwrite existing apex.harness.json").action(function action24(folderArg, opts) {
11032
11544
  const folder = resolveFolder(folderArg);
11033
11545
  const targetPath = apexConfigPath(folder);
11034
11546
  if (import_node_fs9.default.existsSync(targetPath) && !opts.force) {
@@ -11048,7 +11560,7 @@ harnessCommand.command("init").argument("<folder>", "AB Test Harness experiment
11048
11560
  () => printSuccess(`Created ${targetPath}`)
11049
11561
  );
11050
11562
  });
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) {
11563
+ 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
11564
  const folder = resolveFolder(folderArg);
11053
11565
  const created = await createSession(folder);
11054
11566
  await runAbHarness(["build", folder], folder);
@@ -11115,7 +11627,7 @@ harnessCommand.command("dev").argument("<folder>", "AB Test Harness experiment f
11115
11627
  process.exitCode = 0;
11116
11628
  });
11117
11629
  });
11118
- harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action23(folderArg) {
11630
+ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experiment folder").action(async function action26(folderArg) {
11119
11631
  const folder = resolveFolder(folderArg);
11120
11632
  const created = await createSession(folder);
11121
11633
  await runAbHarness(["verify", folder], folder);
@@ -11490,7 +12002,7 @@ Examples:
11490
12002
  apex workspace publish ./workspace
11491
12003
  `
11492
12004
  );
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) {
12005
+ 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
12006
  try {
11495
12007
  const creds = requireCredentials();
11496
12008
  const expRes = await getExperiment(creds, experimentId);
@@ -11540,7 +12052,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
11540
12052
  throw error;
11541
12053
  }
11542
12054
  });
11543
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action25(folderArg) {
12055
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action28(folderArg) {
11544
12056
  try {
11545
12057
  const folder = folderPath2(folderArg);
11546
12058
  const session = readSession(folder);
@@ -11572,7 +12084,7 @@ operatorCommand.command("status").argument("[folder]", "Operator workspace folde
11572
12084
  fail(this, error instanceof Error ? error.message : String(error));
11573
12085
  }
11574
12086
  });
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) {
12087
+ 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
12088
  try {
11577
12089
  const folder = folderPath2(folderArg);
11578
12090
  const session = readSession(folder);
@@ -11651,7 +12163,7 @@ operatorCommand.command("dev").argument("[folder]", "Operator workspace folder",
11651
12163
  throw error;
11652
12164
  }
11653
12165
  });
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) {
12166
+ 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
12167
  try {
11656
12168
  const result = await runPublish(folderArg, opts);
11657
12169
  const build = result.build;
@@ -11695,7 +12207,7 @@ function briefLine(item) {
11695
12207
  ].filter(Boolean);
11696
12208
  return parts.join("\n");
11697
12209
  }
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) {
12210
+ 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
12211
  const creds = requireCredentials();
11700
12212
  const limit = parseInt(opts.limit, 10);
11701
12213
  const hasExperiment = opts.unbuilt ? false : opts.built ? true : void 0;
@@ -11749,7 +12261,7 @@ More results: --cursor ${res.data.pagination.next_cursor}`));
11749
12261
  }
11750
12262
  );
11751
12263
  });
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) {
12264
+ 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
12265
  if (opts.output !== void 0) {
11754
12266
  process.stderr.write(
11755
12267
  `Warning: --output is deprecated for apex roadmap handoff; use --out instead. --output will be removed in v${LEGACY_OUTPUT_REMOVAL_VERSION}.
@@ -11968,7 +12480,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
11968
12480
  console.log(`${index + 1}. ${check}`);
11969
12481
  });
11970
12482
  });
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) {
12483
+ 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
12484
  const creds = requireCredentials();
11973
12485
  const res = await verifyTracking(creds, {
11974
12486
  minutes: Number.parseInt(opts.minutes, 10),
@@ -12225,7 +12737,7 @@ function printGoal(goal) {
12225
12737
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
12226
12738
  console.log();
12227
12739
  }
12228
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action31(opts) {
12740
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action34(opts) {
12229
12741
  const creds = requireCredentials();
12230
12742
  const res = await listGoals(creds, { limit: Number.parseInt(opts.limit, 10) });
12231
12743
  if (!res.ok || !res.data) {
@@ -12249,7 +12761,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
12249
12761
  );
12250
12762
  });
12251
12763
  });
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) {
12764
+ 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
12765
  const creds = requireCredentials();
12254
12766
  const body5 = cleanBody({
12255
12767
  name: opts.name,
@@ -12275,10 +12787,10 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
12275
12787
  }
12276
12788
  output(this, { goal: res.data }, () => printGoal(res.data));
12277
12789
  });
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(
12790
+ 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
12791
  "--value-property <property>",
12280
12792
  "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) {
12793
+ ).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
12794
  const creds = requireCredentials();
12283
12795
  const body5 = cleanBody({
12284
12796
  name: opts.name,
@@ -12310,7 +12822,7 @@ var update = new Command("update").description("Update a goal").argument("<id>",
12310
12822
  var test = new Command("test").description("Dry run a goal URL pattern against traffic").option(
12311
12823
  "--pattern <regex>",
12312
12824
  "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) {
12825
+ ).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
12826
  if (!opts.pattern && !opts.goal)
12315
12827
  fail(this, "Provide --pattern or --goal.", EXIT_CODES.validation);
12316
12828
  if (opts.pattern && opts.goal)
@@ -12392,7 +12904,7 @@ var test = new Command("test").description("Dry run a goal URL pattern against t
12392
12904
  }
12393
12905
  );
12394
12906
  });
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) {
12907
+ 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
12908
  const creds = requireCredentials();
12397
12909
  const res = await suggestGoals(creds);
12398
12910
  if (!res.ok || !res.data)
@@ -12440,7 +12952,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
12440
12952
  );
12441
12953
  }
12442
12954
  });
12443
- var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update).addCommand(test).addCommand(suggest).addHelpText(
12955
+ var goalsCommand = new Command("goals").description("Manage conversion and revenue goals").addCommand(list2).addCommand(create2).addCommand(update2).addCommand(test).addCommand(suggest).addHelpText(
12444
12956
  "after",
12445
12957
  `
12446
12958
  Examples:
@@ -12482,7 +12994,7 @@ function printFunnel(funnel) {
12482
12994
  console.log(` ${step.order}. ${step.label} \u2014 ${step.url_pattern}`);
12483
12995
  console.log();
12484
12996
  }
12485
- var list3 = new Command("list").alias("ls").description("List funnels").action(async function action36() {
12997
+ var list3 = new Command("list").alias("ls").description("List funnels").action(async function action39() {
12486
12998
  const res = await listFunnels(requireCredentials());
12487
12999
  if (!res.ok || !res.data)
12488
13000
  fail(
@@ -12501,7 +13013,7 @@ var list3 = new Command("list").alias("ls").description("List funnels").action(a
12501
13013
  )
12502
13014
  );
12503
13015
  });
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) {
13016
+ 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
13017
  const res = await createFunnel(requireCredentials(), {
12506
13018
  name: opts.name,
12507
13019
  steps: readSteps(this, opts.stepsFile)
@@ -12515,7 +13027,7 @@ var create3 = new Command("create").description("Create a funnel").requiredOptio
12515
13027
  );
12516
13028
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12517
13029
  });
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) {
13030
+ 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
13031
  const res = await updateFunnel(requireCredentials(), id, {
12520
13032
  steps: readSteps(this, opts.stepsFile)
12521
13033
  });
@@ -12528,7 +13040,7 @@ var update2 = new Command("update").description("Replace a funnel's steps").argu
12528
13040
  );
12529
13041
  output(this, { funnel: res.data }, () => printFunnel(res.data));
12530
13042
  });
12531
- var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action39(id) {
13043
+ var del2 = new Command("delete").description("Delete a funnel").argument("<id>", "Funnel ID").action(async function action42(id) {
12532
13044
  const res = await deleteFunnel(requireCredentials(), id);
12533
13045
  if (!res.ok)
12534
13046
  fail(
@@ -12539,7 +13051,7 @@ var del2 = new Command("delete").description("Delete a funnel").argument("<id>",
12539
13051
  );
12540
13052
  output(this, { deleted: true, id }, () => console.log(`Deleted funnel ${id}`));
12541
13053
  });
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) {
13054
+ 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
13055
  if (!/^[1-9]\d*$/.test(opts.days))
12544
13056
  fail(this, "--days must be a positive integer", EXIT_CODES.validation);
12545
13057
  const days = Number.parseInt(opts.days, 10);
@@ -12563,11 +13075,11 @@ var run = new Command("run").description("Run funnel analysis").argument("<id>",
12563
13075
  )
12564
13076
  );
12565
13077
  });
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) {
13078
+ 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
13079
  const steps = readSteps(this, opts.stepsFile);
12568
13080
  output(this, { valid: true, steps }, () => console.log(`Valid funnel steps: ${steps.length}`));
12569
13081
  });
12570
- var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update2).addCommand(del2).addCommand(run).addCommand(validate2);
13082
+ var funnelsCommand = new Command("funnels").description("Manage analytics funnels").addCommand(list3).addCommand(create3).addCommand(update3).addCommand(del2).addCommand(run).addCommand(validate2);
12571
13083
 
12572
13084
  // src/commands/environments.ts
12573
13085
  init_source();
@@ -12624,11 +13136,11 @@ async function load2(command) {
12624
13136
  }
12625
13137
  return result.data;
12626
13138
  }
12627
- var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action42() {
13139
+ var list4 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action45() {
12628
13140
  const { environments } = await load2(this);
12629
13141
  output(this, { environments }, () => printEnvironments(environments.items));
12630
13142
  });
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) {
13143
+ 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
13144
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12633
13145
  if (!normalizedDomain) {
12634
13146
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12675,7 +13187,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
12675
13187
  () => printEnvironments(result.data.environments.items)
12676
13188
  );
12677
13189
  });
12678
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action44(domain) {
13190
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action47(domain) {
12679
13191
  const normalizedDomain = normalizeEnvironmentDomain(domain);
12680
13192
  if (!normalizedDomain) {
12681
13193
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -12739,11 +13251,11 @@ function printConsent(data) {
12739
13251
  `Split: ${data.prerequisites.showVariationsRule && data.prerequisites.startTrackingRule ? "canonical" : "off"}`
12740
13252
  );
12741
13253
  }
12742
- var get2 = new Command("get").description("Show consent settings").action(async function action45() {
13254
+ var get2 = new Command("get").description("Show consent settings").action(async function action48() {
12743
13255
  const data = await load3(this);
12744
13256
  output(this, data, () => printConsent(data));
12745
13257
  });
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) {
13258
+ 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
13259
  const consentMode = {
12748
13260
  enabled: true,
12749
13261
  provider: opts.provider,
@@ -12764,7 +13276,7 @@ var set = new Command("set").description("Enable and configure consent gating").
12764
13276
  warn(this, result.data.precompile);
12765
13277
  output(this, result.data, () => printConsent(result.data));
12766
13278
  });
12767
- var off = new Command("off").description("Disable consent gating").action(async function action47() {
13279
+ var off = new Command("off").description("Disable consent gating").action(async function action50() {
12768
13280
  const result = await updateConsent(requireCredentials(), { consentMode: { enabled: false } });
12769
13281
  if (!result.ok || !result.data)
12770
13282
  fail(
@@ -13078,8 +13590,8 @@ function score2(path24, patterns) {
13078
13590
  0
13079
13591
  );
13080
13592
  }
13081
- function best(fields2, patterns, numeric2 = false) {
13082
- const ranked = fields2.filter(
13593
+ function best(fields3, patterns, numeric2 = false) {
13594
+ const ranked = fields3.filter(
13083
13595
  (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
13084
13596
  ).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
13597
  if (!ranked.length) return [];
@@ -13104,15 +13616,15 @@ function proposeTriggers(report) {
13104
13616
  continue;
13105
13617
  const descriptors = candidate.fields ?? candidate.paths;
13106
13618
  const payload = candidate.properties ?? candidate.payload ?? candidate;
13107
- const fields2 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
13619
+ const fields3 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
13108
13620
  const revenues = best(
13109
- fields2,
13621
+ fields3,
13110
13622
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
13111
13623
  true
13112
13624
  );
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)$/]);
13625
+ const orderIds = best(fields3, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
13626
+ const currencies = best(fields3, [/currency/, /(^|\.)cur(rency)?$/]);
13627
+ const items = best(fields3, [/(^|\.)((line_?)?items|products)$/]);
13116
13628
  if (!revenues.length || !orderIds.length || !currencies.length) continue;
13117
13629
  for (const revenue of revenues)
13118
13630
  for (const orderId of orderIds)
@@ -13662,11 +14174,11 @@ async function patchWithRetry(command, build) {
13662
14174
  warn2(command, result.data.precompile);
13663
14175
  return result.data;
13664
14176
  }
13665
- var list5 = new Command("list").alias("ls").description("List install domains").action(async function action48() {
14177
+ var list5 = new Command("list").alias("ls").description("List install domains").action(async function action51() {
13666
14178
  const data = await load4(this);
13667
14179
  output(this, data, () => printDomains(data));
13668
14180
  });
13669
- var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action49(domain) {
14181
+ var add = new Command("add").description("Add install domains").argument("<domain...>").action(async function action52(domain) {
13670
14182
  const added = domain.map(normalizeDomain);
13671
14183
  const data = await patchWithRetry(
13672
14184
  this,
@@ -13674,7 +14186,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
13674
14186
  );
13675
14187
  output(this, data, () => printDomains(data));
13676
14188
  });
13677
- var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action50(domain) {
14189
+ var remove = new Command("remove").description("Remove install domains").argument("<domain...>").action(async function action53(domain) {
13678
14190
  const removeSet = new Set(domain.map(normalizeDomain));
13679
14191
  const data = await patchWithRetry(
13680
14192
  this,
@@ -13682,7 +14194,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
13682
14194
  );
13683
14195
  output(this, data, () => printDomains(data));
13684
14196
  });
13685
- var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action51(domain) {
14197
+ var set3 = new Command("set").description("Replace install domains").argument("<domain...>").action(async function action54(domain) {
13686
14198
  const replacement = domain.map(normalizeDomain);
13687
14199
  const data = await patchWithRetry(this, () => replacement);
13688
14200
  output(this, data, () => printDomains(data));
@@ -13774,14 +14286,14 @@ async function patchWithRetry2(command, build) {
13774
14286
  );
13775
14287
  return result.data;
13776
14288
  }
13777
- var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action52() {
14289
+ var get4 = new Command("get").description("Show cross-domain handover settings").action(async function action55() {
13778
14290
  const data = await load5(this);
13779
14291
  output(this, data, () => printSettings(data.externalTracking));
13780
14292
  });
13781
14293
  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
14294
  "--signed",
13783
14295
  "Require signed handover tokens (the default; unsigning is a platform-support action)"
13784
- ).action(async function action53(opts) {
14296
+ ).action(async function action56(opts) {
13785
14297
  const domains = opts.domain.map(normalizeDomain2);
13786
14298
  if (domains.some((domain) => !domain))
13787
14299
  fail(
@@ -13801,14 +14313,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
13801
14313
  }));
13802
14314
  output(this, data, () => printSettings(data.externalTracking));
13803
14315
  });
13804
- var off2 = new Command("off").description("Disable cross-domain handover").action(async function action54() {
14316
+ var off2 = new Command("off").description("Disable cross-domain handover").action(async function action57() {
13805
14317
  const data = await patchWithRetry2(this, (current) => ({
13806
14318
  externalTracking: { domains: [], signed: true },
13807
14319
  ifRevision: current.revision
13808
14320
  }));
13809
14321
  output(this, data, () => printSettings(data.externalTracking));
13810
14322
  });
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) {
14323
+ 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
14324
  const config = await load5(this);
13813
14325
  const target = normalizeDomain2(opts.toDomain);
13814
14326
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
@@ -14851,7 +15363,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
14851
15363
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
14852
15364
  "--worker <url>",
14853
15365
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
14854
- ).action(async function action56(options) {
15366
+ ).action(async function action59(options) {
14855
15367
  const data = await runDoctor(options);
14856
15368
  if (options.check && data.checks.length === 0)
14857
15369
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
@@ -14873,7 +15385,7 @@ function printDoctorReport(checks) {
14873
15385
  var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
14874
15386
  "--since <date>",
14875
15387
  "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
14876
- ).action(async function action57(opts) {
15388
+ ).action(async function action60(opts) {
14877
15389
  if (opts.since !== void 0 && parseDay(opts.since) === null)
14878
15390
  fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
14879
15391
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
@@ -14933,7 +15445,7 @@ function analyticsSubcommand(name, description, defaultDays) {
14933
15445
  command.option("--planning-days <n>", "Planning window in days", "180");
14934
15446
  command.option("--limit <n>", "Maximum page rows", "50");
14935
15447
  }
14936
- return command.action(async function action71(opts) {
15448
+ return command.action(async function action74(opts) {
14937
15449
  const result = await getAnalytics(requireCredentials(), name, {
14938
15450
  days: positiveInteger(this, opts.days, "--days"),
14939
15451
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -14953,7 +15465,7 @@ function urlsCommand() {
14953
15465
  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
15466
  "--match <regex>",
14955
15467
  "Client-side regex filter; exhausts inventory pages before filtering"
14956
- ).option("--limit <n>", "Maximum rows to print", "50").action(async function action71(opts) {
15468
+ ).option("--limit <n>", "Maximum rows to print", "50").action(async function action74(opts) {
14957
15469
  const creds = requireCredentials();
14958
15470
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
14959
15471
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -15147,10 +15659,10 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
15147
15659
  const chunks = [];
15148
15660
  let bytes = 0;
15149
15661
  let settled = false;
15150
- const settle = (action71) => {
15662
+ const settle = (action74) => {
15151
15663
  if (settled) return;
15152
15664
  settled = true;
15153
- action71();
15665
+ action74();
15154
15666
  };
15155
15667
  stream.on("data", (chunk) => {
15156
15668
  if (settled) return;
@@ -15221,11 +15733,11 @@ function pinnedFetch(url, addresses, limits) {
15221
15733
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
15222
15734
  };
15223
15735
  let settled = false;
15224
- const settle = (action71) => {
15736
+ const settle = (action74) => {
15225
15737
  if (settled) return;
15226
15738
  settled = true;
15227
15739
  clearTimeout(deadline);
15228
- action71();
15740
+ action74();
15229
15741
  };
15230
15742
  const req = send3(
15231
15743
  url,
@@ -15560,7 +16072,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
15560
16072
  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
16073
  "--worker-url <url>",
15562
16074
  "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) {
16075
+ ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action61(opts) {
15564
16076
  const creds = requireCredentials();
15565
16077
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
15566
16078
  if (!shopId)
@@ -15860,11 +16372,11 @@ function write(name) {
15860
16372
  });
15861
16373
  return c;
15862
16374
  }
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);
16375
+ function lifecycle(action74) {
16376
+ return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a flag`).argument("<id>").action(async function(id) {
16377
+ const r = await transitionManagedDelivery(requireCredentials(), kind, id, action74);
15866
16378
  if (!r.ok || !r.data)
15867
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
16379
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
15868
16380
  output(this, { flag: r.data }, () => detail(r.data));
15869
16381
  });
15870
16382
  }
@@ -15902,13 +16414,74 @@ function parse(command, value) {
15902
16414
  );
15903
16415
  }
15904
16416
  }
15905
- function detail2(x) {
16417
+ function collect2(value, previous) {
16418
+ return [...previous, value];
16419
+ }
16420
+ function customFieldsRecord(value) {
16421
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
16422
+ }
16423
+ function typedFieldValue(definition, raw) {
16424
+ if (raw === "null") return null;
16425
+ if (raw.trim() === "") return null;
16426
+ switch (definition?.type) {
16427
+ case "number": {
16428
+ const parsed = Number(raw.trim());
16429
+ return Number.isFinite(parsed) ? parsed : raw;
16430
+ }
16431
+ case "boolean": {
16432
+ const trimmed = raw.trim().toLowerCase();
16433
+ if (["true", "yes", "y", "1"].includes(trimmed)) return true;
16434
+ if (["false", "no", "n", "0"].includes(trimmed)) return false;
16435
+ return raw;
16436
+ }
16437
+ default:
16438
+ return raw;
16439
+ }
16440
+ }
16441
+ async function customFieldsFromPairs(command, pairs) {
16442
+ if (pairs.length === 0) return void 0;
16443
+ const parsedPairs = pairs.map((pair) => {
16444
+ const separator = pair.indexOf("=");
16445
+ if (separator <= 0)
16446
+ fail(command, `Invalid --field "${pair}": use key=value.`, EXIT_CODES.validation);
16447
+ return { key: pair.slice(0, separator), raw: pair.slice(separator + 1) };
16448
+ });
16449
+ const fields3 = await listBacklogFields(requireCredentials());
16450
+ if (!fields3.ok || !fields3.data) {
16451
+ process.stderr.write(
16452
+ `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.
16453
+ `
16454
+ );
16455
+ }
16456
+ const byKey = new Map(
16457
+ (fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field])
16458
+ );
16459
+ const values = {};
16460
+ for (const { key, raw } of parsedPairs) values[key] = typedFieldValue(byKey.get(key), raw);
16461
+ return values;
16462
+ }
16463
+ function detail2(x, labels) {
15906
16464
  console.log();
15907
16465
  console.log(source_default.bold(String(x.title ?? x.label ?? x.key ?? x.id)));
15908
16466
  console.log(` ID: ${source_default.dim(x.id)}`);
15909
16467
  if (x.status) console.log(` State: ${x.status}`);
16468
+ const custom = customFieldsRecord(x.custom_fields);
16469
+ if (Object.keys(custom).length > 0) {
16470
+ console.log();
16471
+ for (const [key, value] of Object.entries(custom)) {
16472
+ console.log(` ${labels.get(key) ?? key}: ${value === null ? "-" : String(value)}`);
16473
+ }
16474
+ }
15910
16475
  console.log();
15911
16476
  }
16477
+ async function fieldLabels(command, item) {
16478
+ if (shouldUseJson(command)) return /* @__PURE__ */ new Map();
16479
+ if (Object.keys(customFieldsRecord(item.custom_fields)).length === 0) return /* @__PURE__ */ new Map();
16480
+ const fields3 = await listBacklogFields(requireCredentials());
16481
+ return new Map(
16482
+ (fields3.ok && fields3.data ? fields3.data.data : []).map((field) => [field.key, field.label])
16483
+ );
16484
+ }
15912
16485
  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
16486
  const r = await listInsights(
15914
16487
  requireCredentials(),
@@ -15937,27 +16510,35 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
15937
16510
  const r = await getInsight(requireCredentials(), kind2, id);
15938
16511
  if (!r.ok || !r.data)
15939
16512
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15940
- output(this, { backlog_item: r.data }, () => detail2(r.data));
16513
+ const labels = await fieldLabels(this, r.data);
16514
+ output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
15941
16515
  });
15942
- function write2(action71) {
15943
- const c = new Command(action71).description(
15944
- `${action71 === "create" ? "Create" : "Update"} a ${singular}`
16516
+ function write2(action74) {
16517
+ const c = new Command(action74).description(
16518
+ `${action74 === "create" ? "Create" : "Update"} a ${singular}`
15945
16519
  );
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"
16520
+ if (action74 === "update") c.argument("<id>", `${singular} ID`);
16521
+ c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").option(
16522
+ "--field <key=value>",
16523
+ "Set one of the shop's own fields; repeatable. Use key=null to clear it.",
16524
+ collect2,
16525
+ []
15950
16526
  );
15951
16527
  c.action(async function(...args) {
15952
16528
  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;
16529
+ const id = action74 === "update" ? String(args[0]) : void 0;
16530
+ const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
15955
16531
  const body5 = parse(this, raw);
16532
+ const customFields = await customFieldsFromPairs(this, o.field);
16533
+ if (customFields) {
16534
+ body5.custom_fields = { ...customFieldsRecord(body5.custom_fields), ...customFields };
16535
+ }
15956
16536
  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);
16537
+ const r = action74 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
15958
16538
  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));
16539
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
16540
+ const labels = await fieldLabels(this, r.data);
16541
+ output(this, { backlog_item: r.data }, () => detail2(r.data, labels));
15961
16542
  });
15962
16543
  return c;
15963
16544
  }
@@ -16002,7 +16583,99 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
16002
16583
  fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
16003
16584
  output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
16004
16585
  });
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);
16586
+ function fieldDetail(field) {
16587
+ console.log();
16588
+ console.log(source_default.bold(field.label));
16589
+ console.log(` ID: ${source_default.dim(field.id)}`);
16590
+ console.log(` Key: ${field.key}`);
16591
+ console.log(` Type: ${field.type}`);
16592
+ if (field.options.length > 0) console.log(` Choices: ${field.options.join(", ")}`);
16593
+ console.log(` Required: ${field.required ? "yes" : "no"}`);
16594
+ console.log(` Visible to clients: ${field.client_visible ? "yes" : "no"}`);
16595
+ if (field.archived_at) console.log(` Archived: ${field.archived_at}`);
16596
+ console.log();
16597
+ }
16598
+ function boolOption(command, flag, value) {
16599
+ if (value === void 0) return void 0;
16600
+ const trimmed = value.trim().toLowerCase();
16601
+ if (["true", "yes", "y", "1"].includes(trimmed)) return true;
16602
+ if (["false", "no", "n", "0"].includes(trimmed)) return false;
16603
+ fail(command, `Invalid ${flag} "${value}": use true or false.`, EXIT_CODES.validation);
16604
+ }
16605
+ var fieldsList = new Command("list").alias("ls").description("List the shop's idea fields").option("--include-archived", "Include archived fields").action(async function(o) {
16606
+ const r = await listBacklogFields(requireCredentials(), o.includeArchived);
16607
+ if (!r.ok || !r.data)
16608
+ fail(this, r.error ?? "Failed to list fields", classifyStatus(r.status), r.errorDetails);
16609
+ output(
16610
+ this,
16611
+ { fields: r.data.data },
16612
+ () => printTable(
16613
+ ["KEY", "LABEL", "TYPE", "REQUIRED", "CLIENT"],
16614
+ r.data.data.map((field) => [
16615
+ field.key,
16616
+ field.label,
16617
+ field.type,
16618
+ field.required ? "yes" : "no",
16619
+ field.client_visible ? "yes" : "no"
16620
+ ]),
16621
+ [24, 28, 14, 10, 8]
16622
+ )
16623
+ );
16624
+ });
16625
+ var fieldsCreate = new Command("create").description("Add an idea field").requiredOption("--label <text>", "What the field is called").requiredOption(
16626
+ "--type <type>",
16627
+ "short_text, long_text, number, boolean, single_choice, date, url or person"
16628
+ ).option("--key <key>", "Field key; derived from the label when omitted").option(
16629
+ "--option <value>",
16630
+ "A choice for a single_choice field; repeatable",
16631
+ collect2,
16632
+ []
16633
+ ).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) {
16634
+ const required = boolOption(this, "--required", o.required);
16635
+ const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
16636
+ const r = await createBacklogField(requireCredentials(), {
16637
+ label: o.label,
16638
+ type: o.type,
16639
+ ...o.key ? { key: o.key } : {},
16640
+ ...o.option.length > 0 ? { options: o.option } : {},
16641
+ ...required === void 0 ? {} : { required },
16642
+ ...clientVisible === void 0 ? {} : { client_visible: clientVisible }
16643
+ });
16644
+ if (!r.ok || !r.data)
16645
+ fail(this, r.error ?? "Failed to create field", classifyStatus(r.status), r.errorDetails);
16646
+ output(this, { field: r.data }, () => fieldDetail(r.data));
16647
+ });
16648
+ var fieldsUpdate = new Command("update").description("Update an idea field").argument("<id>", "field ID").option("--label <text>", "What the field is called").option(
16649
+ "--option <value>",
16650
+ "Replace the choices of a single_choice field; repeatable",
16651
+ collect2,
16652
+ []
16653
+ ).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) {
16654
+ const required = boolOption(this, "--required", o.required);
16655
+ const clientVisible = boolOption(this, "--client-visible", o.clientVisible);
16656
+ const archived = boolOption(this, "--archived", o.archived);
16657
+ const body5 = {
16658
+ ...o.label ? { label: o.label } : {},
16659
+ ...o.option.length > 0 ? { options: o.option } : {},
16660
+ ...required === void 0 ? {} : { required },
16661
+ ...clientVisible === void 0 ? {} : { client_visible: clientVisible },
16662
+ ...archived === void 0 ? {} : { archived }
16663
+ };
16664
+ if (!Object.keys(body5).length)
16665
+ fail(this, "Provide at least one field option to change.", EXIT_CODES.validation);
16666
+ const r = await updateBacklogField(requireCredentials(), id, body5);
16667
+ if (!r.ok || !r.data)
16668
+ fail(this, r.error ?? "Failed to update field", classifyStatus(r.status), r.errorDetails);
16669
+ output(this, { field: r.data }, () => fieldDetail(r.data));
16670
+ });
16671
+ var fieldsArchive = new Command("archive").description("Archive an idea field; values already on ideas are kept").argument("<id>", "field ID").action(async function(id) {
16672
+ const r = await archiveBacklogField(requireCredentials(), id);
16673
+ if (!r.ok || !r.data)
16674
+ fail(this, r.error ?? "Failed to archive field", classifyStatus(r.status), r.errorDetails);
16675
+ output(this, { field: r.data }, () => fieldDetail(r.data));
16676
+ });
16677
+ var fields2 = new Command("fields").description("Manage the shop's own idea fields").addCommand(fieldsList).addCommand(fieldsCreate).addCommand(fieldsUpdate).addCommand(fieldsArchive);
16678
+ 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
16679
 
16007
16680
  // src/commands/signals.ts
16008
16681
  var import_node_fs19 = __toESM(require("node:fs"));
@@ -16064,24 +16737,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
16064
16737
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
16065
16738
  output(this, { signal: r.data }, () => detail3(r.data));
16066
16739
  });
16067
- function write3(action71) {
16068
- const c = new Command(action71).description(
16069
- `${action71 === "create" ? "Create" : "Update"} a ${singular2}`
16740
+ function write3(action74) {
16741
+ const c = new Command(action74).description(
16742
+ `${action74 === "create" ? "Create" : "Update"} a ${singular2}`
16070
16743
  );
16071
- if (action71 === "update") c.argument("<id>", `${singular2} ID`);
16744
+ if (action74 === "update") c.argument("<id>", `${singular2} ID`);
16072
16745
  c.argument("[json]", "Inline JSON, @file, or file path").option(
16073
16746
  "--file <path>",
16074
16747
  "JSON body file"
16075
16748
  );
16076
16749
  c.action(async function(...args) {
16077
16750
  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;
16751
+ const id = action74 === "update" ? String(args[0]) : void 0;
16752
+ const raw = o.file ? `@${o.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
16080
16753
  const body5 = parse2(this, raw);
16081
16754
  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);
16755
+ const r = action74 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
16083
16756
  if (!r.ok || !r.data)
16084
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
16757
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
16085
16758
  output(this, { signal: r.data }, () => detail3(r.data));
16086
16759
  });
16087
16760
  return c;
@@ -16173,11 +16846,11 @@ function write4(name) {
16173
16846
  });
16174
16847
  return c;
16175
16848
  }
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);
16849
+ function lifecycle2(action74) {
16850
+ return new Command(action74).description(`${action74[0].toUpperCase()}${action74.slice(1)} a personalization`).argument("<id>").action(async function(id) {
16851
+ const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action74);
16179
16852
  if (!r.ok || !r.data)
16180
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
16853
+ fail(this, r.error ?? `Failed to ${action74}`, classifyStatus(r.status), r.errorDetails);
16181
16854
  output(this, { personalization: r.data }, () => detail4(r.data));
16182
16855
  });
16183
16856
  }
@@ -16259,21 +16932,21 @@ function targetingResourceCommand(kind5) {
16259
16932
  );
16260
16933
  })
16261
16934
  );
16262
- for (const action71 of ["create", "update"]) {
16263
- const command = new Command(action71).description(
16264
- `${action71 === "create" ? "Create" : "Update"} a ${singular3}`
16935
+ for (const action74 of ["create", "update"]) {
16936
+ const command = new Command(action74).description(
16937
+ `${action74 === "create" ? "Create" : "Update"} a ${singular3}`
16265
16938
  );
16266
- if (action71 === "update") command.argument("<id>");
16939
+ if (action74 === "update") command.argument("<id>");
16267
16940
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
16268
16941
  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;
16942
+ const id = action74 === "update" ? String(args[0]) : void 0;
16943
+ const value = opts.file ? `@${opts.file}` : String(args[action74 === "update" ? 1 : 0] ?? "") || void 0;
16271
16944
  const requestBody = body3(this, value);
16272
- if (action71 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
16945
+ if (action74 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
16273
16946
  fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
16274
16947
  const response = await requestTargetingResource(
16275
16948
  requireCredentials(),
16276
- action71 === "create" ? "POST" : "PATCH",
16949
+ action74 === "create" ? "POST" : "PATCH",
16277
16950
  kind5,
16278
16951
  id,
16279
16952
  requestBody
@@ -16281,7 +16954,7 @@ function targetingResourceCommand(kind5) {
16281
16954
  if (!response.ok || !response.data)
16282
16955
  fail(
16283
16956
  this,
16284
- response.error ?? `Failed to ${action71}`,
16957
+ response.error ?? `Failed to ${action74}`,
16285
16958
  classifyStatus(response.status),
16286
16959
  response.errorDetails
16287
16960
  );
@@ -16290,7 +16963,7 @@ function targetingResourceCommand(kind5) {
16290
16963
  {
16291
16964
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
16292
16965
  },
16293
- () => console.log(source_default.green(`${action71}d ${singular3}`))
16966
+ () => console.log(source_default.green(`${action74}d ${singular3}`))
16294
16967
  );
16295
16968
  });
16296
16969
  root.addCommand(command);
@@ -16364,7 +17037,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
16364
17037
  // src/commands/brand.ts
16365
17038
  init_source();
16366
17039
  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() {
17040
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action62() {
16368
17041
  const creds = requireCredentials();
16369
17042
  const res = await getBrandContext(creds);
16370
17043
  if (res.status === 404) {
@@ -16397,7 +17070,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
16397
17070
 
16398
17071
  // src/commands/targeting.ts
16399
17072
  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) {
17073
+ 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
17074
  const creds = requireCredentials();
16402
17075
  const res = await testTargeting(creds, url, { status: opts.status });
16403
17076
  if (!res.ok || !res.data) {
@@ -16482,7 +17155,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16482
17155
  "--type <type>",
16483
17156
  "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
16484
17157
  "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) {
17158
+ ).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
17159
  if (!SOS_TYPES.has(opts.type)) {
16487
17160
  fail(
16488
17161
  this,
@@ -16509,7 +17182,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16509
17182
  }
16510
17183
  output(this, res.data, () => printCreatedTicket(res.data));
16511
17184
  });
16512
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action62() {
17185
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action65() {
16513
17186
  const creds = requireCredentials();
16514
17187
  const res = await listMySos(creds);
16515
17188
  if (!res.ok || !res.data) {
@@ -16658,7 +17331,7 @@ var create5 = new Command("create").description("Create a shop").argument("[json
16658
17331
  const r = await createShop(requireUserCredentials(), payload);
16659
17332
  done(this, r, "create shop");
16660
17333
  });
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) {
17334
+ 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
17335
  const b = raw === void 0 ? {} : body4(this, raw);
16663
17336
  if (o.holdoutEnabled && o.holdoutDisabled)
16664
17337
  fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
@@ -16791,7 +17464,7 @@ shopware.addCommand(
16791
17464
  );
16792
17465
  })
16793
17466
  );
16794
- var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(update3).addCommand(doctorCommand).addCommand(
17467
+ var shopsCommand = new Command("shops").description("Manage organization shops").addCommand(list12).addCommand(get9).addCommand(create5).addCommand(update4).addCommand(doctorCommand).addCommand(
16795
17468
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
16796
17469
  ).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
17470
 
@@ -16799,6 +17472,7 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
16799
17472
  function optionSchema(option) {
16800
17473
  return {
16801
17474
  flags: option.flags,
17475
+ ...option.argChoices ? { choices: option.argChoices } : {},
16802
17476
  description: option.description,
16803
17477
  required: option.required,
16804
17478
  optional: option.optional,
@@ -16824,7 +17498,7 @@ function commandSchema(command) {
16824
17498
  };
16825
17499
  }
16826
17500
  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() {
17501
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action74() {
16828
17502
  const schema = commandSchema(program3);
16829
17503
  output(this, schema, () => {
16830
17504
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -17239,7 +17913,7 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
17239
17913
  return `${lines.join("\n")}
17240
17914
  `;
17241
17915
  }
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) {
17916
+ 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
17917
  if (opts.gtm && opts.gtmUrl)
17244
17918
  fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
17245
17919
  if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
@@ -17442,7 +18116,7 @@ function installSkill(paths, targetRoot) {
17442
18116
  import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
17443
18117
  return target;
17444
18118
  }
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) {
18119
+ 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
18120
  let paths;
17447
18121
  try {
17448
18122
  paths = skillPaths();
@@ -18066,7 +18740,7 @@ function selectedEvents(opts) {
18066
18740
  }
18067
18741
  var list14 = new Command("list").alias("ls").description(
18068
18742
  "List webhooks. JSON includes availableEvents; secrets are never returned after create."
18069
- ).action(async function action65() {
18743
+ ).action(async function action68() {
18070
18744
  const data = await loadWebhookIndex(this);
18071
18745
  output(this, data, () => printWebhooks(data.data));
18072
18746
  });
@@ -18075,7 +18749,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
18075
18749
  "Webhook event to subscribe to; repeat for multiple events",
18076
18750
  (value, previous = []) => [...previous, value],
18077
18751
  []
18078
- ).action(async function action66(opts) {
18752
+ ).action(async function action69(opts) {
18079
18753
  const events = selectedEvents(opts);
18080
18754
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
18081
18755
  const index = await loadWebhookIndex(this);
@@ -18107,7 +18781,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
18107
18781
  }
18108
18782
  );
18109
18783
  });
18110
- var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action67(id) {
18784
+ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
18111
18785
  requireShopSecretKey(this);
18112
18786
  const result = handleResult(
18113
18787
  this,
@@ -18116,7 +18790,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
18116
18790
  );
18117
18791
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
18118
18792
  });
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) {
18793
+ 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
18794
  const limit = Number.parseInt(opts.limit, 10);
18121
18795
  if (!Number.isInteger(limit) || limit < 1)
18122
18796
  fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
@@ -18128,12 +18802,12 @@ var deliveries = new Command("deliveries").description("List webhook deliveries
18128
18802
  );
18129
18803
  output(this, result, () => printDeliveries(result.data, result.webhook));
18130
18804
  });
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(
18805
+ 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
18806
  "--event <event>",
18133
18807
  "Replacement event; repeat for multiple events",
18134
18808
  (value, previous = []) => [...previous, value],
18135
18809
  []
18136
- ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action69(id, opts) {
18810
+ ).option("--active", "Enable the webhook").option("--inactive", "Disable the webhook").action(async function action72(id, opts) {
18137
18811
  if (opts.active && opts.inactive)
18138
18812
  fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
18139
18813
  const events = selectedEvents(opts);
@@ -18166,7 +18840,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
18166
18840
  );
18167
18841
  output(this, { webhook: result }, () => printWebhooks([result]));
18168
18842
  });
18169
- var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action70(id) {
18843
+ var test3 = new Command("test").description("Send a signed test event to a webhook").argument("<id>", "Webhook id").action(async function action73(id) {
18170
18844
  requireShopSecretKey(this);
18171
18845
  const result = handleResult(
18172
18846
  this,
@@ -18181,7 +18855,7 @@ var test3 = new Command("test").description("Send a signed test event to a webho
18181
18855
  )
18182
18856
  );
18183
18857
  });
18184
- var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update4).addCommand(remove4).addCommand(deliveries).addCommand(test3);
18858
+ var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create7).addCommand(update5).addCommand(remove4).addCommand(deliveries).addCommand(test3);
18185
18859
 
18186
18860
  // src/commands/simulate.ts
18187
18861
  init_source();