@drip-apex/apex-cli 0.6.2 → 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.
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)
@@ -6988,14 +6988,14 @@ var init_open = __esm({
6988
6988
  }
6989
6989
  const subprocess = import_node_child_process5.default.spawn(command, cliArguments, childProcessOptions);
6990
6990
  if (options.wait) {
6991
- return new Promise((resolve, reject) => {
6991
+ return new Promise((resolve2, reject) => {
6992
6992
  subprocess.once("error", reject);
6993
6993
  subprocess.once("close", (exitCode) => {
6994
6994
  if (!options.allowNonzeroExitCode && exitCode > 0) {
6995
6995
  reject(new Error(`Exited with code ${exitCode}`));
6996
6996
  return;
6997
6997
  }
6998
- resolve(subprocess);
6998
+ resolve2(subprocess);
6999
6999
  });
7000
7000
  });
7001
7001
  }
@@ -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.2",
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);
@@ -8369,9 +8406,9 @@ function resolveSelection(tree, input) {
8369
8406
  function ask(question) {
8370
8407
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stderr });
8371
8408
  return new Promise(
8372
- (resolve) => rl.question(question, (answer) => {
8409
+ (resolve2) => rl.question(question, (answer) => {
8373
8410
  rl.close();
8374
- resolve(answer.trim());
8411
+ resolve2(answer.trim());
8375
8412
  })
8376
8413
  );
8377
8414
  }
@@ -8449,7 +8486,7 @@ function prompt(question, hide = false) {
8449
8486
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
8450
8487
  throw new Error("Interactive input is unavailable in non-TTY mode.");
8451
8488
  }
8452
- return new Promise((resolve) => {
8489
+ return new Promise((resolve2) => {
8453
8490
  const rl = import_node_readline2.default.createInterface({
8454
8491
  input: process.stdin,
8455
8492
  output: process.stderr
@@ -8469,13 +8506,13 @@ function prompt(question, hide = false) {
8469
8506
  stdout.write = originalWrite;
8470
8507
  console.error();
8471
8508
  rl.close();
8472
- resolve(answer.trim());
8509
+ resolve2(answer.trim());
8473
8510
  });
8474
8511
  masking = true;
8475
8512
  } else {
8476
8513
  rl.question(question, (answer) => {
8477
8514
  rl.close();
8478
- resolve(answer.trim());
8515
+ resolve2(answer.trim());
8479
8516
  });
8480
8517
  }
8481
8518
  });
@@ -8501,7 +8538,7 @@ function parseFormBody(body5) {
8501
8538
  async function waitForBrowserLogin(baseUrl, userMode = false) {
8502
8539
  const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
8503
8540
  const clientName = import_node_os5.default.hostname() || "Local machine";
8504
- return new Promise((resolve, reject) => {
8541
+ return new Promise((resolve2, reject) => {
8505
8542
  let settled = false;
8506
8543
  let timeout = null;
8507
8544
  const finish = (err, apiKey) => {
@@ -8513,7 +8550,7 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
8513
8550
  } catch {
8514
8551
  }
8515
8552
  if (err) reject(err);
8516
- else resolve(apiKey ?? "");
8553
+ else resolve2(apiKey ?? "");
8517
8554
  };
8518
8555
  const server = import_node_http.default.createServer((req, res) => {
8519
8556
  if (req.method === "OPTIONS") {
@@ -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) {
@@ -9414,8 +9590,8 @@ async function confirmApply(message) {
9414
9590
  }
9415
9591
  const readline3 = await import("node:readline");
9416
9592
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
9417
- const answer = await new Promise((resolve) => {
9418
- rl.question(source_default.yellow(`${message} [y/N] `), resolve);
9593
+ const answer = await new Promise((resolve2) => {
9594
+ rl.question(source_default.yellow(`${message} [y/N] `), resolve2);
9419
9595
  });
9420
9596
  rl.close();
9421
9597
  return answer.toLowerCase() === "y";
@@ -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) {
@@ -9828,8 +10096,8 @@ var del = new Command("delete").alias("rm").description("Delete an experiment").
9828
10096
  input: process.stdin,
9829
10097
  output: process.stderr
9830
10098
  });
9831
- const answer = await new Promise((resolve) => {
9832
- rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve);
10099
+ const answer = await new Promise((resolve2) => {
10100
+ rl.question(source_default.yellow(`Delete experiment "${exp.name}" (${exp.id})? [y/N] `), resolve2);
9833
10101
  });
9834
10102
  rl.close();
9835
10103
  if (answer.toLowerCase() !== "y") {
@@ -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
+ )
10325
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
+ )
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
- (resolve) => setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - screenshotNow())))
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");
@@ -10938,11 +11450,11 @@ function cleanupResetJs(experiment) {
10938
11450
  }
10939
11451
  function runAbHarness(args, cwd) {
10940
11452
  const bin = process.env.AB_HARNESS_BIN || "ab-harness";
10941
- return new Promise((resolve, reject) => {
11453
+ return new Promise((resolve2, reject) => {
10942
11454
  const child = (0, import_node_child_process6.spawn)(bin, args, { cwd, stdio: "inherit" });
10943
11455
  child.on("error", reject);
10944
11456
  child.on("exit", (code) => {
10945
- if (code === 0) resolve();
11457
+ if (code === 0) resolve2();
10946
11458
  else reject(new Error(`${bin} ${args.join(" ")} exited with ${code}`));
10947
11459
  });
10948
11460
  });
@@ -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)
@@ -12416,7 +12928,7 @@ var suggest = new Command("suggest").description("Suggest goals and funnels from
12416
12928
  const readline3 = await import("node:readline");
12417
12929
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
12418
12930
  const answer = await new Promise(
12419
- (resolve) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve)
12931
+ (resolve2) => rl.question(source_default.yellow("Create this goal? [y/N] "), resolve2)
12420
12932
  );
12421
12933
  rl.close();
12422
12934
  if (answer.toLowerCase() !== "y") continue;
@@ -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(
@@ -12778,9 +13290,219 @@ var off = new Command("off").description("Disable consent gating").action(async
12778
13290
  });
12779
13291
  var consentCommand = new Command("consent").description("Manage consent gating").addCommand(get2).addCommand(set).addCommand(off);
12780
13292
 
13293
+ // src/commands/project-code.ts
13294
+ var import_node_fs15 = require("node:fs");
13295
+ var import_node_path13 = require("node:path");
13296
+
13297
+ // src/commands/runtime-settings-shop.ts
13298
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13299
+ async function identifyRuntimeSettingsShop(command, requested) {
13300
+ let credentials = requireCredentials();
13301
+ if (credentials.credentialKind === "user") {
13302
+ let shopId = requested;
13303
+ if (!uuidPattern.test(requested)) {
13304
+ const context = await getAccountContext({
13305
+ ...credentials,
13306
+ resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
13307
+ });
13308
+ if (!context.ok || !context.data)
13309
+ fail(command, context.error ?? "Shop discovery failed", classifyStatus(context.status));
13310
+ const match2 = context.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
13311
+ if (!match2)
13312
+ fail(command, "--shop domain is not available to this user", EXIT_CODES.validation);
13313
+ shopId = match2.id;
13314
+ }
13315
+ credentials = { ...credentials, resolvedContext: { ...credentials.resolvedContext, shopId } };
13316
+ }
13317
+ const response = await getInstallDomains(credentials);
13318
+ if (!response.ok || !response.data)
13319
+ fail(command, response.error ?? "Shop identity unavailable", classifyStatus(response.status));
13320
+ const { shop_id, domain } = response.data;
13321
+ if (credentials.credentialKind === "user" && shop_id.toLowerCase() !== credentials.resolvedContext?.shopId?.toLowerCase()) {
13322
+ fail(
13323
+ command,
13324
+ "Shop identity does not match the requested user-key context",
13325
+ EXIT_CODES.validation
13326
+ );
13327
+ }
13328
+ if (requested.toLowerCase() !== shop_id.toLowerCase() && requested.toLowerCase() !== domain.toLowerCase()) {
13329
+ fail(command, "--shop does not match the authenticated shop", EXIT_CODES.validation);
13330
+ }
13331
+ setEnvelopeContext({ shop_id, credential: credentials.credentialKind ?? "shop" });
13332
+ return { shop_id, domain, credentials };
13333
+ }
13334
+
13335
+ // src/commands/project-code.ts
13336
+ var fields = ["enabled", "css", "helperJs", "js"];
13337
+ function readPatch(command, file) {
13338
+ let input;
13339
+ try {
13340
+ input = JSON.parse((0, import_node_fs15.readFileSync)(file, "utf8"));
13341
+ } catch (error) {
13342
+ fail(command, `Cannot read project code JSON: ${String(error)}`, EXIT_CODES.validation);
13343
+ }
13344
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length === 0) {
13345
+ fail(command, "Project code must be a nonempty partial object", EXIT_CODES.validation);
13346
+ }
13347
+ for (const [key, value] of Object.entries(input)) {
13348
+ if (!fields.includes(key))
13349
+ fail(command, `Unknown project code field: ${key}`, EXIT_CODES.validation);
13350
+ if (key === "enabled" ? typeof value !== "boolean" : value !== null && (typeof value !== "string" || value.length > 2e4)) {
13351
+ fail(
13352
+ command,
13353
+ `${key} must be ${key === "enabled" ? "a boolean" : "null or a string of at most 20,000 characters"}`,
13354
+ EXIT_CODES.validation
13355
+ );
13356
+ }
13357
+ }
13358
+ return input;
13359
+ }
13360
+ async function readCurrent(command, shop) {
13361
+ const response = await getRuntimeSettings(shop.credentials);
13362
+ if (!response.ok || !response.data)
13363
+ fail(
13364
+ command,
13365
+ response.error ?? "Runtime settings read failed",
13366
+ classifyStatus(response.status)
13367
+ );
13368
+ const data = response.data;
13369
+ if (data.shop_id !== shop.shop_id)
13370
+ fail(command, "Runtime settings returned a different shop", EXIT_CODES.validation);
13371
+ const projectCode = data.runtime_settings?.projectCode;
13372
+ if (!projectCode || typeof projectCode.enabled !== "boolean" || [projectCode.css, projectCode.helperJs, projectCode.js].some(
13373
+ (code) => typeof code !== "string"
13374
+ ) || !(data.version === null || typeof data.version === "string" && data.version.length > 0)) {
13375
+ fail(
13376
+ command,
13377
+ "Runtime settings must include complete project code and an opaque version",
13378
+ EXIT_CODES.general
13379
+ );
13380
+ }
13381
+ return { projectCode, version: data.version };
13382
+ }
13383
+ function effectiveDiff(before, patch) {
13384
+ const after = { ...before };
13385
+ if (patch.enabled !== void 0) after.enabled = patch.enabled;
13386
+ for (const key of ["css", "helperJs", "js"]) {
13387
+ if (patch[key] !== void 0) after[key] = patch[key]?.trim() || "";
13388
+ }
13389
+ const changedFields = fields.filter((field) => before[field] !== after[field]);
13390
+ const changes = Object.fromEntries(
13391
+ changedFields.map((field) => [field, { before: before[field], after: after[field] }])
13392
+ );
13393
+ return { changedFields, changes, before, after };
13394
+ }
13395
+ var projectCodeCommand = new Command("project-code").description(
13396
+ "Inspect, preview, and back up shop project code"
13397
+ );
13398
+ projectCodeCommand.addCommand(
13399
+ new Command("get").description("Read complete project code and its opaque version").requiredOption("--shop <domain-or-id>", "Shop domain or UUID").action(async (options, command) => {
13400
+ const { credentials: _credentials, ...identity } = await identifyRuntimeSettingsShop(
13401
+ command,
13402
+ options.shop
13403
+ );
13404
+ const current = await readCurrent(command, { ...identity, credentials: _credentials });
13405
+ output(
13406
+ command,
13407
+ { ...identity, ...current },
13408
+ () => process.stdout.write(`${JSON.stringify({ ...identity, ...current }, null, 2)}
13409
+ `)
13410
+ );
13411
+ })
13412
+ );
13413
+ for (const mode of ["diff", "set"]) {
13414
+ const command = new Command(mode).description(
13415
+ mode === "diff" ? "Preview an effective partial project code change without writing" : "Back up prior code and apply a version-checked partial change"
13416
+ ).requiredOption("--file <path>", "Partial projectCode JSON file").requiredOption("--shop <domain-or-id>", "Shop domain or UUID");
13417
+ if (mode === "set")
13418
+ command.requiredOption(
13419
+ "--backup <path>",
13420
+ "New backup file; an existing file is never replaced"
13421
+ );
13422
+ command.action(async (options, actionCommand) => {
13423
+ const patch = readPatch(actionCommand, options.file);
13424
+ const shop = await identifyRuntimeSettingsShop(actionCommand, options.shop);
13425
+ const current = await readCurrent(actionCommand, shop);
13426
+ const { credentials: _credentials, ...identity } = shop;
13427
+ const diff = effectiveDiff(current.projectCode, patch);
13428
+ if (mode === "diff" || diff.changedFields.length === 0) {
13429
+ const result2 = {
13430
+ ...identity,
13431
+ status: diff.changedFields.length ? "changed" : "unchanged",
13432
+ version: current.version,
13433
+ diff
13434
+ };
13435
+ output(
13436
+ actionCommand,
13437
+ result2,
13438
+ () => process.stdout.write(`${JSON.stringify(result2, null, 2)}
13439
+ `)
13440
+ );
13441
+ return;
13442
+ }
13443
+ const backupPath = (0, import_node_path13.resolve)(options.backup);
13444
+ try {
13445
+ (0, import_node_fs15.writeFileSync)(
13446
+ backupPath,
13447
+ `${JSON.stringify({ ...identity, ...current, capturedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
13448
+ `,
13449
+ { flag: "wx", mode: 384 }
13450
+ );
13451
+ } catch (error) {
13452
+ fail(
13453
+ actionCommand,
13454
+ `Backup failed; project code was not sent: ${String(error)}`,
13455
+ EXIT_CODES.general
13456
+ );
13457
+ }
13458
+ const response = await updateRuntimeSettings(shop.credentials, {
13459
+ projectCode: patch,
13460
+ expectedVersion: current.version
13461
+ });
13462
+ if (!response.ok || !response.data) {
13463
+ fail(
13464
+ actionCommand,
13465
+ response.error ?? "Project code update failed",
13466
+ classifyStatus(response.status),
13467
+ { ...response.errorDetails, backupPath }
13468
+ );
13469
+ }
13470
+ const data = response.data;
13471
+ const warnings = [...data.warnings ?? []];
13472
+ if (data.precompile?.status === "warning" || data.precompile?.status === "error") {
13473
+ warnings.push(
13474
+ `Project code saved; publication ${data.precompile.status}: ${data.precompile.summary}`
13475
+ );
13476
+ }
13477
+ if (data.shop_id !== identity.shop_id)
13478
+ warnings.push(
13479
+ "The write response has a different shop identity; inspect the saved state before another write."
13480
+ );
13481
+ for (const warning of warnings) process.stderr.write(`WARNING: ${warning}
13482
+ `);
13483
+ const result = {
13484
+ ...identity,
13485
+ status: "saved",
13486
+ diff,
13487
+ backupPath,
13488
+ version: data.version,
13489
+ writtenVersion: data.writtenVersion,
13490
+ precompile: data.precompile,
13491
+ warnings
13492
+ };
13493
+ output(
13494
+ actionCommand,
13495
+ result,
13496
+ () => process.stdout.write(`${JSON.stringify(result, null, 2)}
13497
+ `)
13498
+ );
13499
+ });
13500
+ projectCodeCommand.addCommand(command);
13501
+ }
13502
+
12781
13503
  // src/commands/triggers.ts
12782
- var import_node_fs15 = __toESM(require("node:fs"));
12783
- var import_node_path13 = __toESM(require("node:path"));
13504
+ var import_node_fs16 = __toESM(require("node:fs"));
13505
+ var import_node_path14 = __toESM(require("node:path"));
12784
13506
 
12785
13507
  // src/trigger-proposal.ts
12786
13508
  function runtimePath(path24) {
@@ -12868,8 +13590,8 @@ function score2(path24, patterns) {
12868
13590
  0
12869
13591
  );
12870
13592
  }
12871
- function best(fields, patterns, numeric2 = false) {
12872
- const ranked = fields.filter(
13593
+ function best(fields3, patterns, numeric2 = false) {
13594
+ const ranked = fields3.filter(
12873
13595
  (field) => !numeric2 || field.numericType === true || field.values.some((value) => typeof value === "number") || field.magnitude !== void 0
12874
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));
12875
13597
  if (!ranked.length) return [];
@@ -12894,15 +13616,15 @@ function proposeTriggers(report) {
12894
13616
  continue;
12895
13617
  const descriptors = candidate.fields ?? candidate.paths;
12896
13618
  const payload = candidate.properties ?? candidate.payload ?? candidate;
12897
- const fields = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
13619
+ const fields3 = descriptors !== void 0 ? fieldsFrom(descriptors, "", true) : fieldsFrom(payload, "", isDescriptorArray(payload));
12898
13620
  const revenues = best(
12899
- fields,
13621
+ fields3,
12900
13622
  [/revenue/, /total.*amount|amount.*total/, /grand.*total|total/, /value|amount/],
12901
13623
  true
12902
13624
  );
12903
- const orderIds = best(fields, [/order.*id|id.*order/, /transaction.*id/, /(^|\.)id$/]);
12904
- const currencies = best(fields, [/currency/, /(^|\.)cur(rency)?$/]);
12905
- const items = best(fields, [/(^|\.)((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)$/]);
12906
13628
  if (!revenues.length || !orderIds.length || !currencies.length) continue;
12907
13629
  for (const revenue of revenues)
12908
13630
  for (const orderId of orderIds)
@@ -13197,10 +13919,10 @@ function writeWarnings(result) {
13197
13919
  `);
13198
13920
  }
13199
13921
  }
13200
- var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13922
+ var uuidPattern2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13201
13923
  async function identify(command, requestedShop) {
13202
13924
  let credentials = requireCredentials();
13203
- if (credentials.credentialKind === "user" && !uuidPattern.test(requestedShop)) {
13925
+ if (credentials.credentialKind === "user" && !uuidPattern2.test(requestedShop)) {
13204
13926
  const discoveryCredentials = {
13205
13927
  ...credentials,
13206
13928
  resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
@@ -13273,7 +13995,7 @@ var get3 = new Command("get").description("Get data layer triggers").requiredOpt
13273
13995
  var set2 = new Command("set").requiredOption("--file <json>", "JSON trigger array").requiredOption("--shop <domain-or-id>", "Shop domain or Apex shop ID").action(async function(opts) {
13274
13996
  let triggers;
13275
13997
  try {
13276
- triggers = strictTriggers(JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.file), "utf8")));
13998
+ triggers = strictTriggers(JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.file), "utf8")));
13277
13999
  } catch (error) {
13278
14000
  fail(
13279
14001
  this,
@@ -13300,7 +14022,7 @@ var propose = new Command("propose").requiredOption("--from-file <u2-report.json
13300
14022
  let proposals;
13301
14023
  let report;
13302
14024
  try {
13303
- report = JSON.parse(import_node_fs15.default.readFileSync(import_node_path13.default.resolve(opts.fromFile), "utf8"));
14025
+ report = JSON.parse(import_node_fs16.default.readFileSync(import_node_path14.default.resolve(opts.fromFile), "utf8"));
13304
14026
  proposals = proposeTriggers(report);
13305
14027
  } catch (error) {
13306
14028
  fail(this, error instanceof Error ? error.message : String(error), 2);
@@ -13452,11 +14174,11 @@ async function patchWithRetry(command, build) {
13452
14174
  warn2(command, result.data.precompile);
13453
14175
  return result.data;
13454
14176
  }
13455
- 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() {
13456
14178
  const data = await load4(this);
13457
14179
  output(this, data, () => printDomains(data));
13458
14180
  });
13459
- 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) {
13460
14182
  const added = domain.map(normalizeDomain);
13461
14183
  const data = await patchWithRetry(
13462
14184
  this,
@@ -13464,7 +14186,7 @@ var add = new Command("add").description("Add install domains").argument("<domai
13464
14186
  );
13465
14187
  output(this, data, () => printDomains(data));
13466
14188
  });
13467
- 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) {
13468
14190
  const removeSet = new Set(domain.map(normalizeDomain));
13469
14191
  const data = await patchWithRetry(
13470
14192
  this,
@@ -13472,7 +14194,7 @@ var remove = new Command("remove").description("Remove install domains").argumen
13472
14194
  );
13473
14195
  output(this, data, () => printDomains(data));
13474
14196
  });
13475
- 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) {
13476
14198
  const replacement = domain.map(normalizeDomain);
13477
14199
  const data = await patchWithRetry(this, () => replacement);
13478
14200
  output(this, data, () => printDomains(data));
@@ -13564,14 +14286,14 @@ async function patchWithRetry2(command, build) {
13564
14286
  );
13565
14287
  return result.data;
13566
14288
  }
13567
- 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() {
13568
14290
  const data = await load5(this);
13569
14291
  output(this, data, () => printSettings(data.externalTracking));
13570
14292
  });
13571
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(
13572
14294
  "--signed",
13573
14295
  "Require signed handover tokens (the default; unsigning is a platform-support action)"
13574
- ).action(async function action53(opts) {
14296
+ ).action(async function action56(opts) {
13575
14297
  const domains = opts.domain.map(normalizeDomain2);
13576
14298
  if (domains.some((domain) => !domain))
13577
14299
  fail(
@@ -13591,14 +14313,14 @@ var set4 = new Command("set").description("Enable cross-domain identity handover
13591
14313
  }));
13592
14314
  output(this, data, () => printSettings(data.externalTracking));
13593
14315
  });
13594
- 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() {
13595
14317
  const data = await patchWithRetry2(this, (current) => ({
13596
14318
  externalTracking: { domains: [], signed: true },
13597
14319
  ifRevision: current.revision
13598
14320
  }));
13599
14321
  output(this, data, () => printSettings(data.externalTracking));
13600
14322
  });
13601
- var qa2 = new Command("qa").description("Check static link/form handover parameter carriage").requiredOption("--from <url>", "Page URL to inspect").requiredOption("--to-domain <domain>", "Target domain that should carry the handover parameter").action(async function action55(opts) {
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) {
13602
14324
  const config = await load5(this);
13603
14325
  const target = normalizeDomain2(opts.toDomain);
13604
14326
  if (!target) fail(this, "--to-domain must be a valid hostname", EXIT_CODES.validation);
@@ -14641,7 +15363,7 @@ var doctorCommand = new Command("doctor").description("Check shop configuration
14641
15363
  ).option("--check <id>", "Run one check (for example purchase_capture)").option(
14642
15364
  "--worker <url>",
14643
15365
  `Apex Worker base URL; defaults to DRIP_WORKER_URL or ${PRODUCTION_WORKER_BASE_URL}`
14644
- ).action(async function action56(options) {
15366
+ ).action(async function action59(options) {
14645
15367
  const data = await runDoctor(options);
14646
15368
  if (options.check && data.checks.length === 0)
14647
15369
  fail(this, `Unknown doctor check: ${options.check}`, EXIT_CODES.validation);
@@ -14663,7 +15385,7 @@ function printDoctorReport(checks) {
14663
15385
  var consentRateCommand = new Command("consent-rate").description("Estimate analytics consent acceptance from visitor series").option(
14664
15386
  "--since <date>",
14665
15387
  "Consent gating start date (YYYY-MM-DD); splits the baseline and gated windows"
14666
- ).action(async function action57(opts) {
15388
+ ).action(async function action60(opts) {
14667
15389
  if (opts.since !== void 0 && parseDay(opts.since) === null)
14668
15390
  fail(this, "--since must be a calendar date in YYYY-MM-DD form", EXIT_CODES.validation);
14669
15391
  const result = await getVisitorSeries(requireCredentials(), { days: VISITOR_SERIES_DAYS });
@@ -14723,7 +15445,7 @@ function analyticsSubcommand(name, description, defaultDays) {
14723
15445
  command.option("--planning-days <n>", "Planning window in days", "180");
14724
15446
  command.option("--limit <n>", "Maximum page rows", "50");
14725
15447
  }
14726
- return command.action(async function action71(opts) {
15448
+ return command.action(async function action74(opts) {
14727
15449
  const result = await getAnalytics(requireCredentials(), name, {
14728
15450
  days: positiveInteger(this, opts.days, "--days"),
14729
15451
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -14743,7 +15465,7 @@ function urlsCommand() {
14743
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(
14744
15466
  "--match <regex>",
14745
15467
  "Client-side regex filter; exhausts inventory pages before filtering"
14746
- ).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) {
14747
15469
  const creds = requireCredentials();
14748
15470
  const days = positiveInteger(this, opts.days, "--days") ?? 14;
14749
15471
  const displayLimit = Math.min(200, positiveInteger(this, opts.limit, "--limit") ?? 50);
@@ -14932,15 +15654,15 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14932
15654
  `Response from ${hostname} uses unsupported content encoding "${encoding}"`
14933
15655
  )
14934
15656
  );
14935
- return new Promise((resolve, reject) => {
15657
+ return new Promise((resolve2, reject) => {
14936
15658
  const stream = decoderFor(encoding);
14937
15659
  const chunks = [];
14938
15660
  let bytes = 0;
14939
15661
  let settled = false;
14940
- const settle = (action71) => {
15662
+ const settle = (action74) => {
14941
15663
  if (settled) return;
14942
15664
  settled = true;
14943
- action71();
15665
+ action74();
14944
15666
  };
14945
15667
  stream.on("data", (chunk) => {
14946
15668
  if (settled) return;
@@ -14952,7 +15674,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14952
15674
  }
14953
15675
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
14954
15676
  stream.destroy();
14955
- settle(() => resolve({ body: Buffer.concat(chunks), truncated: true }));
15677
+ settle(() => resolve2({ body: Buffer.concat(chunks), truncated: true }));
14956
15678
  return;
14957
15679
  }
14958
15680
  chunks.push(chunk);
@@ -14962,7 +15684,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14962
15684
  "error",
14963
15685
  (error) => settle(() => {
14964
15686
  if (limits.onExceed === "truncate" && inputTruncated && chunks.length > 0)
14965
- resolve({ body: Buffer.concat(chunks), truncated: true });
15687
+ resolve2({ body: Buffer.concat(chunks), truncated: true });
14966
15688
  else
14967
15689
  reject(
14968
15690
  new BodyDecodeError(
@@ -14973,7 +15695,7 @@ function decodeBody(raw, header, limits, hostname, inputTruncated = false) {
14973
15695
  );
14974
15696
  stream.on(
14975
15697
  "end",
14976
- () => settle(() => resolve({ body: Buffer.concat(chunks), truncated: false }))
15698
+ () => settle(() => resolve2({ body: Buffer.concat(chunks), truncated: false }))
14977
15699
  );
14978
15700
  stream.end(raw);
14979
15701
  });
@@ -15000,7 +15722,7 @@ function responseFromIncoming(res, body5, decoded) {
15000
15722
  });
15001
15723
  }
15002
15724
  function pinnedFetch(url, addresses, limits) {
15003
- return new Promise((resolve, reject) => {
15725
+ return new Promise((resolve2, reject) => {
15004
15726
  const send3 = url.protocol === "https:" ? import_node_https.request : import_node_http2.request;
15005
15727
  const pinnedLookup = (_hostname, options, callback) => {
15006
15728
  if (options?.all)
@@ -15011,11 +15733,11 @@ function pinnedFetch(url, addresses, limits) {
15011
15733
  else callback(null, addresses[0], (0, import_node_net.isIP)(addresses[0]));
15012
15734
  };
15013
15735
  let settled = false;
15014
- const settle = (action71) => {
15736
+ const settle = (action74) => {
15015
15737
  if (settled) return;
15016
15738
  settled = true;
15017
15739
  clearTimeout(deadline);
15018
- action71();
15740
+ action74();
15019
15741
  };
15020
15742
  const req = send3(
15021
15743
  url,
@@ -15047,7 +15769,7 @@ function pinnedFetch(url, addresses, limits) {
15047
15769
  chunks.push(chunk.subarray(0, limits.maxBytes - bytes));
15048
15770
  req.destroy();
15049
15771
  settle(
15050
- () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve, reject)
15772
+ () => finalizeIncoming(url, res, Buffer.concat(chunks), true, limits).then(resolve2, reject)
15051
15773
  );
15052
15774
  return;
15053
15775
  }
@@ -15062,7 +15784,7 @@ function pinnedFetch(url, addresses, limits) {
15062
15784
  reject(new Error(`No HTTP status from ${url.hostname}`));
15063
15785
  return;
15064
15786
  }
15065
- finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve, reject);
15787
+ finalizeIncoming(url, res, Buffer.concat(chunks), false, limits).then(resolve2, reject);
15066
15788
  })
15067
15789
  );
15068
15790
  }
@@ -15350,7 +16072,7 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
15350
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(
15351
16073
  "--worker-url <url>",
15352
16074
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
15353
- ).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) {
15354
16076
  const creds = requireCredentials();
15355
16077
  const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
15356
16078
  if (!shopId)
@@ -15494,8 +16216,8 @@ var installCommand = new Command("install").description("Check Apex storefront i
15494
16216
 
15495
16217
  // src/commands/flags.ts
15496
16218
  init_source();
15497
- var import_node_fs16 = __toESM(require("node:fs"));
15498
- var import_node_path14 = __toESM(require("node:path"));
16219
+ var import_node_fs17 = __toESM(require("node:fs"));
16220
+ var import_node_path15 = __toESM(require("node:path"));
15499
16221
 
15500
16222
  // src/edge-status.ts
15501
16223
  function getEdgeDeliveryStatus(deliveryType, expectedWorstCaseSeconds = 360) {
@@ -15537,7 +16259,7 @@ function body(command, value, opts) {
15537
16259
  let parsed = {};
15538
16260
  if (value) {
15539
16261
  const source = value.startsWith("@") ? value.slice(1) : value;
15540
- const text = value.startsWith("@") || import_node_fs16.default.existsSync(source) ? import_node_fs16.default.readFileSync(import_node_path14.default.resolve(process.cwd(), source), "utf8") : value;
16262
+ const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
15541
16263
  try {
15542
16264
  parsed = JSON.parse(text);
15543
16265
  } catch (e) {
@@ -15650,11 +16372,11 @@ function write(name) {
15650
16372
  });
15651
16373
  return c;
15652
16374
  }
15653
- function lifecycle(action71) {
15654
- return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a flag`).argument("<id>").action(async function(id) {
15655
- const r = await transitionManagedDelivery(requireCredentials(), kind, id, action71);
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);
15656
16378
  if (!r.ok || !r.data)
15657
- 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);
15658
16380
  output(this, { flag: r.data }, () => detail(r.data));
15659
16381
  });
15660
16382
  }
@@ -15670,8 +16392,8 @@ Examples:
15670
16392
  );
15671
16393
 
15672
16394
  // src/commands/backlog.ts
15673
- var import_node_fs17 = __toESM(require("node:fs"));
15674
- var import_node_path15 = __toESM(require("node:path"));
16395
+ var import_node_fs18 = __toESM(require("node:fs"));
16396
+ var import_node_path16 = __toESM(require("node:path"));
15675
16397
  init_source();
15676
16398
  var kind2 = "backlog";
15677
16399
  var singular = "backlog item";
@@ -15679,7 +16401,7 @@ function parse(command, value) {
15679
16401
  if (!value) return {};
15680
16402
  try {
15681
16403
  const source = value.startsWith("@") ? value.slice(1) : value;
15682
- const text = value.startsWith("@") || import_node_fs17.default.existsSync(source) ? import_node_fs17.default.readFileSync(import_node_path15.default.resolve(process.cwd(), source), "utf8") : value;
16404
+ const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
15683
16405
  const body5 = JSON.parse(text);
15684
16406
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15685
16407
  throw new Error("body must be an object");
@@ -15692,13 +16414,74 @@ function parse(command, value) {
15692
16414
  );
15693
16415
  }
15694
16416
  }
15695
- 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) {
15696
16464
  console.log();
15697
16465
  console.log(source_default.bold(String(x.title ?? x.label ?? x.key ?? x.id)));
15698
16466
  console.log(` ID: ${source_default.dim(x.id)}`);
15699
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
+ }
15700
16475
  console.log();
15701
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
+ }
15702
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) {
15703
16486
  const r = await listInsights(
15704
16487
  requireCredentials(),
@@ -15727,27 +16510,35 @@ var get6 = new Command("get").description("Get a backlog item").argument("<id>",
15727
16510
  const r = await getInsight(requireCredentials(), kind2, id);
15728
16511
  if (!r.ok || !r.data)
15729
16512
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15730
- 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));
15731
16515
  });
15732
- function write2(action71) {
15733
- const c = new Command(action71).description(
15734
- `${action71 === "create" ? "Create" : "Update"} a ${singular}`
16516
+ function write2(action74) {
16517
+ const c = new Command(action74).description(
16518
+ `${action74 === "create" ? "Create" : "Update"} a ${singular}`
15735
16519
  );
15736
- if (action71 === "update") c.argument("<id>", `${singular} ID`);
15737
- c.argument("[json]", "Inline JSON, @file, or file path").option(
15738
- "--file <path>",
15739
- "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
+ []
15740
16526
  );
15741
16527
  c.action(async function(...args) {
15742
16528
  const o = this.opts();
15743
- const id = action71 === "update" ? String(args[0]) : void 0;
15744
- const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
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;
15745
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
+ }
15746
16536
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
15747
- 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);
15748
16538
  if (!r.ok || !r.data)
15749
- fail(this, r.error ?? `Failed to ${action71}`, classifyStatus(r.status), r.errorDetails);
15750
- 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));
15751
16542
  });
15752
16543
  return c;
15753
16544
  }
@@ -15779,7 +16570,7 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15779
16570
  const readline3 = await import("node:readline");
15780
16571
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
15781
16572
  const answer = await new Promise(
15782
- (resolve) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve)
16573
+ (resolve2) => rl.question(source_default.yellow(`Delete backlog item ${id}? [y/N] `), resolve2)
15783
16574
  );
15784
16575
  rl.close();
15785
16576
  if (answer.toLowerCase() !== "y") {
@@ -15792,11 +16583,103 @@ var remove2 = new Command("delete").description("Delete a backlog item").argumen
15792
16583
  fail(this, r.error ?? "Failed to delete", classifyStatus(r.status), r.errorDetails);
15793
16584
  output(this, { deleted: true, id }, () => console.log(`Deleted ${id}`));
15794
16585
  });
15795
- var backlogCommand = new Command("backlog").description("Manage backlog").addCommand(list7).addCommand(get6).addCommand(write2("create")).addCommand(write2("update")).addCommand(archive).addCommand(restore).addCommand(remove2);
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);
15796
16679
 
15797
16680
  // src/commands/signals.ts
15798
- var import_node_fs18 = __toESM(require("node:fs"));
15799
- var import_node_path16 = __toESM(require("node:path"));
16681
+ var import_node_fs19 = __toESM(require("node:fs"));
16682
+ var import_node_path17 = __toESM(require("node:path"));
15800
16683
  init_source();
15801
16684
  var kind3 = "signals";
15802
16685
  var singular2 = "signal";
@@ -15804,7 +16687,7 @@ function parse2(command, value) {
15804
16687
  if (!value) return {};
15805
16688
  try {
15806
16689
  const source = value.startsWith("@") ? value.slice(1) : value;
15807
- const text = value.startsWith("@") || import_node_fs18.default.existsSync(source) ? import_node_fs18.default.readFileSync(import_node_path16.default.resolve(process.cwd(), source), "utf8") : value;
16690
+ const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
15808
16691
  const body5 = JSON.parse(text);
15809
16692
  if (!body5 || typeof body5 !== "object" || Array.isArray(body5))
15810
16693
  throw new Error("body must be an object");
@@ -15854,24 +16737,24 @@ var get7 = new Command("get").description("Get a signal").argument("<id>", "sign
15854
16737
  fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
15855
16738
  output(this, { signal: r.data }, () => detail3(r.data));
15856
16739
  });
15857
- function write3(action71) {
15858
- const c = new Command(action71).description(
15859
- `${action71 === "create" ? "Create" : "Update"} a ${singular2}`
16740
+ function write3(action74) {
16741
+ const c = new Command(action74).description(
16742
+ `${action74 === "create" ? "Create" : "Update"} a ${singular2}`
15860
16743
  );
15861
- if (action71 === "update") c.argument("<id>", `${singular2} ID`);
16744
+ if (action74 === "update") c.argument("<id>", `${singular2} ID`);
15862
16745
  c.argument("[json]", "Inline JSON, @file, or file path").option(
15863
16746
  "--file <path>",
15864
16747
  "JSON body file"
15865
16748
  );
15866
16749
  c.action(async function(...args) {
15867
16750
  const o = this.opts();
15868
- const id = action71 === "update" ? String(args[0]) : void 0;
15869
- const raw = o.file ? `@${o.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
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;
15870
16753
  const body5 = parse2(this, raw);
15871
16754
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
15872
- 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);
15873
16756
  if (!r.ok || !r.data)
15874
- 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);
15875
16758
  output(this, { signal: r.data }, () => detail3(r.data));
15876
16759
  });
15877
16760
  return c;
@@ -15886,14 +16769,14 @@ var signalsCommand = new Command("signals").description("Manage signals").addCom
15886
16769
 
15887
16770
  // src/commands/personalizations.ts
15888
16771
  init_source();
15889
- var import_node_fs19 = __toESM(require("node:fs"));
15890
- var import_node_path17 = __toESM(require("node:path"));
16772
+ var import_node_fs20 = __toESM(require("node:fs"));
16773
+ var import_node_path18 = __toESM(require("node:path"));
15891
16774
  var kind4 = "personalizations";
15892
16775
  function body2(command, value, opts) {
15893
16776
  let parsed = {};
15894
16777
  if (value) {
15895
16778
  const source = value.startsWith("@") ? value.slice(1) : value;
15896
- const text = value.startsWith("@") || import_node_fs19.default.existsSync(source) ? import_node_fs19.default.readFileSync(import_node_path17.default.resolve(process.cwd(), source), "utf8") : value;
16779
+ const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
15897
16780
  try {
15898
16781
  parsed = JSON.parse(text);
15899
16782
  } catch (e) {
@@ -15963,11 +16846,11 @@ function write4(name) {
15963
16846
  });
15964
16847
  return c;
15965
16848
  }
15966
- function lifecycle2(action71) {
15967
- return new Command(action71).description(`${action71[0].toUpperCase()}${action71.slice(1)} a personalization`).argument("<id>").action(async function(id) {
15968
- const r = await transitionManagedDelivery(requireCredentials(), kind4, id, action71);
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);
15969
16852
  if (!r.ok || !r.data)
15970
- 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);
15971
16854
  output(this, { personalization: r.data }, () => detail4(r.data));
15972
16855
  });
15973
16856
  }
@@ -15975,13 +16858,13 @@ var personalizationsCommand = new Command("personalizations").description("Manag
15975
16858
 
15976
16859
  // src/commands/targeting-resources.ts
15977
16860
  init_source();
15978
- var import_node_fs20 = __toESM(require("node:fs"));
15979
- var import_node_path18 = __toESM(require("node:path"));
16861
+ var import_node_fs21 = __toESM(require("node:fs"));
16862
+ var import_node_path19 = __toESM(require("node:path"));
15980
16863
  function body3(command, value) {
15981
16864
  if (!value) return {};
15982
16865
  const source = value.startsWith("@") ? value.slice(1) : value;
15983
16866
  try {
15984
- const text = value.startsWith("@") || import_node_fs20.default.existsSync(source) ? import_node_fs20.default.readFileSync(import_node_path18.default.resolve(process.cwd(), source), "utf8") : value;
16867
+ const text = value.startsWith("@") || import_node_fs21.default.existsSync(source) ? import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), source), "utf8") : value;
15985
16868
  return JSON.parse(text);
15986
16869
  } catch (error) {
15987
16870
  fail(
@@ -16049,21 +16932,21 @@ function targetingResourceCommand(kind5) {
16049
16932
  );
16050
16933
  })
16051
16934
  );
16052
- for (const action71 of ["create", "update"]) {
16053
- const command = new Command(action71).description(
16054
- `${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}`
16055
16938
  );
16056
- if (action71 === "update") command.argument("<id>");
16939
+ if (action74 === "update") command.argument("<id>");
16057
16940
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
16058
16941
  const opts = this.opts();
16059
- const id = action71 === "update" ? String(args[0]) : void 0;
16060
- const value = opts.file ? `@${opts.file}` : String(args[action71 === "update" ? 1 : 0] ?? "") || void 0;
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;
16061
16944
  const requestBody = body3(this, value);
16062
- 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)
16063
16946
  fail(this, `Updating a ${singular3} requires at least one field.`, EXIT_CODES.validation);
16064
16947
  const response = await requestTargetingResource(
16065
16948
  requireCredentials(),
16066
- action71 === "create" ? "POST" : "PATCH",
16949
+ action74 === "create" ? "POST" : "PATCH",
16067
16950
  kind5,
16068
16951
  id,
16069
16952
  requestBody
@@ -16071,7 +16954,7 @@ function targetingResourceCommand(kind5) {
16071
16954
  if (!response.ok || !response.data)
16072
16955
  fail(
16073
16956
  this,
16074
- response.error ?? `Failed to ${action71}`,
16957
+ response.error ?? `Failed to ${action74}`,
16075
16958
  classifyStatus(response.status),
16076
16959
  response.errorDetails
16077
16960
  );
@@ -16080,7 +16963,7 @@ function targetingResourceCommand(kind5) {
16080
16963
  {
16081
16964
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
16082
16965
  },
16083
- () => console.log(source_default.green(`${action71}d ${singular3}`))
16966
+ () => console.log(source_default.green(`${action74}d ${singular3}`))
16084
16967
  );
16085
16968
  });
16086
16969
  root.addCommand(command);
@@ -16101,8 +16984,8 @@ function targetingResourceCommand(kind5) {
16101
16984
  input: process.stdin,
16102
16985
  output: process.stderr
16103
16986
  });
16104
- const answer = await new Promise((resolve) => {
16105
- rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve);
16987
+ const answer = await new Promise((resolve2) => {
16988
+ rl.question(source_default.yellow(`Delete exclusion group "${id}"? [y/N] `), resolve2);
16106
16989
  });
16107
16990
  rl.close();
16108
16991
  if (answer.toLowerCase() !== "y") {
@@ -16154,7 +17037,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
16154
17037
  // src/commands/brand.ts
16155
17038
  init_source();
16156
17039
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
16157
- 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() {
16158
17041
  const creds = requireCredentials();
16159
17042
  const res = await getBrandContext(creds);
16160
17043
  if (res.status === 404) {
@@ -16187,7 +17070,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
16187
17070
 
16188
17071
  // src/commands/targeting.ts
16189
17072
  init_source();
16190
- var test2 = new Command("test").description("Evaluate experiment URL targeting for a page").argument("<url>", "URL to evaluate").option("--status <status>", "Comma-separated statuses to include, or all", "all").action(async function action60(url, opts) {
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) {
16191
17074
  const creds = requireCredentials();
16192
17075
  const res = await testTargeting(creds, url, { status: opts.status });
16193
17076
  if (!res.ok || !res.data) {
@@ -16232,8 +17115,8 @@ var targetingCommand = new Command("targeting").description("Test experiment tar
16232
17115
 
16233
17116
  // src/commands/sos.ts
16234
17117
  init_source();
16235
- var import_node_fs21 = __toESM(require("node:fs"));
16236
- var import_node_path19 = __toESM(require("node:path"));
17118
+ var import_node_fs22 = __toESM(require("node:fs"));
17119
+ var import_node_path20 = __toESM(require("node:path"));
16237
17120
  var SOS_TYPES = /* @__PURE__ */ new Set([
16238
17121
  "full_handover",
16239
17122
  "design",
@@ -16246,7 +17129,7 @@ var SOS_TYPES = /* @__PURE__ */ new Set([
16246
17129
  function readText(value) {
16247
17130
  if (!value) return void 0;
16248
17131
  if (!value.startsWith("@")) return value;
16249
- return import_node_fs21.default.readFileSync(import_node_path19.default.resolve(process.cwd(), value.slice(1)), "utf8");
17132
+ return import_node_fs22.default.readFileSync(import_node_path20.default.resolve(process.cwd(), value.slice(1)), "utf8");
16250
17133
  }
16251
17134
  function printTicket(ticket, reused) {
16252
17135
  console.log();
@@ -16272,7 +17155,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16272
17155
  "--type <type>",
16273
17156
  "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
16274
17157
  "qa"
16275
- ).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action61(descriptionArg, opts) {
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) {
16276
17159
  if (!SOS_TYPES.has(opts.type)) {
16277
17160
  fail(
16278
17161
  this,
@@ -16299,7 +17182,7 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
16299
17182
  }
16300
17183
  output(this, res.data, () => printCreatedTicket(res.data));
16301
17184
  });
16302
- 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() {
16303
17186
  const creds = requireCredentials();
16304
17187
  const res = await listMySos(creds);
16305
17188
  if (!res.ok || !res.data) {
@@ -16380,13 +17263,13 @@ var markRead = new Command("mark-read").option("--channel <key>", "Mark one chan
16380
17263
  var inboxCommand = new Command("inbox").description("Read and send inbox messages").addCommand(list11).addCommand(send2).addCommand(markRead);
16381
17264
 
16382
17265
  // src/commands/shops.ts
16383
- var import_node_fs22 = __toESM(require("node:fs"));
16384
- var import_node_path20 = __toESM(require("node:path"));
17266
+ var import_node_fs23 = __toESM(require("node:fs"));
17267
+ var import_node_path21 = __toESM(require("node:path"));
16385
17268
  function body4(command, raw) {
16386
17269
  if (!raw) return {};
16387
17270
  try {
16388
17271
  const source = raw.startsWith("@") ? raw.slice(1) : raw;
16389
- const text = raw.startsWith("@") || import_node_fs22.default.existsSync(source) ? import_node_fs22.default.readFileSync(import_node_path20.default.resolve(source), "utf8") : raw;
17272
+ const text = raw.startsWith("@") || import_node_fs23.default.existsSync(source) ? import_node_fs23.default.readFileSync(import_node_path21.default.resolve(source), "utf8") : raw;
16390
17273
  const value = JSON.parse(text);
16391
17274
  if (!value || typeof value !== "object" || Array.isArray(value))
16392
17275
  throw new Error("body must be an object");
@@ -16448,7 +17331,7 @@ var create5 = new Command("create").description("Create a shop").argument("[json
16448
17331
  const r = await createShop(requireUserCredentials(), payload);
16449
17332
  done(this, r, "create shop");
16450
17333
  });
16451
- 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) {
16452
17335
  const b = raw === void 0 ? {} : body4(this, raw);
16453
17336
  if (o.holdoutEnabled && o.holdoutDisabled)
16454
17337
  fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
@@ -16581,7 +17464,7 @@ shopware.addCommand(
16581
17464
  );
16582
17465
  })
16583
17466
  );
16584
- 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(
16585
17468
  settings("auto-stop", "Read or update auto-stop defaults", { on: "enable", off: "disable" })
16586
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);
16587
17470
 
@@ -16589,6 +17472,7 @@ var shopsCommand = new Command("shops").description("Manage organization shops")
16589
17472
  function optionSchema(option) {
16590
17473
  return {
16591
17474
  flags: option.flags,
17475
+ ...option.argChoices ? { choices: option.argChoices } : {},
16592
17476
  description: option.description,
16593
17477
  required: option.required,
16594
17478
  optional: option.optional,
@@ -16614,7 +17498,7 @@ function commandSchema(command) {
16614
17498
  };
16615
17499
  }
16616
17500
  function createSchemaCommand(program3) {
16617
- 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() {
16618
17502
  const schema = commandSchema(program3);
16619
17503
  output(this, schema, () => {
16620
17504
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
@@ -17029,7 +17913,7 @@ ${event.event || "(unnamed event)"} [${event.dlName}] \u2014 ${event.count} obse
17029
17913
  return `${lines.join("\n")}
17030
17914
  `;
17031
17915
  }
17032
- var discoverCommand = new Command("discover").description("Read back the tracking schema observed for the current shop").option("--minutes <n>", "Observation window in minutes").option("--gtm <url-or-id>", "Mine a public page or GTM container locally").option("--gtm-url <url>", "Fetch an explicit GTM container URL, including first-party proxies").option("--create-dimension <path>", "Create a custom dimension from an observed dataLayer path").option("--event <name>", "Select the observed event when a path appears on multiple events").option("--key <signal-key>", "Override the dimension signal key").option("--label <text>", "Override the dimension label").action(async function action63(opts) {
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) {
17033
17917
  if (opts.gtm && opts.gtmUrl)
17034
17918
  fail(this, "Use only one of --gtm or --gtm-url", EXIT_CODES.validation);
17035
17919
  if (opts.createDimension && (opts.gtm || opts.gtmUrl)) {
@@ -17199,16 +18083,16 @@ var discoverCommand = new Command("discover").description("Read back the trackin
17199
18083
  });
17200
18084
 
17201
18085
  // src/commands/skills.ts
17202
- var import_node_fs23 = __toESM(require("node:fs"));
17203
- var import_node_path21 = __toESM(require("node:path"));
18086
+ var import_node_fs24 = __toESM(require("node:fs"));
18087
+ var import_node_path22 = __toESM(require("node:path"));
17204
18088
  function packageRoot() {
17205
18089
  const candidates = [
17206
- import_node_path21.default.resolve(__dirname, ".."),
17207
- import_node_path21.default.resolve(import_node_path21.default.dirname(process.argv[1] ?? process.cwd()), ".."),
17208
- import_node_path21.default.resolve(process.cwd(), "packages/cli")
18090
+ import_node_path22.default.resolve(__dirname, ".."),
18091
+ import_node_path22.default.resolve(import_node_path22.default.dirname(process.argv[1] ?? process.cwd()), ".."),
18092
+ import_node_path22.default.resolve(process.cwd(), "packages/cli")
17209
18093
  ];
17210
18094
  for (const candidate of candidates) {
17211
- if (import_node_fs23.default.existsSync(import_node_path21.default.join(candidate, "AGENTS.md"))) return candidate;
18095
+ if (import_node_fs24.default.existsSync(import_node_path22.default.join(candidate, "AGENTS.md"))) return candidate;
17212
18096
  }
17213
18097
  throw new Error(
17214
18098
  `Apex CLI package files not found. Looked for AGENTS.md in: ${candidates.join(", ")}`
@@ -17217,22 +18101,22 @@ function packageRoot() {
17217
18101
  function skillPaths() {
17218
18102
  const root = packageRoot();
17219
18103
  return {
17220
- agents: import_node_path21.default.join(root, "AGENTS.md"),
17221
- skill: import_node_path21.default.join(root, "skills", "apex-cli", "SKILL.md")
18104
+ agents: import_node_path22.default.join(root, "AGENTS.md"),
18105
+ skill: import_node_path22.default.join(root, "skills", "apex-cli", "SKILL.md")
17222
18106
  };
17223
18107
  }
17224
18108
  function installSkill(paths, targetRoot) {
17225
- const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs23.default.existsSync(filePath));
18109
+ const missing = [paths.skill, paths.agents].filter((filePath) => !import_node_fs24.default.existsSync(filePath));
17226
18110
  if (missing.length > 0) {
17227
18111
  throw new Error(`Apex CLI package files not found. Missing: ${missing.join(", ")}`);
17228
18112
  }
17229
- const target = import_node_path21.default.resolve(targetRoot, "apex-cli");
17230
- import_node_fs23.default.mkdirSync(target, { recursive: true });
17231
- import_node_fs23.default.copyFileSync(paths.skill, import_node_path21.default.join(target, "SKILL.md"));
17232
- import_node_fs23.default.copyFileSync(paths.agents, import_node_path21.default.join(target, "AGENTS.md"));
18113
+ const target = import_node_path22.default.resolve(targetRoot, "apex-cli");
18114
+ import_node_fs24.default.mkdirSync(target, { recursive: true });
18115
+ import_node_fs24.default.copyFileSync(paths.skill, import_node_path22.default.join(target, "SKILL.md"));
18116
+ import_node_fs24.default.copyFileSync(paths.agents, import_node_path22.default.join(target, "AGENTS.md"));
17233
18117
  return target;
17234
18118
  }
17235
- var skillsCommand = new Command("skills").description("Print or install the Apex CLI agent skill files").option("--install", "Copy the Apex CLI skill into ./.claude/skills/apex-cli").option("--target <dir>", "Install target skills directory", ".claude/skills").option("--json", "Print JSON").action(function action64(opts) {
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) {
17236
18120
  let paths;
17237
18121
  try {
17238
18122
  paths = skillPaths();
@@ -17329,8 +18213,8 @@ async function confirmArchive(command, shopId, force) {
17329
18213
  }
17330
18214
  const readline3 = await import("node:readline");
17331
18215
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17332
- const answer = await new Promise((resolve) => {
17333
- rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve);
18216
+ const answer = await new Promise((resolve2) => {
18217
+ rl.question("Archive this shop? Apex delivery will be disabled. [y/N] ", resolve2);
17334
18218
  });
17335
18219
  rl.close();
17336
18220
  if (answer.toLowerCase() !== "y") {
@@ -17384,7 +18268,7 @@ async function confirm(c, force, label, retry) {
17384
18268
  const readline3 = await import("node:readline");
17385
18269
  const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
17386
18270
  const answer = await new Promise(
17387
- (resolve) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve)
18271
+ (resolve2) => rl.question(source_default.yellow(`${label}? [y/N] `), resolve2)
17388
18272
  );
17389
18273
  rl.close();
17390
18274
  if (answer.toLowerCase() !== "y") {
@@ -17856,7 +18740,7 @@ function selectedEvents(opts) {
17856
18740
  }
17857
18741
  var list14 = new Command("list").alias("ls").description(
17858
18742
  "List webhooks. JSON includes availableEvents; secrets are never returned after create."
17859
- ).action(async function action65() {
18743
+ ).action(async function action68() {
17860
18744
  const data = await loadWebhookIndex(this);
17861
18745
  output(this, data, () => printWebhooks(data.data));
17862
18746
  });
@@ -17865,7 +18749,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
17865
18749
  "Webhook event to subscribe to; repeat for multiple events",
17866
18750
  (value, previous = []) => [...previous, value],
17867
18751
  []
17868
- ).action(async function action66(opts) {
18752
+ ).action(async function action69(opts) {
17869
18753
  const events = selectedEvents(opts);
17870
18754
  if (events.length === 0) fail(this, "At least one --event is required", EXIT_CODES.validation);
17871
18755
  const index = await loadWebhookIndex(this);
@@ -17897,7 +18781,7 @@ var create7 = new Command("create").description(`Create a webhook. Prints the si
17897
18781
  }
17898
18782
  );
17899
18783
  });
17900
- 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) {
17901
18785
  requireShopSecretKey(this);
17902
18786
  const result = handleResult(
17903
18787
  this,
@@ -17906,7 +18790,7 @@ var remove4 = new Command("delete").alias("rm").description("Delete a webhook").
17906
18790
  );
17907
18791
  output(this, result, () => console.log(source_default.green(`Deleted webhook ${id}`)));
17908
18792
  });
17909
- var deliveries = new Command("deliveries").description("List webhook deliveries including failure and disabled state").argument("<id>", "Webhook id").option("--limit <n>", "Maximum deliveries to show", "20").action(async function action68(id, opts) {
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) {
17910
18794
  const limit = Number.parseInt(opts.limit, 10);
17911
18795
  if (!Number.isInteger(limit) || limit < 1)
17912
18796
  fail(this, "--limit must be a positive integer", EXIT_CODES.validation);
@@ -17918,12 +18802,12 @@ var deliveries = new Command("deliveries").description("List webhook deliveries
17918
18802
  );
17919
18803
  output(this, result, () => printDeliveries(result.data, result.webhook));
17920
18804
  });
17921
- 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(
17922
18806
  "--event <event>",
17923
18807
  "Replacement event; repeat for multiple events",
17924
18808
  (value, previous = []) => [...previous, value],
17925
18809
  []
17926
- ).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) {
17927
18811
  if (opts.active && opts.inactive)
17928
18812
  fail(this, "Use only one of --active or --inactive", EXIT_CODES.validation);
17929
18813
  const events = selectedEvents(opts);
@@ -17956,7 +18840,7 @@ var update4 = new Command("update").description("Update a webhook URL, events, o
17956
18840
  );
17957
18841
  output(this, { webhook: result }, () => printWebhooks([result]));
17958
18842
  });
17959
- 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) {
17960
18844
  requireShopSecretKey(this);
17961
18845
  const result = handleResult(
17962
18846
  this,
@@ -17971,14 +18855,14 @@ var test3 = new Command("test").description("Send a signed test event to a webho
17971
18855
  )
17972
18856
  );
17973
18857
  });
17974
- 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);
17975
18859
 
17976
18860
  // src/commands/simulate.ts
17977
18861
  init_source();
17978
- var import_node_fs24 = __toESM(require("node:fs"));
17979
- var import_node_path22 = __toESM(require("node:path"));
18862
+ var import_node_fs25 = __toESM(require("node:fs"));
18863
+ var import_node_path23 = __toESM(require("node:path"));
17980
18864
  function readJson2(file) {
17981
- const text = file === "-" ? import_node_fs24.default.readFileSync(0, "utf8") : import_node_fs24.default.readFileSync(import_node_path22.default.resolve(file), "utf8");
18865
+ const text = file === "-" ? import_node_fs25.default.readFileSync(0, "utf8") : import_node_fs25.default.readFileSync(import_node_path23.default.resolve(file), "utf8");
17982
18866
  try {
17983
18867
  return JSON.parse(text);
17984
18868
  } catch (error) {
@@ -18100,8 +18984,8 @@ var simulateCommand = new Command("simulate").description("Simulate data-layer t
18100
18984
  });
18101
18985
 
18102
18986
  // src/commands/config-resources.ts
18103
- var import_node_fs25 = __toESM(require("node:fs"));
18104
- var import_node_path23 = __toESM(require("node:path"));
18987
+ var import_node_fs26 = __toESM(require("node:fs"));
18988
+ var import_node_path24 = __toESM(require("node:path"));
18105
18989
  function show(resource) {
18106
18990
  process.stdout.write(`${JSON.stringify(resource, null, 2)}
18107
18991
  `);
@@ -18127,8 +19011,8 @@ async function confirmAssetDelete(id) {
18127
19011
  input: process.stdin,
18128
19012
  output: process.stderr
18129
19013
  });
18130
- const answer = await new Promise((resolve) => {
18131
- rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve);
19014
+ const answer = await new Promise((resolve2) => {
19015
+ rl.question(`Delete asset "${id}"? This cannot be undone. [y/N] `, resolve2);
18132
19016
  });
18133
19017
  rl.close();
18134
19018
  return answer.trim().toLowerCase() === "y";
@@ -18174,12 +19058,12 @@ function readFamily(family, label) {
18174
19058
  }
18175
19059
  var assetReads = readFamily("assets", "assets");
18176
19060
  var upload = new Command("upload").description("Upload an asset").argument("<file>").action(async function(file) {
18177
- const absolute = import_node_path23.default.resolve(process.cwd(), file);
18178
- if (!import_node_fs25.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
18179
- const fileStat = import_node_fs25.default.statSync(absolute);
19061
+ const absolute = import_node_path24.default.resolve(process.cwd(), file);
19062
+ if (!import_node_fs26.default.existsSync(absolute)) fail(this, `File not found: ${file}`, 3);
19063
+ const fileStat = import_node_fs26.default.statSync(absolute);
18180
19064
  if (!fileStat.isFile()) fail(this, `File not found: ${file}`, 3);
18181
19065
  if (fileStat.size > MAX_ASSET_SIZE_BYTES) fail(this, `${file} is larger than 10 MB`, 3);
18182
- const mimeType = ASSET_MIME_TYPES[import_node_path23.default.extname(absolute).toLowerCase()];
19066
+ const mimeType = ASSET_MIME_TYPES[import_node_path24.default.extname(absolute).toLowerCase()];
18183
19067
  if (!mimeType)
18184
19068
  fail(
18185
19069
  this,
@@ -18188,8 +19072,8 @@ var upload = new Command("upload").description("Upload an asset").argument("<fil
18188
19072
  );
18189
19073
  const r = await uploadAssets(requireCredentials(), [
18190
19074
  {
18191
- name: import_node_path23.default.basename(absolute),
18192
- bytes: import_node_fs25.default.readFileSync(absolute),
19075
+ name: import_node_path24.default.basename(absolute),
19076
+ bytes: import_node_fs26.default.readFileSync(absolute),
18193
19077
  type: mimeType
18194
19078
  }
18195
19079
  ]);
@@ -18279,6 +19163,7 @@ program2.addCommand(funnelsCommand);
18279
19163
  program2.addCommand(environmentsCommand);
18280
19164
  program2.addCommand(consentCommand);
18281
19165
  program2.addCommand(triggersCommand);
19166
+ program2.addCommand(projectCodeCommand);
18282
19167
  program2.addCommand(domainsCommand);
18283
19168
  program2.addCommand(handoverCommand);
18284
19169
  program2.addCommand(analyticsCommand);