@drip-apex/apex-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.js +313 -101
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,6 +28,9 @@ DRIP_API_KEY=apx_your_key_here apex experiments list --json
28
28
 
29
29
  ```bash
30
30
  apex experiments list
31
+ apex experiments create --name "Redirect test" --type redirect \
32
+ --source-url "https://shop.example/products/source" \
33
+ --destination "https://landing.example/products/treatment"
31
34
  apex experiments pull <id-or-slug> --dir ./my-test
32
35
  apex experiments plan ./my-test --json
33
36
  apex experiments apply ./my-test --json
package/dist/cli.js CHANGED
@@ -6743,16 +6743,16 @@ __export(ora_exports, {
6743
6743
  function ora(options) {
6744
6744
  return new Ora(options);
6745
6745
  }
6746
- async function oraPromise(action24, options) {
6747
- const actionIsFunction = typeof action24 === "function";
6748
- const actionIsPromise = typeof action24.then === "function";
6746
+ async function oraPromise(action25, options) {
6747
+ const actionIsFunction = typeof action25 === "function";
6748
+ const actionIsPromise = typeof action25.then === "function";
6749
6749
  if (!actionIsFunction && !actionIsPromise) {
6750
6750
  throw new TypeError("Parameter `action` must be a Function or a Promise");
6751
6751
  }
6752
6752
  const { successText, failText } = typeof options === "object" ? options : { successText: void 0, failText: void 0 };
6753
6753
  const spinner = ora(options).start();
6754
6754
  try {
6755
- const promise = actionIsFunction ? action24(spinner) : action24;
6755
+ const promise = actionIsFunction ? action25(spinner) : action25;
6756
6756
  const result = await promise;
6757
6757
  spinner.succeed(
6758
6758
  successText === void 0 ? void 0 : typeof successText === "string" ? successText : successText(result)
@@ -7090,7 +7090,7 @@ init_source();
7090
7090
 
7091
7091
  // src/commands/login.ts
7092
7092
  init_source();
7093
- var import_node_crypto = __toESM(require("node:crypto"));
7093
+ var import_node_crypto2 = __toESM(require("node:crypto"));
7094
7094
  var import_node_http = __toESM(require("node:http"));
7095
7095
  var import_node_os4 = __toESM(require("node:os"));
7096
7096
  var import_node_readline = __toESM(require("node:readline"));
@@ -7356,6 +7356,7 @@ function findConfigPath() {
7356
7356
  }
7357
7357
 
7358
7358
  // src/api.ts
7359
+ var import_node_crypto = require("node:crypto");
7359
7360
  var mockEnvironmentPatchAttempts = 0;
7360
7361
  var mockEnvironmentReads = 0;
7361
7362
  function extractErrorMessage(json, status2) {
@@ -7484,6 +7485,45 @@ function mockResponse(method, path20, body5) {
7484
7485
  if (process.env.APEX_CLI_MOCK_API !== "1") return null;
7485
7486
  const url = new URL(path20, "https://mock.apex.local");
7486
7487
  const exp = mockExperiment();
7488
+ if (method === "GET" && url.pathname === `/api/v1/experiments/${exp.id}/filesystem`) {
7489
+ const fixtures = [
7490
+ ["experiment.json", '{"schemaVersion":1,"name":"Fixture Test"}\n'],
7491
+ ["targeting.json", '{"url":[],"segments":[]}\n'],
7492
+ ["experiment/index.js", "window.__apexExperiment = true;\n"],
7493
+ ["experiment/reset.js", "delete window.__apexExperiment;\n"],
7494
+ ["experiment/index.css", ".apex-experiment { display: block; }\n"],
7495
+ ["metrics.json", '{"primary":null,"secondary":[]}\n'],
7496
+ ["qa.json", '{"status":"pending"}\n'],
7497
+ ["launch.json", '{"status":"draft"}\n'],
7498
+ ["variations/control/variation.json", '{"slug":"control","isControl":true}\n'],
7499
+ ["variations/control/mutations.json", "[]\n"],
7500
+ ["variations/control/index.css", ".control { color: inherit; }\n"],
7501
+ ["variations/control/index.js", "window.__apexControl = true;\n"],
7502
+ ["variations/control/reset.js", "delete window.__apexControl;\n"],
7503
+ ["variations/control/anti-flicker.json", "[]\n"]
7504
+ ];
7505
+ const invalidPaths = {
7506
+ invalid: "../secret",
7507
+ traversal: "variations/../../x",
7508
+ absolute: "/abs/path"
7509
+ };
7510
+ const invalidPath = invalidPaths[process.env.APEX_CLI_MOCK_FILESYSTEM_PATHS ?? ""];
7511
+ if (invalidPath) {
7512
+ fixtures.push([invalidPath, "must not be written\n"]);
7513
+ }
7514
+ return {
7515
+ ok: true,
7516
+ status: 200,
7517
+ data: {
7518
+ rootName: "fixture-test",
7519
+ files: fixtures.map(([filePath, content]) => ({
7520
+ path: filePath,
7521
+ content,
7522
+ sha256: (0, import_node_crypto.createHash)("sha256").update(content).digest("hex")
7523
+ }))
7524
+ }
7525
+ };
7526
+ }
7487
7527
  const configMatch = url.pathname.match(
7488
7528
  /^\/api\/v1\/(assets|landing-pages)(?:\/([^/]+))?$/
7489
7529
  );
@@ -7663,16 +7703,16 @@ function mockResponse(method, path20, body5) {
7663
7703
  /^\/api\/v1\/(flags|personalizations)(?:\/([^/]+))?(?:\/(activate|pause|archive|status))?$/
7664
7704
  );
7665
7705
  if (managedMatch) {
7666
- const [, kind5, id = `${kind5.slice(0, -1)}_1`, action24] = managedMatch;
7706
+ const [, kind5, id = `${kind5.slice(0, -1)}_1`, action25] = managedMatch;
7667
7707
  const input = body5 ?? {};
7668
7708
  const item = {
7669
7709
  id,
7670
7710
  key: String(input.key ?? `fixture-${kind5.slice(0, -1)}`),
7671
7711
  name: String(input.name ?? `Fixture ${kind5.slice(0, -1)}`),
7672
- status: action24 === "activate" ? "active" : action24 === "pause" ? "paused" : action24 === "archive" ? "archived" : "draft",
7712
+ status: action25 === "activate" ? "active" : action25 === "pause" ? "paused" : action25 === "archive" ? "archived" : "draft",
7673
7713
  updated_at: "2026-07-19T00:00:00.000Z"
7674
7714
  };
7675
- if (kind5 === "flags" && action24 === "status")
7715
+ if (kind5 === "flags" && action25 === "status")
7676
7716
  return {
7677
7717
  ok: true,
7678
7718
  status: 200,
@@ -7705,11 +7745,11 @@ function mockResponse(method, path20, body5) {
7705
7745
  /^\/api\/v1\/(backlog|signals)(?:\/([^/]+))?(?:\/(archive|restore))?$/
7706
7746
  );
7707
7747
  if (insightMatch) {
7708
- const [, kind5, id = `${kind5}_1`, action24] = insightMatch;
7748
+ const [, kind5, id = `${kind5}_1`, action25] = insightMatch;
7709
7749
  const input = body5 ?? {};
7710
- if (action24 && (kind5 !== "backlog" || method !== "POST"))
7711
- return { ok: false, status: 405, error: `${action24} requires POST` };
7712
- const archived = action24 === "archive" || method === "DELETE" || url.searchParams.get("include_archived") === "true";
7750
+ if (action25 && (kind5 !== "backlog" || method !== "POST"))
7751
+ return { ok: false, status: 405, error: `${action25} requires POST` };
7752
+ const archived = action25 === "archive" || method === "DELETE" || url.searchParams.get("include_archived") === "true";
7713
7753
  const item = {
7714
7754
  id,
7715
7755
  ...kind5 === "backlog" ? {
@@ -7735,7 +7775,7 @@ function mockResponse(method, path20, body5) {
7735
7775
  return { ok: true, status: 200, data: { ok: true } };
7736
7776
  return {
7737
7777
  ok: true,
7738
- status: method === "POST" && !action24 ? 201 : 200,
7778
+ status: method === "POST" && !action25 ? 201 : 200,
7739
7779
  data: item
7740
7780
  };
7741
7781
  }
@@ -7749,6 +7789,38 @@ function mockResponse(method, path20, body5) {
7749
7789
  }
7750
7790
  };
7751
7791
  }
7792
+ if (method === "POST" && url.pathname === "/api/v1/experiments") {
7793
+ const input = body5 ?? {};
7794
+ if (input.experiment_type === "redirect" && process.env.APEX_CLI_MOCK_REDIRECT_DISABLED === "1") {
7795
+ return {
7796
+ ok: false,
7797
+ status: 403,
7798
+ error: "Redirect tests are not enabled for this shop"
7799
+ };
7800
+ }
7801
+ const requestedVariations = Array.isArray(input.variations) ? input.variations : exp.variations ?? [];
7802
+ return {
7803
+ ok: true,
7804
+ status: 201,
7805
+ data: {
7806
+ ...exp,
7807
+ name: String(input.name ?? exp.name),
7808
+ slug: typeof input.slug === "string" || input.slug === null ? input.slug : exp.slug,
7809
+ experiment_type: input.experiment_type === "redirect" ? "redirect" : "mutation",
7810
+ traffic_allocation: typeof input.traffic_allocation === "number" ? input.traffic_allocation : exp.traffic_allocation,
7811
+ store_url: typeof input.store_url === "string" ? input.store_url : null,
7812
+ targeting: input.targeting ?? null,
7813
+ variations: requestedVariations.map((variation, index) => ({
7814
+ id: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
7815
+ name: String(variation.name ?? `Variation ${index + 1}`),
7816
+ weight: typeof variation.weight === "number" ? variation.weight : 0,
7817
+ is_control: variation.is_control === true,
7818
+ redirect_url: typeof variation.redirect_url === "string" ? variation.redirect_url : null,
7819
+ mutations: []
7820
+ }))
7821
+ }
7822
+ };
7823
+ }
7752
7824
  if (method === "GET" && (url.pathname === `/api/v1/experiments/${exp.id}` || url.pathname === `/api/v1/experiments/by-slug/${exp.slug}`)) {
7753
7825
  return { ok: true, status: 200, data: exp };
7754
7826
  }
@@ -7822,6 +7894,7 @@ function mockResponse(method, path20, body5) {
7822
7894
  }
7823
7895
  };
7824
7896
  }
7897
+ const observedPaths = files.map((file) => file.path).filter((filePath) => typeof filePath === "string");
7825
7898
  return {
7826
7899
  ok: true,
7827
7900
  status: 200,
@@ -7841,7 +7914,8 @@ function mockResponse(method, path20, body5) {
7841
7914
  errors: [],
7842
7915
  warnings: [],
7843
7916
  invalidatesQa: false,
7844
- precompileRequired: false
7917
+ precompileRequired: false,
7918
+ ...process.env.APEX_CLI_MOCK_FILESYSTEM_PLAN === "echo-paths" ? { observedPaths } : {}
7845
7919
  }
7846
7920
  };
7847
7921
  }
@@ -8497,8 +8571,8 @@ function requestTargetingResource(creds2, method, kind5, id, body5, limit, curso
8497
8571
  const path20 = `/api/v1/${kind5}${id ? `/${id.split("/").map(encodeURIComponent).join("/")}` : ""}${query ? `?${query}` : ""}`;
8498
8572
  return request(creds2, method, path20, body5);
8499
8573
  }
8500
- function managedPath(kind5, id, action24) {
8501
- return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action24 ? `/${action24}` : ""}`;
8574
+ function managedPath(kind5, id, action25) {
8575
+ return `/api/v1/${kind5}${id ? `/${encodeURIComponent(id)}` : ""}${action25 ? `/${action25}` : ""}`;
8502
8576
  }
8503
8577
  function listManagedDeliveries(creds2, kind5, limit) {
8504
8578
  const q = limit ? `?limit=${limit}` : "";
@@ -8520,8 +8594,8 @@ function createManagedDelivery(creds2, kind5, body5) {
8520
8594
  function updateManagedDelivery(creds2, kind5, id, body5) {
8521
8595
  return request(creds2, "PATCH", managedPath(kind5, id), body5);
8522
8596
  }
8523
- function transitionManagedDelivery(creds2, kind5, id, action24) {
8524
- return request(creds2, "POST", managedPath(kind5, id, action24));
8597
+ function transitionManagedDelivery(creds2, kind5, id, action25) {
8598
+ return request(creds2, "POST", managedPath(kind5, id, action25));
8525
8599
  }
8526
8600
  function listConfigResources(creds2, family, opts) {
8527
8601
  const params = new URLSearchParams();
@@ -8784,7 +8858,7 @@ function parseFormBody(body5) {
8784
8858
  return new URLSearchParams(body5);
8785
8859
  }
8786
8860
  async function waitForBrowserLogin(baseUrl, userMode = false) {
8787
- const state = import_node_crypto.default.randomBytes(24).toString("base64url");
8861
+ const state = import_node_crypto2.default.randomBytes(24).toString("base64url");
8788
8862
  const clientName = import_node_os4.default.hostname() || "Local machine";
8789
8863
  return new Promise((resolve, reject) => {
8790
8864
  let settled = false;
@@ -9013,7 +9087,7 @@ var whoamiCommand = new Command("whoami").description("Show current authenticati
9013
9087
 
9014
9088
  // src/commands/experiments.ts
9015
9089
  init_source();
9016
- var import_node_crypto2 = __toESM(require("node:crypto"));
9090
+ var import_node_crypto3 = __toESM(require("node:crypto"));
9017
9091
  var import_node_fs6 = __toESM(require("node:fs"));
9018
9092
  var import_node_path4 = __toESM(require("node:path"));
9019
9093
  function formatDate(iso) {
@@ -9084,9 +9158,25 @@ function printExperimentResults(results2) {
9084
9158
  }
9085
9159
  console.log();
9086
9160
  }
9087
- var FILESYSTEM_FILE_RE = /^(experiment\.json|targeting\.json|metrics\.json|qa\.json|launch\.json|variations\/[^/]+\/(?:variation\.json|mutations\.json|index\.css|index\.js|reset\.js|anti-flicker\.json))$/;
9161
+ var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
9162
+ topLevel: ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"],
9163
+ experiment: ["index.css", "index.js", "reset.js"],
9164
+ variation: [
9165
+ "variation.json",
9166
+ "mutations.json",
9167
+ "index.css",
9168
+ "index.js",
9169
+ "reset.js",
9170
+ "anti-flicker.json"
9171
+ ]
9172
+ };
9173
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9174
+ var pathAlternation = (paths) => paths.map(escapeRegex).join("|");
9175
+ var FILESYSTEM_FILE_RE = new RegExp(
9176
+ `^(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel)}|experiment/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment)})|variations/[^/]+/(?:${pathAlternation(EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation)}))$`
9177
+ );
9088
9178
  function sha256Text(value) {
9089
- return import_node_crypto2.default.createHash("sha256").update(value).digest("hex");
9179
+ return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
9090
9180
  }
9091
9181
  function folderPath(folder) {
9092
9182
  return import_node_path4.default.resolve(process.cwd(), folder);
@@ -9126,8 +9216,12 @@ function validateFilesystemPath(relativePath) {
9126
9216
  return normalized;
9127
9217
  }
9128
9218
  function writeExperimentFilesystemFiles(folder, files) {
9129
- for (const file of files) {
9130
- const relativePath = validateFilesystemPath(file.path);
9219
+ const validatedFiles = files.map((file) => ({
9220
+ ...file,
9221
+ relativePath: validateFilesystemPath(file.path)
9222
+ }));
9223
+ for (const file of validatedFiles) {
9224
+ const relativePath = file.relativePath;
9131
9225
  const absPath = import_node_path4.default.join(folder, relativePath);
9132
9226
  import_node_fs6.default.mkdirSync(import_node_path4.default.dirname(absPath), { recursive: true });
9133
9227
  import_node_fs6.default.writeFileSync(absPath, file.content, "utf8");
@@ -9135,26 +9229,25 @@ function writeExperimentFilesystemFiles(folder, files) {
9135
9229
  }
9136
9230
  function listExperimentFilesystemFiles(folder) {
9137
9231
  const files = [];
9138
- const topLevel = ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"];
9139
- for (const fileName of topLevel) {
9232
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.topLevel) {
9140
9233
  const absPath = import_node_path4.default.join(folder, fileName);
9141
9234
  if (!import_node_fs6.default.existsSync(absPath)) continue;
9142
9235
  const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9143
9236
  files.push({ path: fileName, content, sha256: sha256Text(content) });
9144
9237
  }
9238
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.experiment) {
9239
+ const relativePath = `experiment/${fileName}`;
9240
+ const absPath = import_node_path4.default.join(folder, relativePath);
9241
+ if (!import_node_fs6.default.existsSync(absPath)) continue;
9242
+ const content = import_node_fs6.default.readFileSync(absPath, "utf8");
9243
+ files.push({ path: relativePath, content, sha256: sha256Text(content) });
9244
+ }
9145
9245
  const variationsRoot = import_node_path4.default.join(folder, "variations");
9146
9246
  if (import_node_fs6.default.existsSync(variationsRoot)) {
9147
9247
  for (const variationDir of import_node_fs6.default.readdirSync(variationsRoot, { withFileTypes: true })) {
9148
9248
  if (!variationDir.isDirectory()) continue;
9149
9249
  const base = import_node_path4.default.join(variationsRoot, variationDir.name);
9150
- for (const fileName of [
9151
- "variation.json",
9152
- "mutations.json",
9153
- "index.css",
9154
- "index.js",
9155
- "reset.js",
9156
- "anti-flicker.json"
9157
- ]) {
9250
+ for (const fileName of EXPERIMENT_FILESYSTEM_PATH_SCHEMA.variation) {
9158
9251
  const absPath = import_node_path4.default.join(base, fileName);
9159
9252
  if (!import_node_fs6.default.existsSync(absPath)) continue;
9160
9253
  const relativePath = validateFilesystemPath(
@@ -9365,23 +9458,134 @@ var results = new Command("results").description("Read experiment results and ti
9365
9458
  fail(this, error instanceof Error ? error.message : String(error));
9366
9459
  }
9367
9460
  });
9368
- 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("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async (opts) => {
9461
+ function collectCreateOption(value, previous) {
9462
+ return [...previous, value];
9463
+ }
9464
+ function parseHttpDestination(value) {
9465
+ const trimmed = value.trim();
9466
+ try {
9467
+ const url = new URL(trimmed);
9468
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error();
9469
+ } catch {
9470
+ throw new CliError(
9471
+ `Invalid redirect destination "${value}": expected an HTTP(S) URL.`,
9472
+ EXIT_CODES.validation
9473
+ );
9474
+ }
9475
+ return trimmed;
9476
+ }
9477
+ function sourceStoreUrl(sourceRules) {
9478
+ for (const rule of sourceRules) {
9479
+ if (rule.includes("*")) continue;
9480
+ try {
9481
+ const url = new URL(rule);
9482
+ if (url.protocol === "http:" || url.protocol === "https:") {
9483
+ return rule;
9484
+ }
9485
+ } catch {
9486
+ }
9487
+ }
9488
+ return null;
9489
+ }
9490
+ 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(
9491
+ "--source-url <pattern>",
9492
+ "Positive source URL rule (repeatable; accepts exact URLs, paths, or wildcards)",
9493
+ collectCreateOption,
9494
+ []
9495
+ ).option(
9496
+ "--destination <url>",
9497
+ "Redirect treatment destination (repeatable; creates one treatment per URL)",
9498
+ collectCreateOption,
9499
+ []
9500
+ ).option("--traffic <pct>", "Traffic allocation (0-100)", "100").action(async function action7(opts) {
9501
+ if (opts.type !== "mutation" && opts.type !== "redirect") {
9502
+ fail(
9503
+ this,
9504
+ "--type must be mutation or redirect",
9505
+ EXIT_CODES.validation
9506
+ );
9507
+ }
9508
+ const traffic = Number(opts.traffic);
9509
+ if (!Number.isFinite(traffic) || traffic < 0 || traffic > 100) {
9510
+ fail(
9511
+ this,
9512
+ "--traffic must be a number from 0 to 100",
9513
+ EXIT_CODES.validation
9514
+ );
9515
+ }
9516
+ const sourceRules = opts.sourceUrl.map((value) => value.trim());
9517
+ if (sourceRules.some((value) => value.length === 0)) {
9518
+ fail(this, "--source-url cannot be empty", EXIT_CODES.validation);
9519
+ }
9520
+ if (opts.type !== "redirect" && opts.destination.length > 0) {
9521
+ fail(
9522
+ this,
9523
+ "--destination is only valid with --type redirect",
9524
+ EXIT_CODES.validation
9525
+ );
9526
+ }
9527
+ if (opts.type === "redirect" && sourceRules.length === 0) {
9528
+ fail(
9529
+ this,
9530
+ "Redirect experiments require at least one --source-url rule",
9531
+ EXIT_CODES.validation
9532
+ );
9533
+ }
9534
+ if (opts.type === "redirect" && opts.destination.length === 0) {
9535
+ fail(
9536
+ this,
9537
+ "Redirect experiments require at least one --destination URL",
9538
+ EXIT_CODES.validation
9539
+ );
9540
+ }
9541
+ const destinations = opts.destination.map(parseHttpDestination);
9542
+ const armWeight = destinations.length > 0 ? 1 / (destinations.length + 1) : null;
9369
9543
  const creds2 = requireCredentials();
9370
9544
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9371
- const spinner = ora2("Creating experiment...").start();
9545
+ const spinner = process.stderr.isTTY ? ora2("Creating experiment...").start() : null;
9372
9546
  const res = await createExperiment(creds2, {
9373
9547
  name: opts.name,
9374
9548
  slug: opts.slug ?? null,
9375
9549
  description: opts.description ?? null,
9376
- traffic_allocation: parseInt(opts.traffic, 10) / 100
9550
+ experiment_type: opts.type,
9551
+ traffic_allocation: traffic / 100,
9552
+ ...sourceRules.length > 0 ? {
9553
+ store_url: sourceStoreUrl(sourceRules),
9554
+ targeting: {
9555
+ url: sourceRules.map((pattern) => ({
9556
+ type: "simple",
9557
+ pattern,
9558
+ include: true
9559
+ }))
9560
+ }
9561
+ } : {},
9562
+ ...armWeight !== null ? {
9563
+ variations: [
9564
+ {
9565
+ name: "Control",
9566
+ weight: armWeight,
9567
+ is_control: true
9568
+ },
9569
+ ...destinations.map((redirectUrl, index) => ({
9570
+ name: destinations.length === 1 ? "Treatment" : `Treatment ${index + 1}`,
9571
+ weight: armWeight,
9572
+ is_control: false,
9573
+ redirect_url: redirectUrl
9574
+ }))
9575
+ ]
9576
+ } : {}
9377
9577
  });
9378
- if (!res.ok) {
9379
- spinner.fail("Failed to create experiment");
9380
- printError(res.error);
9381
- process.exit(1);
9578
+ if (!res.ok || !res.data) {
9579
+ spinner?.fail("Failed to create experiment");
9580
+ fail(
9581
+ this,
9582
+ res.error ?? "Failed to create experiment",
9583
+ classifyStatus(res.status),
9584
+ res.errorDetails
9585
+ );
9382
9586
  }
9383
- spinner.succeed("Experiment created");
9384
- printExperiment(res.data);
9587
+ spinner?.succeed("Experiment created");
9588
+ output(this, { experiment: res.data }, () => printExperiment(res.data));
9385
9589
  });
9386
9590
  var start = new Command("start").description("Start an experiment").argument("<id-or-slug>", "Experiment ID or slug").action(async (idOrSlug) => {
9387
9591
  const creds2 = requireCredentials();
@@ -9436,7 +9640,7 @@ var complete = new Command("complete").description("Complete/stop an experiment"
9436
9640
  `Experiment "${exp.name}" is now ${statusBadge("completed")}`
9437
9641
  );
9438
9642
  });
9439
- 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 action7(idOrSlug, opts) {
9643
+ 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 action8(idOrSlug, opts) {
9440
9644
  const creds2 = requireCredentials();
9441
9645
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9442
9646
  const exp = await resolveExperiment(idOrSlug);
@@ -9490,7 +9694,6 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9490
9694
  const res = await getExperimentFilesystem(creds2, exp.id);
9491
9695
  if (!res.ok || !res.data) throw new Error(res.error ?? "Failed to pull experiment filesystem");
9492
9696
  const folder = folderPath(opts.dir ?? res.data.rootName);
9493
- import_node_fs6.default.mkdirSync(folder, { recursive: true });
9494
9697
  writeExperimentFilesystemFiles(folder, res.data.files);
9495
9698
  writeFilesystemSession(folder, {
9496
9699
  schemaVersion: 1,
@@ -9509,7 +9712,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
9509
9712
  process.exit(1);
9510
9713
  }
9511
9714
  });
9512
- var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action8(folderArg) {
9715
+ var planFilesystem = new Command("plan").description("Plan experiment filesystem changes").argument("[folder]", "Experiment filesystem folder", ".").action(async function action9(folderArg) {
9513
9716
  const creds2 = requireCredentials();
9514
9717
  try {
9515
9718
  const folder = folderPath(folderArg);
@@ -9531,7 +9734,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
9531
9734
  fail(this, error instanceof Error ? error.message : String(error));
9532
9735
  }
9533
9736
  });
9534
- var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").action(async function action9(folderArg, opts) {
9737
+ var applyFilesystem = new Command("apply").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("-f, --force", "Skip confirmation when QA will be invalidated").action(async function action10(folderArg, opts) {
9535
9738
  const creds2 = requireCredentials();
9536
9739
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9537
9740
  try {
@@ -9811,7 +10014,7 @@ function isRecord(value) {
9811
10014
  }
9812
10015
  var pushCommand = new Command("push").description(
9813
10016
  "Push experiment definitions from config file to the API (idempotent by slug)"
9814
- ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action10(opts) {
10017
+ ).option("-c, --config <path>", "Config file path").option("--dry-run", "Show what would be pushed without making changes").action(async function action11(opts) {
9815
10018
  const creds2 = requireCredentials();
9816
10019
  const ora2 = (await Promise.resolve().then(() => (init_ora(), ora_exports))).default;
9817
10020
  const configPath = opts.config ?? findConfigPath();
@@ -10237,12 +10440,12 @@ harnessCommand.command("publish").argument("<folder>", "AB Test Harness experime
10237
10440
 
10238
10441
  // src/commands/operator.ts
10239
10442
  init_source();
10240
- var import_node_crypto3 = __toESM(require("node:crypto"));
10443
+ var import_node_crypto4 = __toESM(require("node:crypto"));
10241
10444
  var import_node_fs9 = __toESM(require("node:fs"));
10242
10445
  var import_node_path7 = __toESM(require("node:path"));
10243
10446
  var CANONICAL_FILE_RE = /^(src\/(?:index\.js|index\.scss|info\.js|mutations\.json)|experiment\.config\.json|metrics\.json|targeting\.json|dev-doc\.md|brand\.md|goals\/[a-zA-Z0-9_.-]+\.json|segments\/[a-zA-Z0-9_.-]+\.json|network\/[a-zA-Z0-9_.-]+\.recipe\.json)$/;
10244
10447
  function sha256Text2(value) {
10245
- return import_node_crypto3.default.createHash("sha256").update(value).digest("hex");
10448
+ return import_node_crypto4.default.createHash("sha256").update(value).digest("hex");
10246
10449
  }
10247
10450
  function byteLength(value) {
10248
10451
  return Buffer.byteLength(value, "utf8");
@@ -10520,7 +10723,7 @@ operatorCommand.command("checkout").argument("<experiment-id>", "Apex experiment
10520
10723
  process.exit(1);
10521
10724
  }
10522
10725
  });
10523
- operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action11(folderArg) {
10726
+ operatorCommand.command("status").argument("[folder]", "Operator workspace folder", ".").action(function action12(folderArg) {
10524
10727
  try {
10525
10728
  const folder = folderPath2(folderArg);
10526
10729
  const session = readSession(folder);
@@ -10901,7 +11104,7 @@ trackingCommand.command("qa").description("Print the QA checklist for a tracking
10901
11104
  console.log(`${index + 1}. ${check}`);
10902
11105
  });
10903
11106
  });
10904
- 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 action12(opts) {
11107
+ 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 action13(opts) {
10905
11108
  const creds2 = requireCredentials();
10906
11109
  const res = await verifyTracking(creds2, {
10907
11110
  minutes: Number.parseInt(opts.minutes, 10),
@@ -10969,7 +11172,7 @@ function printGoal(goal) {
10969
11172
  console.log(` Count once: ${goal.count_once ? "yes" : "no"}`);
10970
11173
  console.log();
10971
11174
  }
10972
- var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action13(opts) {
11175
+ var list2 = new Command("list").alias("ls").description("List goals").option("-n, --limit <n>", "Max results", "50").action(async function action14(opts) {
10973
11176
  const creds2 = requireCredentials();
10974
11177
  const res = await listGoals(creds2, { limit: Number.parseInt(opts.limit, 10) });
10975
11178
  if (!res.ok || !res.data) {
@@ -10991,7 +11194,7 @@ var list2 = new Command("list").alias("ls").description("List goals").option("-n
10991
11194
  );
10992
11195
  });
10993
11196
  });
10994
- var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").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 action14(opts) {
11197
+ var create2 = new Command("create").description("Create a goal").requiredOption("--name <name>", "Goal name").requiredOption("--type <type>", "click|pageview|custom|revenue").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 action15(opts) {
10995
11198
  const creds2 = requireCredentials();
10996
11199
  const body5 = cleanBody({
10997
11200
  name: opts.name,
@@ -11009,7 +11212,7 @@ var create2 = new Command("create").description("Create a goal").requiredOption(
11009
11212
  }
11010
11213
  output(this, { goal: res.data }, () => printGoal(res.data));
11011
11214
  });
11012
- 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("--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 action15(id, opts) {
11215
+ 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("--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 action16(id, opts) {
11013
11216
  const creds2 = requireCredentials();
11014
11217
  const body5 = cleanBody({
11015
11218
  name: opts.name,
@@ -11083,11 +11286,11 @@ async function load2(command) {
11083
11286
  }
11084
11287
  return result.data;
11085
11288
  }
11086
- var list3 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action16() {
11289
+ var list3 = new Command("list").alias("ls").description("List runtime environments and mapped domains").action(async function action17() {
11087
11290
  const { environments } = await load2(this);
11088
11291
  output(this, { environments }, () => printEnvironments(environments.items));
11089
11292
  });
11090
- 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 action17(domain, opts) {
11293
+ 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 action18(domain, opts) {
11091
11294
  const normalizedDomain = normalizeEnvironmentDomain(domain);
11092
11295
  if (!normalizedDomain) {
11093
11296
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -11130,7 +11333,7 @@ var setDomain = new Command("set-domain").description("Map a domain to a runtime
11130
11333
  () => printEnvironments(result.data.environments.items)
11131
11334
  );
11132
11335
  });
11133
- var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action18(domain) {
11336
+ var removeDomain = new Command("remove-domain").description("Remove a domain mapping").argument("<domain>", "Domain to remove").action(async function action19(domain) {
11134
11337
  const normalizedDomain = normalizeEnvironmentDomain(domain);
11135
11338
  if (!normalizedDomain) {
11136
11339
  fail(this, "Domain must be a valid hostname containing a dot", EXIT_CODES.validation);
@@ -11198,7 +11401,7 @@ function analyticsSubcommand(name, description, defaultDays) {
11198
11401
  command.option("--planning-days <n>", "Planning window in days", "180");
11199
11402
  command.option("--limit <n>", "Maximum page rows", "50");
11200
11403
  }
11201
- return command.action(async function action24(opts) {
11404
+ return command.action(async function action25(opts) {
11202
11405
  const result = await getAnalytics(requireCredentials(), name, {
11203
11406
  days: positiveInteger(this, opts.days, "--days"),
11204
11407
  planningDays: positiveInteger(this, opts.planningDays, "--planning-days"),
@@ -11385,18 +11588,18 @@ function write(name) {
11385
11588
  });
11386
11589
  return c;
11387
11590
  }
11388
- function lifecycle(action24) {
11389
- return new Command(action24).description(`${action24[0].toUpperCase()}${action24.slice(1)} a flag`).argument("<id>").action(async function(id) {
11591
+ function lifecycle(action25) {
11592
+ return new Command(action25).description(`${action25[0].toUpperCase()}${action25.slice(1)} a flag`).argument("<id>").action(async function(id) {
11390
11593
  const r = await transitionManagedDelivery(
11391
11594
  requireCredentials(),
11392
11595
  kind,
11393
11596
  id,
11394
- action24
11597
+ action25
11395
11598
  );
11396
11599
  if (!r.ok || !r.data)
11397
11600
  fail(
11398
11601
  this,
11399
- r.error ?? `Failed to ${action24}`,
11602
+ r.error ?? `Failed to ${action25}`,
11400
11603
  classifyStatus(r.status),
11401
11604
  r.errorDetails
11402
11605
  );
@@ -11440,18 +11643,18 @@ var get3 = new Command("get").description("Get a backlog item").argument("<id>",
11440
11643
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
11441
11644
  output(this, { backlog_item: r.data }, () => detail2(r.data));
11442
11645
  });
11443
- function write2(action24) {
11444
- const c = new Command(action24).description(`${action24 === "create" ? "Create" : "Update"} a ${singular}`);
11445
- if (action24 === "update") c.argument("<id>", `${singular} ID`);
11646
+ function write2(action25) {
11647
+ const c = new Command(action25).description(`${action25 === "create" ? "Create" : "Update"} a ${singular}`);
11648
+ if (action25 === "update") c.argument("<id>", `${singular} ID`);
11446
11649
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
11447
11650
  c.action(async function(...args) {
11448
11651
  const o = this.opts();
11449
- const id = action24 === "update" ? String(args[0]) : void 0;
11450
- const raw = o.file ? `@${o.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
11652
+ const id = action25 === "update" ? String(args[0]) : void 0;
11653
+ const raw = o.file ? `@${o.file}` : String(args[action25 === "update" ? 1 : 0] ?? "") || void 0;
11451
11654
  const body5 = parse(this, raw);
11452
11655
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
11453
- const r = action24 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
11454
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action24}`, classifyStatus(r.status), r.errorDetails);
11656
+ const r = action25 === "create" ? await createInsight(requireCredentials(), kind2, body5) : await updateInsight(requireCredentials(), kind2, id, body5);
11657
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action25}`, classifyStatus(r.status), r.errorDetails);
11455
11658
  output(this, { backlog_item: r.data }, () => detail2(r.data));
11456
11659
  });
11457
11660
  return c;
@@ -11520,18 +11723,18 @@ var get4 = new Command("get").description("Get a signal").argument("<id>", "sign
11520
11723
  if (!r.ok || !r.data) fail(this, r.error ?? "Failed to get", classifyStatus(r.status), r.errorDetails);
11521
11724
  output(this, { signal: r.data }, () => detail3(r.data));
11522
11725
  });
11523
- function write3(action24) {
11524
- const c = new Command(action24).description(`${action24 === "create" ? "Create" : "Update"} a ${singular2}`);
11525
- if (action24 === "update") c.argument("<id>", `${singular2} ID`);
11726
+ function write3(action25) {
11727
+ const c = new Command(action25).description(`${action25 === "create" ? "Create" : "Update"} a ${singular2}`);
11728
+ if (action25 === "update") c.argument("<id>", `${singular2} ID`);
11526
11729
  c.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file");
11527
11730
  c.action(async function(...args) {
11528
11731
  const o = this.opts();
11529
- const id = action24 === "update" ? String(args[0]) : void 0;
11530
- const raw = o.file ? `@${o.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
11732
+ const id = action25 === "update" ? String(args[0]) : void 0;
11733
+ const raw = o.file ? `@${o.file}` : String(args[action25 === "update" ? 1 : 0] ?? "") || void 0;
11531
11734
  const body5 = parse2(this, raw);
11532
11735
  if (!Object.keys(body5).length) fail(this, "Provide a JSON body.", EXIT_CODES.validation);
11533
- const r = action24 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
11534
- if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action24}`, classifyStatus(r.status), r.errorDetails);
11736
+ const r = action25 === "create" ? await createInsight(requireCredentials(), kind3, body5) : await updateInsight(requireCredentials(), kind3, id, body5);
11737
+ if (!r.ok || !r.data) fail(this, r.error ?? `Failed to ${action25}`, classifyStatus(r.status), r.errorDetails);
11535
11738
  output(this, { signal: r.data }, () => detail3(r.data));
11536
11739
  });
11537
11740
  return c;
@@ -11649,20 +11852,20 @@ function write4(name) {
11649
11852
  });
11650
11853
  return c;
11651
11854
  }
11652
- function lifecycle2(action24) {
11653
- return new Command(action24).description(
11654
- `${action24[0].toUpperCase()}${action24.slice(1)} a personalization`
11855
+ function lifecycle2(action25) {
11856
+ return new Command(action25).description(
11857
+ `${action25[0].toUpperCase()}${action25.slice(1)} a personalization`
11655
11858
  ).argument("<id>").action(async function(id) {
11656
11859
  const r = await transitionManagedDelivery(
11657
11860
  requireCredentials(),
11658
11861
  kind4,
11659
11862
  id,
11660
- action24
11863
+ action25
11661
11864
  );
11662
11865
  if (!r.ok || !r.data)
11663
11866
  fail(
11664
11867
  this,
11665
- r.error ?? `Failed to ${action24}`,
11868
+ r.error ?? `Failed to ${action25}`,
11666
11869
  classifyStatus(r.status),
11667
11870
  r.errorDetails
11668
11871
  );
@@ -11758,17 +11961,17 @@ function targetingResourceCommand(kind5) {
11758
11961
  );
11759
11962
  })
11760
11963
  );
11761
- for (const action24 of ["create", "update"]) {
11762
- const command = new Command(action24).description(
11763
- `${action24 === "create" ? "Create" : "Update"} a ${singular3}`
11964
+ for (const action25 of ["create", "update"]) {
11965
+ const command = new Command(action25).description(
11966
+ `${action25 === "create" ? "Create" : "Update"} a ${singular3}`
11764
11967
  );
11765
- if (action24 === "update") command.argument("<id>");
11968
+ if (action25 === "update") command.argument("<id>");
11766
11969
  command.argument("[json]", "Inline JSON, @file, or file path").option("--file <path>", "JSON body file").action(async function(...args) {
11767
11970
  const opts = this.opts();
11768
- const id = action24 === "update" ? String(args[0]) : void 0;
11769
- const value = opts.file ? `@${opts.file}` : String(args[action24 === "update" ? 1 : 0] ?? "") || void 0;
11971
+ const id = action25 === "update" ? String(args[0]) : void 0;
11972
+ const value = opts.file ? `@${opts.file}` : String(args[action25 === "update" ? 1 : 0] ?? "") || void 0;
11770
11973
  const requestBody = body3(this, value);
11771
- if (action24 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
11974
+ if (action25 === "update" && typeof requestBody === "object" && requestBody !== null && !Array.isArray(requestBody) && Object.keys(requestBody).length === 0)
11772
11975
  fail(
11773
11976
  this,
11774
11977
  `Updating a ${singular3} requires at least one field.`,
@@ -11776,7 +11979,7 @@ function targetingResourceCommand(kind5) {
11776
11979
  );
11777
11980
  const response = await requestTargetingResource(
11778
11981
  requireCredentials(),
11779
- action24 === "create" ? "POST" : "PATCH",
11982
+ action25 === "create" ? "POST" : "PATCH",
11780
11983
  kind5,
11781
11984
  id,
11782
11985
  requestBody
@@ -11784,7 +11987,7 @@ function targetingResourceCommand(kind5) {
11784
11987
  if (!response.ok || !response.data)
11785
11988
  fail(
11786
11989
  this,
11787
- response.error ?? `Failed to ${action24}`,
11990
+ response.error ?? `Failed to ${action25}`,
11788
11991
  classifyStatus(response.status),
11789
11992
  response.errorDetails
11790
11993
  );
@@ -11793,7 +11996,7 @@ function targetingResourceCommand(kind5) {
11793
11996
  {
11794
11997
  [kind5 === "exclusion-groups" ? "exclusion_group" : singular3]: response.data
11795
11998
  },
11796
- () => console.log(source_default.green(`${action24}d ${singular3}`))
11999
+ () => console.log(source_default.green(`${action25}d ${singular3}`))
11797
12000
  );
11798
12001
  });
11799
12002
  root.addCommand(command);
@@ -11880,7 +12083,7 @@ var exclusionGroupsCommand = targetingResourceCommand("exclusion-groups");
11880
12083
  // src/commands/brand.ts
11881
12084
  init_source();
11882
12085
  var NO_PROFILE_MESSAGE = "No brand profile yet \u2014 run onboarding or a site check";
11883
- var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action19() {
12086
+ var brandCommand = new Command("brand").description("Print the shop brand context for external agents").action(async function action20() {
11884
12087
  const creds2 = requireCredentials();
11885
12088
  const res = await getBrandContext(creds2);
11886
12089
  if (res.status === 404) {
@@ -11913,7 +12116,7 @@ var brandCommand = new Command("brand").description("Print the shop brand contex
11913
12116
 
11914
12117
  // src/commands/targeting.ts
11915
12118
  init_source();
11916
- var test = 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 action20(url, opts) {
12119
+ var test = 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 action21(url, opts) {
11917
12120
  const creds2 = requireCredentials();
11918
12121
  const res = await testTargeting(creds2, url, { status: opts.status });
11919
12122
  if (!res.ok || !res.data) {
@@ -11970,7 +12173,7 @@ function printTicket(ticket, reused) {
11970
12173
  console.log(` Status: ${ticket.my_status ?? ticket.mapped_status}`);
11971
12174
  console.log();
11972
12175
  }
11973
- var create3 = new Command("create").description("Create an Apex SOS ticket").option("--type <type>", "full_handover|design|development|qa|config|bug", "qa").option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action21(descriptionArg, opts) {
12176
+ var create3 = new Command("create").description("Create an Apex SOS ticket").option("--type <type>", "full_handover|design|development|qa|config|bug", "qa").option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action22(descriptionArg, opts) {
11974
12177
  if (!SOS_TYPES.has(opts.type)) {
11975
12178
  fail(this, "Invalid SOS type. Use full_handover, design, development, qa, config, or bug.", 3);
11976
12179
  }
@@ -11988,7 +12191,7 @@ var create3 = new Command("create").description("Create an Apex SOS ticket").opt
11988
12191
  }
11989
12192
  output(this, res.data, () => printTicket(res.data.ticket, res.data.reused));
11990
12193
  });
11991
- var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action22() {
12194
+ var mine = new Command("mine").description("List SOS tickets created by this API key").action(async function action23() {
11992
12195
  const creds2 = requireCredentials();
11993
12196
  const res = await listMySos(creds2);
11994
12197
  if (!res.ok || !res.data) {
@@ -12110,10 +12313,19 @@ var create4 = new Command("create").description("Create a shop").argument("[json
12110
12313
  const r = await createShop(requireUserCredentials(), payload);
12111
12314
  done(this, r, "create shop");
12112
12315
  });
12113
- var update2 = new Command("update").description("Update a shop").argument("<id>").argument("<json>", "Inline JSON, @file, or file path").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
12114
- const b = body4(this, raw);
12316
+ var update2 = new Command("update").description("Update a shop").argument("<id>").argument("[json]", "Inline JSON, @file, or file path").option("--holdout-pct <percent>", "Global holdout percent (0-10)").option("--holdout-enabled", "Enable global holdout v1").option("--holdout-disabled", "Disable global holdout v1").option("--confirm-tier", "Confirm a commercially sensitive tier change").action(async function(id, raw, o) {
12317
+ const b = raw === void 0 ? {} : body4(this, raw);
12318
+ if (o.holdoutEnabled && o.holdoutDisabled)
12319
+ fail(this, "Use only one of --holdout-enabled or --holdout-disabled", EXIT_CODES.validation);
12320
+ if (o.holdoutPct !== void 0) {
12321
+ const pct = Number(o.holdoutPct);
12322
+ if (!Number.isFinite(pct) || pct < 0 || pct > 10)
12323
+ fail(this, "--holdout-pct must be between 0 and 10", EXIT_CODES.validation);
12324
+ b.holdout_pct = pct;
12325
+ }
12326
+ if (o.holdoutEnabled || o.holdoutDisabled) b.holdout_v1_enabled = Boolean(o.holdoutEnabled);
12115
12327
  if (!Object.keys(b).length)
12116
- fail(this, "Provide a JSON body.", EXIT_CODES.validation);
12328
+ fail(this, "Provide a JSON body or holdout flags.", EXIT_CODES.validation);
12117
12329
  if (Object.hasOwn(b, "tier") && !o.confirmTier)
12118
12330
  fail(
12119
12331
  this,
@@ -12274,7 +12486,7 @@ function commandSchema(command) {
12274
12486
  };
12275
12487
  }
12276
12488
  function createSchemaCommand(program3) {
12277
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action24() {
12489
+ return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action25() {
12278
12490
  const schema = commandSchema(program3);
12279
12491
  output(this, schema, () => {
12280
12492
  console.log(JSON.stringify(schema, null, 2));
@@ -12318,7 +12530,7 @@ function installSkill(paths, targetRoot) {
12318
12530
  import_node_fs20.default.copyFileSync(paths.agents, import_node_path18.default.join(target, "AGENTS.md"));
12319
12531
  return target;
12320
12532
  }
12321
- 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 action23(opts) {
12533
+ 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 action24(opts) {
12322
12534
  let paths;
12323
12535
  try {
12324
12536
  paths = skillPaths();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drip-apex/apex-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "description": "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
6
6
  "private": false,