@drip-apex/apex-cli 0.15.1 → 0.15.3

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 +1375 -76
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7325,7 +7325,7 @@ async function checkForUpdate(current) {
7325
7325
  // package.json
7326
7326
  var package_default = {
7327
7327
  name: "@drip-apex/apex-cli",
7328
- version: "0.15.1",
7328
+ version: "0.15.3",
7329
7329
  license: "MIT",
7330
7330
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7331
7331
  private: false,
@@ -7589,7 +7589,8 @@ function classifyApiError(status4, details) {
7589
7589
  const code = details?.code;
7590
7590
  if (code === "shop_context_required" || code === "org_context_required")
7591
7591
  return EXIT_CODES.validation;
7592
- if (code === "shop_access_denied" || code === "org_access_denied") return EXIT_CODES.auth;
7592
+ if (code === "shop_access_denied" || code === "org_access_denied" || code === "member_suspended")
7593
+ return EXIT_CODES.auth;
7593
7594
  return classifyStatus(status4);
7594
7595
  }
7595
7596
  function confirmationRequired(command, message, retry) {
@@ -7686,7 +7687,7 @@ function errorReason(error) {
7686
7687
  const body5 = error.api?.body;
7687
7688
  const code = body5?.code ?? body5?.error?.code ?? (error.api ? details?.code : void 0);
7688
7689
  if (typeof code === "string" && code) return code;
7689
- if (["shop_context_required", "local_file_error", "validation_failed"].includes(
7690
+ if (["shop_context_required", "local_file_error", "validation_failed", "member_suspended"].includes(
7690
7691
  String(details?.code)
7691
7692
  ))
7692
7693
  return String(details?.code);
@@ -7704,6 +7705,13 @@ function errorReason(error) {
7704
7705
  function errorNextSteps(error) {
7705
7706
  const reason2 = errorReason(error);
7706
7707
  const steps = [];
7708
+ if (reason2 === "member_suspended")
7709
+ return [
7710
+ {
7711
+ command: "contact organization admin",
7712
+ why: "Ask an organization owner or admin to reinstate your membership."
7713
+ }
7714
+ ];
7707
7715
  if (reason2 === "auth_required" || error.api?.status === 401) {
7708
7716
  steps.push({ command: "apex login", why: "Authenticate with a valid API key." });
7709
7717
  if (!error.api)
@@ -8438,6 +8446,7 @@ function listExperiments(creds, opts) {
8438
8446
  if (opts?.includeArchived) params.set("archived", "1");
8439
8447
  if (opts?.view) params.set("view", opts.view);
8440
8448
  if (opts?.fields !== void 0) params.set("fields", opts.fields);
8449
+ if (opts?.url !== void 0) params.set("url", opts.url);
8441
8450
  const qs = params.toString();
8442
8451
  return request(creds, "GET", `/api/v1/experiments${qs ? `?${qs}` : ""}`);
8443
8452
  }
@@ -8485,6 +8494,8 @@ function getExperimentFilesystem(creds, id) {
8485
8494
  function getExperimentResults(creds, id, opts) {
8486
8495
  const params = new URLSearchParams();
8487
8496
  if (opts?.currency) params.set("currency", opts.currency);
8497
+ for (const key of ["segment", "exclude_segment"])
8498
+ if (opts?.[key] !== void 0) params.set(key, opts[key]);
8488
8499
  const qs = params.toString();
8489
8500
  return request(
8490
8501
  creds,
@@ -8530,6 +8541,7 @@ function applyExperimentFilesystem(creds, id, body5) {
8530
8541
  `/api/v1/experiments/${id}/filesystem/apply`,
8531
8542
  {
8532
8543
  files: body5.files,
8544
+ ...body5.accept_qa_invalidation !== void 0 ? { accept_qa_invalidation: body5.accept_qa_invalidation } : {},
8533
8545
  ...body5.confirmTargetingReplace === true ? { confirmTargetingReplace: true } : {}
8534
8546
  }
8535
8547
  );
@@ -8537,6 +8549,17 @@ function applyExperimentFilesystem(creds, id, body5) {
8537
8549
  function getExperimentQaRun(creds, id) {
8538
8550
  return request(creds, "GET", `/api/v1/experiments/${id}/qa/run`);
8539
8551
  }
8552
+ function getExperimentQaEvents(creds, id, options = {}) {
8553
+ const params = new URLSearchParams();
8554
+ if (options.session) params.set("qa_session", options.session);
8555
+ if (options.minutes !== void 0) params.set("minutes", String(options.minutes));
8556
+ if (options.type) params.set("type", options.type);
8557
+ return request(
8558
+ creds,
8559
+ "GET",
8560
+ `/api/v1/experiments/${id}/qa/events${params.size ? `?${params}` : ""}`
8561
+ );
8562
+ }
8540
8563
  function updateExperimentQaRun(creds, id, body5) {
8541
8564
  return request(creds, "PATCH", `/api/v1/experiments/${id}/qa/run`, body5);
8542
8565
  }
@@ -8746,7 +8769,7 @@ function testTargeting(creds, url, opts) {
8746
8769
  if (opts?.status) params.set("status", opts.status);
8747
8770
  return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
8748
8771
  }
8749
- function createSos(creds, body5) {
8772
+ function createSos(creds, body5, options = {}) {
8750
8773
  const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto2.createHash)("sha256").update(
8751
8774
  [
8752
8775
  body5.type,
@@ -8754,11 +8777,29 @@ function createSos(creds, body5) {
8754
8777
  body5.description,
8755
8778
  body5.sub_class ?? "",
8756
8779
  body5.loom_url ?? "",
8780
+ ...options.attachmentDigests ?? [],
8757
8781
  String(Math.floor(Date.now() / 36e5))
8758
8782
  ].join("\0")
8759
8783
  ).digest("hex");
8760
8784
  return request(creds, "POST", "/api/v1/sos", { ...body5, idempotency_key });
8761
8785
  }
8786
+ function createSosUploadGrants(creds, files) {
8787
+ return request(
8788
+ creds,
8789
+ "POST",
8790
+ "/api/v1/sos/attachment-uploads",
8791
+ { files }
8792
+ );
8793
+ }
8794
+ async function uploadSosAttachment(signedUrl, contentType, bytes) {
8795
+ const response = await fetch(signedUrl, {
8796
+ method: "PUT",
8797
+ headers: { "Content-Type": contentType },
8798
+ body: bytes,
8799
+ signal: AbortSignal.timeout(12e4)
8800
+ });
8801
+ if (!response.ok) throw new Error(`Attachment upload failed (${response.status})`);
8802
+ }
8762
8803
  function listMySos(creds) {
8763
8804
  return request(creds, "GET", "/api/v1/sos/mine");
8764
8805
  }
@@ -8869,6 +8910,15 @@ function createAdminCustomer(creds, body5) {
8869
8910
  body5
8870
8911
  );
8871
8912
  }
8913
+ function setAdminCustomerCategory(creds, orgId, body5) {
8914
+ return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
8915
+ }
8916
+ function setAdminCustomerManagedBy(creds, orgId, body5) {
8917
+ return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
8918
+ }
8919
+ function setAdminCustomerWorkspaceType(creds, orgId, body5) {
8920
+ return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
8921
+ }
8872
8922
  function getAdminSettings(creds) {
8873
8923
  return request(userCredentials(creds), "GET", "/api/v1/admin/settings");
8874
8924
  }
@@ -9023,6 +9073,17 @@ function revokeOrgInvite(creds, id) {
9023
9073
  function setOrgMember(creds, id, body5) {
9024
9074
  return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, body5);
9025
9075
  }
9076
+ function suspendOrgMember(creds, id, reason2) {
9077
+ return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, {
9078
+ suspended: true,
9079
+ ...reason2 ? { reason: reason2 } : {}
9080
+ });
9081
+ }
9082
+ function reinstateOrgMember(creds, id) {
9083
+ return request(userCreds(creds), "PATCH", `/api/v1/org/members/${encodeURIComponent(id)}`, {
9084
+ suspended: false
9085
+ });
9086
+ }
9026
9087
  function removeOrgMember(creds, id) {
9027
9088
  return request(userCreds(creds), "DELETE", `/api/v1/org/members/${encodeURIComponent(id)}`);
9028
9089
  }
@@ -9166,6 +9227,14 @@ function getOrderJourney(creds, opts) {
9166
9227
  45e3
9167
9228
  );
9168
9229
  }
9230
+ function moveAdminCustomerStores(creds, orgId, body5) {
9231
+ return request(
9232
+ userCredentials(creds),
9233
+ "POST",
9234
+ `/api/v1/admin/customers/${orgId}/move-stores`,
9235
+ body5
9236
+ );
9237
+ }
9169
9238
 
9170
9239
  // src/context.ts
9171
9240
  var import_node_readline = __toESM(require("node:readline"));
@@ -9377,10 +9446,14 @@ async function waitForBrowserLogin(baseUrl, userMode = false) {
9377
9446
  const returnedState = params.get("state");
9378
9447
  const apiKey = params.get("apiKey");
9379
9448
  const error = params.get("error");
9449
+ const code = params.get("code");
9380
9450
  if (error) {
9381
9451
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
9382
9452
  res.end("<h1>Apex CLI login failed</h1><p>You can close this window.</p>");
9383
- finish(new Error(error));
9453
+ const details = code ? { code } : void 0;
9454
+ finish(
9455
+ details ? new CliError(error, classifyApiError(400, details), details) : new Error(error)
9456
+ );
9384
9457
  return;
9385
9458
  }
9386
9459
  if (returnedState !== state) {
@@ -9464,6 +9537,7 @@ var loginCommand = new Command("login").description("Authenticate with Apex").op
9464
9537
  try {
9465
9538
  apiKey = await waitForBrowserLogin(baseUrl, true);
9466
9539
  } catch (err) {
9540
+ if (err instanceof CliError) throw err;
9467
9541
  fail(this, err instanceof Error ? err.message : "Browser login failed.");
9468
9542
  }
9469
9543
  }
@@ -9755,13 +9829,18 @@ function buildQaPreviewUrl(experiment2, options = {}) {
9755
9829
  }
9756
9830
 
9757
9831
  // src/lib/qa-preview-url.ts
9758
- function buildQaPreviewUrl2(storeUrl, experimentId, variationId) {
9759
- const url = buildQaPreviewUrl({
9760
- id: experimentId,
9761
- store_url: storeUrl,
9762
- variations: [{ id: variationId, is_control: false }]
9763
- });
9832
+ function buildQaPreviewUrl2(storeUrl, experimentId, variationId, qaSessionId) {
9833
+ const url = buildQaPreviewUrl(
9834
+ {
9835
+ id: experimentId,
9836
+ store_url: storeUrl,
9837
+ variations: [{ id: variationId, is_control: false }]
9838
+ },
9839
+ { qaSessionId }
9840
+ );
9764
9841
  if (!url) throw new TypeError("Invalid QA preview URL");
9842
+ if (qaSessionId && url.searchParams.get("drip_qa_session") !== qaSessionId)
9843
+ url.searchParams.set("drip_qa_session", qaSessionId);
9765
9844
  return url;
9766
9845
  }
9767
9846
 
@@ -10159,6 +10238,755 @@ function createAssetReferenceRewriter(options) {
10159
10238
  return { rewrite, rewriteUnknown };
10160
10239
  }
10161
10240
 
10241
+ // src/generated/experiment-filesystem.schema.json
10242
+ var experiment_filesystem_schema_default = {
10243
+ $id: "https://api.drip-apex.com/schemas/experiment-filesystem-v1.json",
10244
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10245
+ files: {
10246
+ "experiment.json": {
10247
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10248
+ additionalProperties: false,
10249
+ properties: {
10250
+ autoStopOverride: {
10251
+ anyOf: [
10252
+ {},
10253
+ {
10254
+ type: "null"
10255
+ }
10256
+ ]
10257
+ },
10258
+ countingType: {
10259
+ anyOf: [
10260
+ {
10261
+ enum: ["standard", "setup_test", "aa_test", "non_program"],
10262
+ type: "string"
10263
+ },
10264
+ {
10265
+ type: "null"
10266
+ }
10267
+ ]
10268
+ },
10269
+ description: {
10270
+ anyOf: [
10271
+ {
10272
+ type: "string"
10273
+ },
10274
+ {
10275
+ type: "null"
10276
+ }
10277
+ ]
10278
+ },
10279
+ environmentKey: {
10280
+ type: "string"
10281
+ },
10282
+ experimentType: {
10283
+ enum: ["mutation", "redirect"],
10284
+ type: "string"
10285
+ },
10286
+ id: {
10287
+ anyOf: [
10288
+ {
10289
+ type: "string"
10290
+ },
10291
+ {
10292
+ type: "null"
10293
+ }
10294
+ ]
10295
+ },
10296
+ name: {
10297
+ type: "string"
10298
+ },
10299
+ schemaVersion: {
10300
+ const: 1,
10301
+ type: "number"
10302
+ },
10303
+ sequentialTestingEnabled: {
10304
+ type: "boolean"
10305
+ },
10306
+ significanceLevel: {
10307
+ type: "number"
10308
+ },
10309
+ slug: {
10310
+ anyOf: [
10311
+ {
10312
+ type: "string"
10313
+ },
10314
+ {
10315
+ type: "null"
10316
+ }
10317
+ ]
10318
+ },
10319
+ statisticalPower: {
10320
+ type: "number"
10321
+ },
10322
+ storeUrl: {
10323
+ anyOf: [
10324
+ {
10325
+ type: "string"
10326
+ },
10327
+ {
10328
+ type: "null"
10329
+ }
10330
+ ]
10331
+ },
10332
+ testKey: {
10333
+ anyOf: [
10334
+ {
10335
+ type: "string"
10336
+ },
10337
+ {
10338
+ type: "null"
10339
+ }
10340
+ ]
10341
+ },
10342
+ trafficAllocation: {
10343
+ maximum: 1,
10344
+ minimum: 0,
10345
+ type: "number"
10346
+ }
10347
+ },
10348
+ required: ["schemaVersion"],
10349
+ type: "object"
10350
+ },
10351
+ "experiment/index.css": {
10352
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10353
+ contentMediaType: "text/css",
10354
+ type: "string"
10355
+ },
10356
+ "experiment/index.js": {
10357
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10358
+ contentMediaType: "text/javascript",
10359
+ type: "string"
10360
+ },
10361
+ "experiment/reset.js": {
10362
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10363
+ contentMediaType: "text/javascript",
10364
+ type: "string"
10365
+ },
10366
+ "launch.json": {
10367
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10368
+ additionalProperties: false,
10369
+ properties: {
10370
+ endedAt: {
10371
+ anyOf: [
10372
+ {
10373
+ type: "string"
10374
+ },
10375
+ {
10376
+ type: "null"
10377
+ }
10378
+ ]
10379
+ },
10380
+ launchApproval: {
10381
+ anyOf: [
10382
+ {
10383
+ additionalProperties: false,
10384
+ properties: {
10385
+ decidedAt: {
10386
+ anyOf: [
10387
+ {
10388
+ type: "string"
10389
+ },
10390
+ {
10391
+ type: "null"
10392
+ }
10393
+ ]
10394
+ },
10395
+ note: {
10396
+ anyOf: [
10397
+ {
10398
+ type: "string"
10399
+ },
10400
+ {
10401
+ type: "null"
10402
+ }
10403
+ ]
10404
+ },
10405
+ status: {
10406
+ type: "string"
10407
+ }
10408
+ },
10409
+ required: ["status", "decidedAt", "note"],
10410
+ type: "object"
10411
+ },
10412
+ {
10413
+ type: "null"
10414
+ }
10415
+ ]
10416
+ },
10417
+ launchGate: {
10418
+ additionalProperties: false,
10419
+ properties: {
10420
+ qaApproved: {
10421
+ type: "boolean"
10422
+ },
10423
+ treatmentArtifactsApproved: {
10424
+ type: "boolean"
10425
+ }
10426
+ },
10427
+ required: ["qaApproved", "treatmentArtifactsApproved"],
10428
+ type: "object"
10429
+ },
10430
+ readonly: {
10431
+ const: true,
10432
+ type: "boolean"
10433
+ },
10434
+ scheduledEndAt: {
10435
+ anyOf: [
10436
+ {
10437
+ type: "string"
10438
+ },
10439
+ {
10440
+ type: "null"
10441
+ }
10442
+ ]
10443
+ },
10444
+ scheduledStartAt: {
10445
+ anyOf: [
10446
+ {
10447
+ type: "string"
10448
+ },
10449
+ {
10450
+ type: "null"
10451
+ }
10452
+ ]
10453
+ },
10454
+ schemaVersion: {
10455
+ const: 1,
10456
+ type: "number"
10457
+ },
10458
+ startedAt: {
10459
+ anyOf: [
10460
+ {
10461
+ type: "string"
10462
+ },
10463
+ {
10464
+ type: "null"
10465
+ }
10466
+ ]
10467
+ },
10468
+ status: {
10469
+ type: "string"
10470
+ }
10471
+ },
10472
+ readOnly: true,
10473
+ required: [
10474
+ "schemaVersion",
10475
+ "readonly",
10476
+ "status",
10477
+ "scheduledStartAt",
10478
+ "scheduledEndAt",
10479
+ "startedAt",
10480
+ "endedAt",
10481
+ "launchApproval",
10482
+ "launchGate"
10483
+ ],
10484
+ type: "object"
10485
+ },
10486
+ "metrics.json": {
10487
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10488
+ additionalProperties: false,
10489
+ properties: {
10490
+ goals: {
10491
+ items: {
10492
+ additionalProperties: false,
10493
+ properties: {
10494
+ capture: {
10495
+ anyOf: [
10496
+ {},
10497
+ {
10498
+ type: "null"
10499
+ }
10500
+ ]
10501
+ },
10502
+ countOnce: {
10503
+ type: "boolean"
10504
+ },
10505
+ eventName: {
10506
+ anyOf: [
10507
+ {
10508
+ type: "string"
10509
+ },
10510
+ {
10511
+ type: "null"
10512
+ }
10513
+ ]
10514
+ },
10515
+ filter: {
10516
+ anyOf: [
10517
+ {},
10518
+ {
10519
+ type: "null"
10520
+ }
10521
+ ]
10522
+ },
10523
+ id: {
10524
+ anyOf: [
10525
+ {
10526
+ type: "string"
10527
+ },
10528
+ {
10529
+ type: "null"
10530
+ }
10531
+ ]
10532
+ },
10533
+ isPrimary: {
10534
+ type: "boolean"
10535
+ },
10536
+ measurement: {},
10537
+ name: {
10538
+ type: "string"
10539
+ },
10540
+ pageId: {
10541
+ anyOf: [
10542
+ {
10543
+ type: "string"
10544
+ },
10545
+ {
10546
+ type: "null"
10547
+ }
10548
+ ]
10549
+ },
10550
+ selector: {
10551
+ anyOf: [
10552
+ {
10553
+ type: "string"
10554
+ },
10555
+ {
10556
+ type: "null"
10557
+ }
10558
+ ]
10559
+ },
10560
+ textMatch: {
10561
+ anyOf: [
10562
+ {
10563
+ type: "string"
10564
+ },
10565
+ {
10566
+ type: "null"
10567
+ }
10568
+ ]
10569
+ },
10570
+ type: {
10571
+ enum: ["click", "pageview", "custom", "revenue"],
10572
+ type: "string"
10573
+ },
10574
+ urlPattern: {
10575
+ anyOf: [
10576
+ {
10577
+ type: "string"
10578
+ },
10579
+ {
10580
+ type: "null"
10581
+ }
10582
+ ]
10583
+ },
10584
+ valueProperty: {
10585
+ anyOf: [
10586
+ {
10587
+ type: "string"
10588
+ },
10589
+ {
10590
+ type: "null"
10591
+ }
10592
+ ]
10593
+ },
10594
+ valueUnit: {
10595
+ anyOf: [
10596
+ {
10597
+ type: "string"
10598
+ },
10599
+ {
10600
+ type: "null"
10601
+ }
10602
+ ]
10603
+ }
10604
+ },
10605
+ required: ["name", "type"],
10606
+ type: "object"
10607
+ },
10608
+ type: "array"
10609
+ },
10610
+ primaryGoalId: {
10611
+ anyOf: [
10612
+ {
10613
+ type: "string"
10614
+ },
10615
+ {
10616
+ type: "null"
10617
+ }
10618
+ ]
10619
+ },
10620
+ schemaVersion: {
10621
+ const: 1,
10622
+ type: "number"
10623
+ }
10624
+ },
10625
+ required: ["schemaVersion", "primaryGoalId", "goals"],
10626
+ type: "object"
10627
+ },
10628
+ "qa.json": {
10629
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10630
+ additionalProperties: false,
10631
+ properties: {
10632
+ latestRun: {
10633
+ anyOf: [
10634
+ {
10635
+ additionalProperties: false,
10636
+ properties: {
10637
+ approvedAt: {
10638
+ anyOf: [
10639
+ {
10640
+ type: "string"
10641
+ },
10642
+ {
10643
+ type: "null"
10644
+ }
10645
+ ]
10646
+ },
10647
+ completedAt: {
10648
+ anyOf: [
10649
+ {
10650
+ type: "string"
10651
+ },
10652
+ {
10653
+ type: "null"
10654
+ }
10655
+ ]
10656
+ },
10657
+ id: {
10658
+ type: "string"
10659
+ },
10660
+ source: {
10661
+ type: "string"
10662
+ },
10663
+ status: {
10664
+ type: "string"
10665
+ },
10666
+ summary: {
10667
+ anyOf: [
10668
+ {},
10669
+ {
10670
+ type: "null"
10671
+ }
10672
+ ]
10673
+ }
10674
+ },
10675
+ required: ["id", "status", "source", "approvedAt", "completedAt", "summary"],
10676
+ type: "object"
10677
+ },
10678
+ {
10679
+ type: "null"
10680
+ }
10681
+ ]
10682
+ },
10683
+ qaApprovedAt: {
10684
+ anyOf: [
10685
+ {
10686
+ type: "string"
10687
+ },
10688
+ {
10689
+ type: "null"
10690
+ }
10691
+ ]
10692
+ },
10693
+ qaApprovedBy: {
10694
+ anyOf: [
10695
+ {
10696
+ type: "string"
10697
+ },
10698
+ {
10699
+ type: "null"
10700
+ }
10701
+ ]
10702
+ },
10703
+ readonly: {
10704
+ const: true,
10705
+ type: "boolean"
10706
+ },
10707
+ schemaVersion: {
10708
+ const: 1,
10709
+ type: "number"
10710
+ }
10711
+ },
10712
+ readOnly: true,
10713
+ required: ["schemaVersion", "readonly", "qaApprovedAt", "qaApprovedBy", "latestRun"],
10714
+ type: "object"
10715
+ },
10716
+ "targeting.json": {
10717
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10718
+ additionalProperties: {},
10719
+ properties: {
10720
+ authorUrl: {
10721
+ items: {
10722
+ additionalProperties: false,
10723
+ properties: {
10724
+ include: {
10725
+ type: "boolean"
10726
+ },
10727
+ match_type: {
10728
+ enum: ["exact", "contains", "wildcard", "regex"],
10729
+ type: "string"
10730
+ },
10731
+ matchType: {
10732
+ enum: ["exact", "contains", "wildcard", "regex"],
10733
+ type: "string"
10734
+ },
10735
+ pattern: {
10736
+ minLength: 1,
10737
+ type: "string"
10738
+ },
10739
+ type: {
10740
+ enum: ["regex", "simple"],
10741
+ type: "string"
10742
+ }
10743
+ },
10744
+ required: ["type", "pattern"],
10745
+ type: "object"
10746
+ },
10747
+ type: "array"
10748
+ },
10749
+ custom: {},
10750
+ device: {},
10751
+ deviceType: {
10752
+ type: "string"
10753
+ },
10754
+ lifecycle: {},
10755
+ pageRules: {
10756
+ items: {},
10757
+ type: "array"
10758
+ },
10759
+ pages: {
10760
+ items: {},
10761
+ type: "array"
10762
+ },
10763
+ segments: {
10764
+ items: {},
10765
+ type: "array"
10766
+ },
10767
+ url: {
10768
+ items: {
10769
+ additionalProperties: false,
10770
+ properties: {
10771
+ include: {
10772
+ type: "boolean"
10773
+ },
10774
+ match_type: {
10775
+ enum: ["exact", "contains", "wildcard", "regex"],
10776
+ type: "string"
10777
+ },
10778
+ matchType: {
10779
+ enum: ["exact", "contains", "wildcard", "regex"],
10780
+ type: "string"
10781
+ },
10782
+ pattern: {
10783
+ minLength: 1,
10784
+ type: "string"
10785
+ },
10786
+ type: {
10787
+ enum: ["regex", "simple"],
10788
+ type: "string"
10789
+ }
10790
+ },
10791
+ required: ["type", "pattern"],
10792
+ type: "object"
10793
+ },
10794
+ type: "array"
10795
+ }
10796
+ },
10797
+ type: "object"
10798
+ },
10799
+ "variations/*/anti-flicker.json": {
10800
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10801
+ items: {
10802
+ type: "string"
10803
+ },
10804
+ type: "array"
10805
+ },
10806
+ "variations/*/index.css": {
10807
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10808
+ contentMediaType: "text/css",
10809
+ type: "string"
10810
+ },
10811
+ "variations/*/index.js": {
10812
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10813
+ contentMediaType: "text/javascript",
10814
+ type: "string"
10815
+ },
10816
+ "variations/*/mutations.json": {
10817
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10818
+ items: {
10819
+ additionalProperties: {},
10820
+ anyOf: [
10821
+ {
10822
+ required: ["selector"]
10823
+ },
10824
+ {
10825
+ properties: {
10826
+ action: {
10827
+ enum: [
10828
+ "priceTest",
10829
+ "shippingTest",
10830
+ "offerTest",
10831
+ "postPurchaseOffer",
10832
+ "checkoutBlock"
10833
+ ]
10834
+ }
10835
+ },
10836
+ required: ["action"]
10837
+ }
10838
+ ],
10839
+ properties: {
10840
+ action: {
10841
+ type: "string"
10842
+ },
10843
+ selector: {
10844
+ minLength: 1,
10845
+ type: "string"
10846
+ },
10847
+ type: {
10848
+ type: "string"
10849
+ }
10850
+ },
10851
+ type: "object"
10852
+ },
10853
+ type: "array"
10854
+ },
10855
+ "variations/*/reset.js": {
10856
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10857
+ contentMediaType: "text/javascript",
10858
+ type: "string"
10859
+ },
10860
+ "variations/*/url-blocks.json": {
10861
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10862
+ items: {},
10863
+ type: "array"
10864
+ },
10865
+ "variations/*/variation.json": {
10866
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10867
+ additionalProperties: false,
10868
+ properties: {
10869
+ artifactReview: {
10870
+ additionalProperties: false,
10871
+ properties: {
10872
+ hash: {
10873
+ anyOf: [
10874
+ {
10875
+ type: "string"
10876
+ },
10877
+ {
10878
+ type: "null"
10879
+ }
10880
+ ]
10881
+ },
10882
+ reviewedAt: {
10883
+ anyOf: [
10884
+ {
10885
+ type: "string"
10886
+ },
10887
+ {
10888
+ type: "null"
10889
+ }
10890
+ ]
10891
+ },
10892
+ reviewedBy: {
10893
+ anyOf: [
10894
+ {
10895
+ type: "string"
10896
+ },
10897
+ {
10898
+ type: "null"
10899
+ }
10900
+ ]
10901
+ },
10902
+ source: {
10903
+ anyOf: [
10904
+ {
10905
+ type: "string"
10906
+ },
10907
+ {
10908
+ type: "null"
10909
+ }
10910
+ ]
10911
+ },
10912
+ status: {
10913
+ type: "string"
10914
+ }
10915
+ },
10916
+ required: ["status"],
10917
+ type: "object"
10918
+ },
10919
+ codeDeliveryMode: {
10920
+ enum: ["inline", "external"],
10921
+ type: "string"
10922
+ },
10923
+ cssArtifactId: {
10924
+ anyOf: [
10925
+ {
10926
+ type: "string"
10927
+ },
10928
+ {
10929
+ type: "null"
10930
+ }
10931
+ ]
10932
+ },
10933
+ id: {
10934
+ anyOf: [
10935
+ {
10936
+ type: "string"
10937
+ },
10938
+ {
10939
+ type: "null"
10940
+ }
10941
+ ]
10942
+ },
10943
+ isControl: {
10944
+ type: "boolean"
10945
+ },
10946
+ jsArtifactId: {
10947
+ anyOf: [
10948
+ {
10949
+ type: "string"
10950
+ },
10951
+ {
10952
+ type: "null"
10953
+ }
10954
+ ]
10955
+ },
10956
+ name: {
10957
+ type: "string"
10958
+ },
10959
+ redirectUrl: {
10960
+ anyOf: [
10961
+ {
10962
+ type: "string"
10963
+ },
10964
+ {
10965
+ type: "null"
10966
+ }
10967
+ ]
10968
+ },
10969
+ schemaVersion: {
10970
+ const: 1,
10971
+ type: "number"
10972
+ },
10973
+ slug: {
10974
+ type: "string"
10975
+ },
10976
+ weight: {
10977
+ maximum: 1,
10978
+ minimum: 0,
10979
+ type: "number"
10980
+ }
10981
+ },
10982
+ required: ["schemaVersion"],
10983
+ type: "object"
10984
+ }
10985
+ },
10986
+ schemaVersion: 1,
10987
+ source: "experiment-filesystem"
10988
+ };
10989
+
10162
10990
  // src/commands/experiments.ts
10163
10991
  function formatDate(iso) {
10164
10992
  return new Date(iso).toLocaleDateString("en-US", {
@@ -10222,6 +11050,13 @@ function formatRevenue(value, currency) {
10222
11050
  return String(value);
10223
11051
  }
10224
11052
  }
11053
+ function audienceLine(segmentApplied) {
11054
+ if (!segmentApplied || typeof segmentApplied !== "object") return null;
11055
+ const { requested, applied, reason: reason2 } = segmentApplied;
11056
+ if (!requested) return null;
11057
+ const label = `${requested.mode === "exclude" ? "excluding " : ""}${String(requested.dim)}=${String(requested.value)}`;
11058
+ return applied === true ? label : `all visitors (${label} not applied: ${typeof reason2 === "string" ? reason2 : "unknown reason"})`;
11059
+ }
10225
11060
  function printExperimentResults(results2, currency) {
10226
11061
  const summary = summarizeResults(results2);
10227
11062
  const { experiment: experiment2, totals, decision, verdictWithheld, statisticsState, presentedOutcome } = summary;
@@ -10229,6 +11064,8 @@ function printExperimentResults(results2, currency) {
10229
11064
  console.log();
10230
11065
  console.log(source_default.bold(String(experiment2.name ?? experiment2.id ?? "Experiment results")));
10231
11066
  console.log(` Status: ${statusBadge(String(experiment2.status ?? "unknown"))}`);
11067
+ const audience = audienceLine(results2.segment_applied);
11068
+ if (audience) console.log(` Audience: ${audience}`);
10232
11069
  console.log(` Visitors: ${String(totals.visitors ?? 0)}`);
10233
11070
  console.log(` Conversions: ${String(totals.primaryConversions ?? 0)}`);
10234
11071
  console.log(` Revenue: ${formatRevenue(totals.revenue, currency)}`);
@@ -10292,18 +11129,11 @@ function printExperimentResults(results2, currency) {
10292
11129
  }
10293
11130
  console.log();
10294
11131
  }
11132
+ var filesystemPaths = Object.keys(experiment_filesystem_schema_default.files);
10295
11133
  var EXPERIMENT_FILESYSTEM_PATH_SCHEMA = {
10296
- topLevel: ["experiment.json", "targeting.json", "metrics.json", "qa.json", "launch.json"],
10297
- experiment: ["index.css", "index.js", "reset.js"],
10298
- variation: [
10299
- "variation.json",
10300
- "mutations.json",
10301
- "url-blocks.json",
10302
- "index.css",
10303
- "index.js",
10304
- "reset.js",
10305
- "anti-flicker.json"
10306
- ]
11134
+ topLevel: filesystemPaths.filter((entry) => !entry.includes("/")),
11135
+ experiment: filesystemPaths.filter((entry) => entry.startsWith("experiment/")).map((entry) => entry.slice("experiment/".length)),
11136
+ variation: filesystemPaths.filter((entry) => entry.startsWith("variations/*/")).map((entry) => entry.slice("variations/*/".length))
10307
11137
  };
10308
11138
  var SERVER_OWNED_EXPERIMENT_FILES = /* @__PURE__ */ new Set(["qa.json", "launch.json"]);
10309
11139
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -10507,14 +11337,31 @@ function printFilesystemPlan(plan2) {
10507
11337
  }
10508
11338
  console.log();
10509
11339
  console.log(`QA invalidated: ${plan2.invalidatesQa ? source_default.yellow("yes") : "no"}`);
10510
- console.log(`Acknowledgment: ${plan2.invalidatesQa ? "--accept-qa-invalidation" : "none"}`);
11340
+ console.log(
11341
+ `QA approval: ${!plan2.qaApproval ? "unknown (older server)" : plan2.qaApproval.present ? `present${plan2.qaApproval.approvedAt ? ` (approved ${plan2.qaApproval.approvedAt})` : ""}` : "none"}`
11342
+ );
11343
+ console.log(`Acknowledgment: ${requiredQaAcknowledgment(plan2) ?? "none"}`);
10511
11344
  console.log(`Precompile: ${plan2.precompileRequired ? "yes" : "no"}`);
10512
11345
  }
11346
+ function requiredQaAcknowledgment(plan2) {
11347
+ return "requiredAcknowledgment" in plan2 ? plan2.requiredAcknowledgment ?? null : plan2.invalidatesQa ? "accept-qa-invalidation" : null;
11348
+ }
10513
11349
  function blockingIssueSummary(issues) {
10514
11350
  if (!issues?.length) return "Experiment filesystem plan has errors.";
10515
11351
  const remaining = issues.length - 1;
10516
11352
  return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
10517
11353
  }
11354
+ function qaApprovalRefusal(approvedAt, folderArg) {
11355
+ return {
11356
+ message: `Applying these changes revokes the QA approval from ${approvedAt ?? "an unspecified time"}; pass --accept-qa-invalidation to confirm.`,
11357
+ details: {
11358
+ reason: "qa_approval_present",
11359
+ qa_approved_at: approvedAt,
11360
+ required_acknowledgment: "accept-qa-invalidation",
11361
+ retry: `apex experiments apply ${folderArg} --accept-qa-invalidation`
11362
+ }
11363
+ };
11364
+ }
10518
11365
  async function getQaPreviewUrls(creds, experimentId) {
10519
11366
  const fallbackDashboardUrl = buildDashboardUrl(
10520
11367
  creds.baseUrl,
@@ -10738,20 +11585,31 @@ var list = new Command("list").alias("ls").description(
10738
11585
  "List experiments in summary view by default; use --full for details or --fields for selected columns (archived experiments are hidden by default)"
10739
11586
  ).option(
10740
11587
  "-s, --status <status>",
10741
- "Filter by status (draft|awaiting_client_approval|changes_requested|client_approved|qa|ready_to_launch|running|paused|completed)"
11588
+ "Filter by status (draft|awaiting_client_approval|changes_requested|client_approved|qa|ready_to_launch|running|paused|completed) (applied server-side)"
11589
+ ).option(
11590
+ "--url <absolute-url>",
11591
+ "Filter by effective storefront URL targeting (applied server-side)"
10742
11592
  ).option("-n, --limit <n>", "Max results", "20").option("--include-archived", "Include archived experiments").option("--full", "Return full experiment details instead of the default summary").option(
10743
11593
  "--fields <list>",
10744
11594
  "Comma-separated fields to return and table columns in order (JSON always keeps id)"
10745
11595
  ).action(async function action3(opts) {
10746
11596
  const creds = requireCredentials();
10747
11597
  const spinner = await createSpinner("Fetching experiments...");
10748
- const res = await listExperiments(creds, {
11598
+ const listDefault = () => listExperiments(creds, {
10749
11599
  status: opts.status,
10750
11600
  limit: parseInt(opts.limit, 10),
10751
11601
  includeArchived: opts.includeArchived,
10752
11602
  view: opts.full ? "full" : "summary",
10753
11603
  fields: opts.fields
10754
11604
  });
11605
+ const res = opts.url !== void 0 ? await listExperiments(creds, {
11606
+ status: opts.status,
11607
+ limit: parseInt(opts.limit, 10),
11608
+ includeArchived: opts.includeArchived,
11609
+ view: opts.full ? "full" : "summary",
11610
+ fields: opts.fields,
11611
+ url: opts.url
11612
+ }) : await listDefault();
10755
11613
  if (!res.ok) {
10756
11614
  spinner?.fail("Failed to fetch experiments");
10757
11615
  fail(this, res.error, classifyStatus(res.status), res.errorDetails);
@@ -10889,7 +11747,13 @@ ${JSON.stringify(data.order, null, 2)}`);
10889
11747
  console.log(`
10890
11748
  Definitions: ${data.definitions_url}`);
10891
11749
  }
10892
- var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").option("--explain", "Explain raw-event exclusions and residuals").option("--goal <id>", "Goal ID or event key for --explain").option("--order <key>", "Order label trail for --explain").option("--samples", "Show sample identifiers with --explain").action(async function action7(idOrSlug, opts) {
11750
+ var results = new Command("results").description("Read experiment results and timeseries by ID or slug").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--currency <code>", "Currency code for revenue formatting").option("--segment <dim:value>", "Include one audience segment").option("--exclude-segment <dim:value>", "Exclude one audience segment (include wins)").option("--explain", "Explain raw-event exclusions and residuals").option("--goal <id>", "Goal ID or event key for --explain").option("--order <key>", "Order label trail for --explain").option("--samples", "Show sample identifiers with --explain").action(async function action7(idOrSlug, opts) {
11751
+ if (opts.explain && (opts.segment !== void 0 || opts.excludeSegment !== void 0))
11752
+ fail(
11753
+ this,
11754
+ "--segment and --exclude-segment are only available for results, without --explain",
11755
+ EXIT_CODES.validation
11756
+ );
10893
11757
  if (!opts.explain && (opts.goal || opts.order || opts.samples))
10894
11758
  fail(this, "--goal, --order and --samples require --explain", EXIT_CODES.validation);
10895
11759
  const creds = requireCredentials();
@@ -10900,7 +11764,11 @@ var results = new Command("results").description("Read experiment results and ti
10900
11764
  spinner?.fail(`Experiment "${idOrSlug}" not found`);
10901
11765
  fail(this, `Experiment "${idOrSlug}" not found.`, EXIT_CODES.validation);
10902
11766
  }
10903
- const res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, { currency: opts.currency });
11767
+ const res = opts.explain ? await getExperimentResultsExplain(creds, exp.id, opts) : await getExperimentResults(creds, exp.id, {
11768
+ currency: opts.currency,
11769
+ segment: opts.segment,
11770
+ exclude_segment: opts.excludeSegment
11771
+ });
10904
11772
  if (!res.ok || !res.data) {
10905
11773
  spinner?.fail("Failed to fetch experiment results");
10906
11774
  fail(
@@ -11174,7 +12042,7 @@ var create = new Command("create").description(
11174
12042
  []
11175
12043
  ).option(
11176
12044
  "--goal <goal-id|new:fields>",
11177
- "Goal id or new:type=...,selector=...,name=... (repeatable)",
12045
+ "Goal id or new:type=...,selector=...,name=... (repeatable; omitted uses the shop default main goal)",
11178
12046
  collectCreateOption,
11179
12047
  []
11180
12048
  ).option(
@@ -11593,6 +12461,7 @@ var pullFilesystem = new Command("pull").description("Pull an experiment as an e
11593
12461
  output(
11594
12462
  this,
11595
12463
  {
12464
+ schemaVersion: 1,
11596
12465
  folder,
11597
12466
  experimentId: exp.id,
11598
12467
  files: res.data.files.map((file) => import_node_path6.default.join(folder, file.path))
@@ -11890,7 +12759,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
11890
12759
  folder,
11891
12760
  experimentId: session.experimentId,
11892
12761
  plan: res.data,
11893
- requiredAcknowledgment: res.data.invalidatesQa ? "accept-qa-invalidation" : null
12762
+ requiredAcknowledgment: requiredQaAcknowledgment(res.data)
11894
12763
  },
11895
12764
  () => printFilesystemPlan(res.data)
11896
12765
  );
@@ -11902,7 +12771,10 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
11902
12771
  var applyFilesystem = new Command("apply").option(
11903
12772
  "--confirm-targeting-replace",
11904
12773
  "Confirm that attaching a Page may replace URL targeting authored through the API"
11905
- ).option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option("--accept-qa-invalidation", "Acknowledge that applying changes invalidates QA approval").addOption(new Option("-f, --force", "Deprecated alias for --accept-qa-invalidation").hideHelp()).option("--no-asset-upload", "Keep inline and local image references unchanged").action(async function action15(folderArg, opts) {
12774
+ ).option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option(
12775
+ "--accept-qa-invalidation",
12776
+ "Confirm an apply that revokes an existing QA approval (required only when the experiment holds one)"
12777
+ ).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) {
11906
12778
  const creds = requireCredentials();
11907
12779
  try {
11908
12780
  const folder = folderPath(folderArg);
@@ -11949,7 +12821,7 @@ var applyFilesystem = new Command("apply").option(
11949
12821
  why: "Retry with hosted asset upload."
11950
12822
  }
11951
12823
  ] : [],
11952
- requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null
12824
+ requiredAcknowledgment: requiredQaAcknowledgment(planRes.data)
11953
12825
  });
11954
12826
  }
11955
12827
  if (planRes.data.changes.length === 0) {
@@ -11966,7 +12838,7 @@ var applyFilesystem = new Command("apply").option(
11966
12838
  synced: synced2,
11967
12839
  dashboardUrl: dashboardUrl2,
11968
12840
  plan: planRes.data,
11969
- requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
12841
+ requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
11970
12842
  message: "No changes."
11971
12843
  },
11972
12844
  () => {
@@ -11978,23 +12850,23 @@ var applyFilesystem = new Command("apply").option(
11978
12850
  );
11979
12851
  return;
11980
12852
  }
11981
- if (planRes.data.invalidatesQa && !opts.force && !opts.acceptQaInvalidation) {
11982
- const ok = await confirmApply("This apply invalidates QA approval. Continue?");
12853
+ let acceptQaInvalidation = Boolean(opts.force || opts.acceptQaInvalidation);
12854
+ if (requiredQaAcknowledgment(planRes.data) && !acceptQaInvalidation) {
12855
+ const refusal = qaApprovalRefusal(planRes.data.qaApproval?.approvedAt ?? null, folderArg);
12856
+ const ok = await confirmApply(`${refusal.message} Continue?`);
11983
12857
  if (ok === null) {
11984
- confirmationRequired(
11985
- this,
11986
- "Applying these changes invalidates QA approval and requires confirmation.",
11987
- `apex experiments apply ${folderArg} --accept-qa-invalidation`
11988
- );
12858
+ fail(this, refusal.message, EXIT_CODES.confirmationRequired, refusal.details);
11989
12859
  }
11990
12860
  if (!ok) {
11991
12861
  output(this, { aborted: true }, () => console.log("Aborted."));
11992
12862
  return;
11993
12863
  }
12864
+ acceptQaInvalidation = true;
11994
12865
  }
11995
12866
  const spinner = process.stderr.isTTY ? await createSpinner("Applying experiment filesystem...") : null;
11996
12867
  let res = await applyExperimentFilesystem(creds, session.experimentId, {
11997
12868
  files,
12869
+ accept_qa_invalidation: acceptQaInvalidation,
11998
12870
  ...opts.confirmTargetingReplace ? { confirmTargetingReplace: true } : {}
11999
12871
  });
12000
12872
  if (!res.ok && res.status === 409 && res.errorDetails?.code === "confirm_targeting_replace_required" && !opts.confirmTargetingReplace) {
@@ -12015,9 +12887,19 @@ var applyFilesystem = new Command("apply").option(
12015
12887
  }
12016
12888
  res = await applyExperimentFilesystem(creds, session.experimentId, {
12017
12889
  files,
12890
+ accept_qa_invalidation: acceptQaInvalidation,
12018
12891
  confirmTargetingReplace: true
12019
12892
  });
12020
12893
  }
12894
+ if (!res.ok && res.status === 409 && res.errorDetails?.code === "qa_acknowledgment_required") {
12895
+ spinner?.stop();
12896
+ const approvedAt = res.errorDetails.qa_approved_at;
12897
+ const refusal = qaApprovalRefusal(
12898
+ typeof approvedAt === "string" ? approvedAt : null,
12899
+ folderArg
12900
+ );
12901
+ fail(this, refusal.message, EXIT_CODES.confirmationRequired, refusal.details);
12902
+ }
12021
12903
  if (!res.ok || !res.data?.applied) {
12022
12904
  spinner?.fail("Failed to apply experiment filesystem");
12023
12905
  fail(this, res.error ?? "Apply failed", classifyStatus(res.status), {
@@ -12056,7 +12938,7 @@ var applyFilesystem = new Command("apply").option(
12056
12938
  experimentId: session.experimentId,
12057
12939
  applied: true,
12058
12940
  plan: planRes.data,
12059
- requiredAcknowledgment: planRes.data.invalidatesQa ? "accept-qa-invalidation" : null,
12941
+ requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
12060
12942
  result: res.data,
12061
12943
  synced,
12062
12944
  dashboardUrl,
@@ -12408,7 +13290,55 @@ var qaEvidence = new Command("evidence").description("Attach screenshot jobs or
12408
13290
  throw error;
12409
13291
  }
12410
13292
  });
12411
- var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
13293
+ var qa = new Command("qa").description("Manage experiment QA runs from the CLI").addCommand(qaStatus).addCommand(
13294
+ new Command("events").description("List QA-tagged events for an experiment session").argument("<id-or-slug>", EXPERIMENT_REFERENCE_DESCRIPTION).option("--session <id>", "QA session id (defaults to the latest QA run)").option("--minutes <n>", "Lookback window in minutes", "60").option("--type <type>", "Event type: all, goal, or custom", "all").action(async function(target, opts) {
13295
+ const minutes = Number(opts.minutes);
13296
+ if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440)
13297
+ fail(this, "--minutes must be an integer between 5 and 1440", EXIT_CODES.validation);
13298
+ if (!["all", "goal", "custom"].includes(opts.type))
13299
+ fail(this, "--type must be all, goal, or custom", EXIT_CODES.validation);
13300
+ const creds = requireCredentials();
13301
+ const experiment2 = { id: (await resolveExperimentForFilesystemOrId(target)).experimentId };
13302
+ let session = opts.session;
13303
+ if (!session) {
13304
+ const status4 = await getExperimentQaRun(creds, experiment2.id);
13305
+ if (!status4.ok || !status4.data)
13306
+ throw new CliError(
13307
+ status4.error ?? "Failed to fetch QA run",
13308
+ classifyStatus(status4.status),
13309
+ status4.errorDetails
13310
+ );
13311
+ session = status4.data.run.id;
13312
+ }
13313
+ const response = await getExperimentQaEvents(creds, experiment2.id, {
13314
+ session,
13315
+ minutes,
13316
+ type: opts.type
13317
+ });
13318
+ if (!response.ok || !response.data)
13319
+ throw new CliError(
13320
+ response.error ?? "Failed to fetch QA events",
13321
+ classifyStatus(response.status),
13322
+ response.errorDetails
13323
+ );
13324
+ output(
13325
+ this,
13326
+ response.data,
13327
+ () => printTable(
13328
+ ["TYPE", "EVENT", "VARIATION", "SESSION", "COUNT", "LAST SEEN", "LAST URL"],
13329
+ response.data.data.map((row) => [
13330
+ row.event_type === "goal" ? "goal" : "custom",
13331
+ row.event_name || row.goal_id,
13332
+ row.variation_id,
13333
+ row.qa_session_id,
13334
+ String(row.events),
13335
+ row.last_event_at ?? "\u2014",
13336
+ row.last_url ?? "\u2014"
13337
+ ])
13338
+ )
13339
+ );
13340
+ })
13341
+ ).addCommand(qaCheck).addCommand(qaNotes).addCommand(qaApprove).addCommand(qaReopen).addCommand(qaEvidence);
12412
13342
  var SCREENSHOT_DEVICES = [
12413
13343
  "desktop_chrome",
12414
13344
  "iphone_15_pro",
@@ -13057,6 +13987,25 @@ var pushCommand = new Command("push").description("Push experiment definitions f
13057
13987
 
13058
13988
  // src/commands/preview.ts
13059
13989
  init_source();
13990
+ async function signedPreviewFromReadiness(creds, experimentId, variationId, targetUrl) {
13991
+ try {
13992
+ const client = createHttpClient(creds, {
13993
+ onNetworkError: (info) => {
13994
+ throw new Error(info.cause);
13995
+ }
13996
+ });
13997
+ const query = new URLSearchParams({ variation_id: variationId, target_url: targetUrl });
13998
+ const response = await client.request(
13999
+ "GET",
14000
+ `/api/v1/experiments/${encodeURIComponent(experimentId)}/readiness?${query}`,
14001
+ void 0,
14002
+ 5e3
14003
+ );
14004
+ return response.ok ? response.data?.data : void 0;
14005
+ } catch {
14006
+ return void 0;
14007
+ }
14008
+ }
13060
14009
  var previewCommand = new Command("preview").description(
13061
14010
  "Open QA preview URL for an experiment variation; proxy previews return a QA run receipt"
13062
14011
  ).argument("<id-or-slug>", "Experiment ID or slug").argument("[variation-id]", "Variation ID (defaults to first non-control)").option("--store-url <url>", "Override experiment store_url (target website URL)").option("--proxy", "Create an artifact-injecting proxy preview").option("--ttl <hours>", "Proxy preview lifetime in hours (1-168)").option("--no-open", "Print URL instead of opening browser").action(
@@ -13168,28 +14117,51 @@ var previewCommand = new Command("preview").description(
13168
14117
  variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
13169
14118
  }
13170
14119
  spinner.stop();
13171
- const storeUrl = opts?.storeUrl ?? exp.preview_target?.url ?? exp.store_url;
14120
+ const storeUrl = opts?.storeUrl ?? (!variation.is_control && variation.redirect_url?.trim() ? variation.redirect_url : null) ?? exp.preview_target?.url ?? exp.store_url;
13172
14121
  if (!storeUrl) {
13173
14122
  fail(
13174
14123
  previewCommand,
13175
14124
  'Experiment is missing "store_url". Set it on the experiment or pass --store-url <url>.'
13176
14125
  );
13177
14126
  }
14127
+ const snapshot = await getExperimentFilesystem(creds, exp.id).catch(() => null);
14128
+ let qaRun;
14129
+ if (snapshot?.ok && snapshot.data) {
14130
+ const qaFile = snapshot.data.files.find((file) => file.path === "qa.json");
14131
+ if (qaFile) {
14132
+ try {
14133
+ const run3 = JSON.parse(qaFile.content)?.latestRun;
14134
+ if (run3 && typeof run3.id === "string" && run3.id.trim()) qaRun = run3;
14135
+ } catch {
14136
+ }
14137
+ }
14138
+ }
13178
14139
  let previewUrl;
13179
14140
  try {
13180
- previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id);
14141
+ previewUrl = buildQaPreviewUrl2(storeUrl, exp.id, variation.id, qaRun?.id);
13181
14142
  } catch {
13182
14143
  fail(
13183
14144
  previewCommand,
13184
14145
  'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".'
13185
14146
  );
13186
14147
  }
14148
+ const signed = await signedPreviewFromReadiness(creds, exp.id, variation.id, storeUrl);
14149
+ const selectedPreviewUrl = signed?.signed_preview_url ?? previewUrl.toString();
14150
+ if (signed?.signed_preview_url) {
14151
+ const signedSessionId = signed.signed_preview_qa_session_id;
14152
+ qaRun = signedSessionId ? qaRun?.id === signedSessionId ? qaRun : { id: signedSessionId } : void 0;
14153
+ }
14154
+ const warning = signed?.signed_preview_url ? null : "Test images may not load on this unsigned link. Open Apex \u2192 QA for a signed link.";
14155
+ if (warning) console.error(warning);
13187
14156
  output(
13188
14157
  previewCommand,
13189
14158
  {
13190
14159
  experimentId: exp.id,
13191
14160
  variationId: variation.id,
13192
- previewUrl: previewUrl.toString(),
14161
+ previewUrl: selectedPreviewUrl,
14162
+ expiresAt: signed?.signed_preview_expires_at ?? null,
14163
+ ...warning ? { warning } : {},
14164
+ ...qaRun ? { qa_run: qaRun, qa_session_id: qaRun.id } : {},
13193
14165
  ...!opts?.storeUrl && exp.preview_target ? { previewTarget: exp.preview_target } : {}
13194
14166
  },
13195
14167
  () => {
@@ -13205,14 +14177,16 @@ var previewCommand = new Command("preview").description(
13205
14177
  }
13206
14178
  }
13207
14179
  console.log();
13208
- console.log(source_default.cyan(previewUrl.toString()));
14180
+ console.log(source_default.cyan(selectedPreviewUrl));
14181
+ if (signed?.signed_preview_expires_at)
14182
+ console.log(`Valid until ${signed.signed_preview_expires_at}.`);
13209
14183
  console.log();
13210
14184
  }
13211
14185
  );
13212
14186
  if (opts?.open !== false && !shouldUseJson(previewCommand)) {
13213
14187
  try {
13214
14188
  const open3 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
13215
- await open3(previewUrl.toString());
14189
+ await open3(selectedPreviewUrl);
13216
14190
  console.log(source_default.dim("Opened in browser."));
13217
14191
  } catch {
13218
14192
  console.log(source_default.dim("Copy the URL above to preview."));
@@ -14199,9 +15173,7 @@ function commonCookieDomain(hostnames) {
14199
15173
  const suffix = suffixes[0];
14200
15174
  return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
14201
15175
  }
14202
- function defaultManifestPath() {
14203
- return import_node_path11.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
14204
- }
15176
+ var DEFAULT_MANIFEST_PATH = ".apex/tracking-manifest.json";
14205
15177
  function readManifest(filePath) {
14206
15178
  if (!import_node_fs13.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
14207
15179
  const parsed = JSON.parse(import_node_fs13.default.readFileSync(filePath, "utf8"));
@@ -14277,7 +15249,7 @@ function buildManifest(urlArgs, opts) {
14277
15249
  };
14278
15250
  }
14279
15251
  function manifestPath(input) {
14280
- return import_node_path11.default.resolve(process.cwd(), input ?? defaultManifestPath());
15252
+ return import_node_path11.default.resolve(process.cwd(), input ?? DEFAULT_MANIFEST_PATH);
14281
15253
  }
14282
15254
  function printManifestPlan(manifest) {
14283
15255
  console.log(source_default.bold("Apex tracking ingestion plan"));
@@ -14305,7 +15277,11 @@ function printManifestPlan(manifest) {
14305
15277
  var trackingCommand = new Command("tracking").description(
14306
15278
  "Create Apex-first tracking ingestion manifests and QA handoffs"
14307
15279
  );
14308
- trackingCommand.command("inspect").description("Create a local tracking manifest from funnel URLs").argument("<urls...>", "Landing, storefront, checkout, or confirmation URLs").option("--shop-id <id>", "Apex shop ID for snippet generation").option("--endpoint <url>", "Apex Worker endpoint", "https://events.drip-apex.com").option("--out <path>", "Manifest output path", defaultManifestPath()).action(function(urls, opts) {
15280
+ trackingCommand.command("inspect").description("Create a local tracking manifest from funnel URLs").argument("<urls...>", "Landing, storefront, checkout, or confirmation URLs").option("--shop-id <id>", "Apex shop ID for snippet generation").option("--endpoint <url>", "Apex Worker endpoint", "https://events.drip-apex.com").option(
15281
+ "--out <path>",
15282
+ "Manifest output path, relative to the current directory",
15283
+ DEFAULT_MANIFEST_PATH
15284
+ ).action(function(urls, opts) {
14309
15285
  const manifest = buildManifest(urls, opts);
14310
15286
  const outPath = manifestPath(opts.out);
14311
15287
  writeManifest(outPath, manifest);
@@ -14321,12 +15297,20 @@ trackingCommand.command("inspect").description("Create a local tracking manifest
14321
15297
  }
14322
15298
  });
14323
15299
  });
14324
- trackingCommand.command("plan").description("Print the implementation plan from a tracking manifest").argument("[manifest]", "Tracking manifest path", defaultManifestPath()).action(function(input) {
15300
+ trackingCommand.command("plan").description("Print the implementation plan from a tracking manifest").argument(
15301
+ "[manifest]",
15302
+ "Tracking manifest path, relative to the current directory",
15303
+ DEFAULT_MANIFEST_PATH
15304
+ ).action(function(input) {
14325
15305
  const filePath = manifestPath(input);
14326
15306
  const manifest = readManifest(filePath);
14327
15307
  output(this, { path: filePath, manifest }, () => printManifestPlan(manifest));
14328
15308
  });
14329
- trackingCommand.command("qa").description("Print the QA checklist for a tracking manifest").argument("[manifest]", "Tracking manifest path", defaultManifestPath()).action(function(input) {
15309
+ trackingCommand.command("qa").description("Print the QA checklist for a tracking manifest").argument(
15310
+ "[manifest]",
15311
+ "Tracking manifest path, relative to the current directory",
15312
+ DEFAULT_MANIFEST_PATH
15313
+ ).action(function(input) {
14330
15314
  const filePath = manifestPath(input);
14331
15315
  const manifest = readManifest(filePath);
14332
15316
  output(this, { path: filePath, checks: manifest.qa.checks }, () => {
@@ -18540,16 +19524,21 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
18540
19524
  message: "Worker served a recognized config document for this shop."
18541
19525
  };
18542
19526
  }
18543
- 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(
19527
+ var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description(
19528
+ "Verify snippet, served config, and recent tracking using --shop-id, the selected shop, or DRIP_SHOP_ID"
19529
+ ).option("--url <url>", "Check one explicit bound storefront URL").option(
19530
+ "--shop-id <id>",
19531
+ "Apex shop ID to verify (falls back to --shop/apex use, then DRIP_SHOP_ID)"
19532
+ ).option(
18544
19533
  "--worker-url <url>",
18545
19534
  "Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
18546
19535
  ).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action65(opts) {
18547
19536
  const creds = requireCredentials();
18548
- const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
19537
+ const shopId = opts.shopId?.trim() || resolveContext(creds).shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
18549
19538
  if (!shopId)
18550
19539
  fail(
18551
19540
  this,
18552
- "Shop ID is required: pass --shop-id <id> or set DRIP_SHOP_ID",
19541
+ "Shop ID is required: pass --shop-id <id>, select a shop with --shop <id> or apex use, or set DRIP_SHOP_ID",
18553
19542
  EXIT_CODES.validation,
18554
19543
  { field: "shop-id" }
18555
19544
  );
@@ -19589,6 +20578,7 @@ var test2 = new Command("test").description("Evaluate experiment URL targeting f
19589
20578
  var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
19590
20579
 
19591
20580
  // src/commands/sos.ts
20581
+ var import_node_crypto7 = require("node:crypto");
19592
20582
  init_source();
19593
20583
  var import_node_fs24 = __toESM(require("node:fs"));
19594
20584
  var import_node_path21 = __toESM(require("node:path"));
@@ -19630,6 +20620,11 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
19630
20620
  "--type <type>",
19631
20621
  "full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
19632
20622
  "qa"
20623
+ ).option(
20624
+ "--attach <file>",
20625
+ "Attach an image or video (repeatable, maximum 5)",
20626
+ (file, files) => [...files, file],
20627
+ []
19633
20628
  ).option("--loom <url>", "Loom URL with reproduction or QA context").option("--experiment <id>", "Experiment ID").option("--backlog-item <id>", "Backlog item ID (internal SOS only)").option("--sub-class <text>", "SOS sub-classification").option("--description <text-or-@file>", "Description text or @path").argument("[description]", "Description text").action(async function action68(descriptionArg, opts) {
19634
20629
  if (!SOS_TYPES.has(opts.type)) {
19635
20630
  fail(
@@ -19641,16 +20636,76 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
19641
20636
  if (opts.experiment && opts.backlogItem) {
19642
20637
  fail(this, "Provide only one of --experiment or --backlog-item", 3);
19643
20638
  }
20639
+ const mediaTypes = {
20640
+ ".png": "image/png",
20641
+ ".jpg": "image/jpeg",
20642
+ ".jpeg": "image/jpeg",
20643
+ ".webp": "image/webp",
20644
+ ".gif": "image/gif",
20645
+ ".mp4": "video/mp4",
20646
+ ".webm": "video/webm",
20647
+ ".mov": "video/quicktime"
20648
+ };
20649
+ if (opts.attach.length > 5) fail(this, "Attach no more than 5 files", 3);
20650
+ const files = opts.attach.map((file) => {
20651
+ const filename = import_node_path21.default.basename(file);
20652
+ const content_type = mediaTypes[import_node_path21.default.extname(file).toLowerCase()];
20653
+ if (!content_type) fail(this, "Unsupported attachment type", 3);
20654
+ let size;
20655
+ try {
20656
+ size = import_node_fs24.default.statSync(file).size;
20657
+ } catch {
20658
+ fail(this, `Attachment not found: ${file}`, 3);
20659
+ }
20660
+ const cap = (content_type.startsWith("video/") ? 50 : 10) * 1024 * 1024;
20661
+ if (size <= 0 || size > cap) fail(this, "Attachment exceeds its size limit or is empty", 3);
20662
+ let bytes;
20663
+ try {
20664
+ bytes = import_node_fs24.default.readFileSync(file);
20665
+ } catch {
20666
+ fail(this, `Attachment not found: ${file}`, 3);
20667
+ }
20668
+ return { filename, content_type, size, bytes };
20669
+ });
19644
20670
  const creds = requireCredentials();
20671
+ const attachmentPaths = [];
20672
+ const attachmentDigests = [];
20673
+ if (files.length) {
20674
+ const grants = await createSosUploadGrants(
20675
+ creds,
20676
+ files.map(({ filename, content_type, size }) => ({ filename, content_type, size }))
20677
+ );
20678
+ if (!grants.ok || !grants.data)
20679
+ fail(
20680
+ this,
20681
+ grants.error ?? "Failed to get upload grants",
20682
+ classifyStatus(grants.status),
20683
+ grants.errorDetails
20684
+ );
20685
+ if (grants.data.data.uploads.length !== files.length)
20686
+ fail(this, "Invalid upload grant response", 1);
20687
+ for (const [index, file] of files.entries()) {
20688
+ const grant = grants.data.data.uploads[index];
20689
+ const bytes = file.bytes;
20690
+ await uploadSosAttachment(grant.signed_url, file.content_type, bytes);
20691
+ attachmentPaths.push(grant.path);
20692
+ attachmentDigests.push((0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex"));
20693
+ }
20694
+ }
19645
20695
  const description = readText(opts.description) ?? descriptionArg ?? `Apex CLI ${opts.type} SOS request${opts.loom ? ` with Loom evidence: ${opts.loom}` : "."}`;
19646
- const res = await createSos(creds, {
19647
- type: opts.type,
19648
- description,
19649
- experiment_id: opts.experiment ?? null,
19650
- backlog_item_id: opts.backlogItem ?? null,
19651
- sub_class: opts.subClass ?? null,
19652
- loom_url: opts.loom ?? null
19653
- });
20696
+ const res = await createSos(
20697
+ creds,
20698
+ {
20699
+ ...attachmentPaths.length ? { attachment_paths: attachmentPaths } : {},
20700
+ type: opts.type,
20701
+ description,
20702
+ experiment_id: opts.experiment ?? null,
20703
+ backlog_item_id: opts.backlogItem ?? null,
20704
+ sub_class: opts.subClass ?? null,
20705
+ loom_url: opts.loom ?? null
20706
+ },
20707
+ { attachmentDigests }
20708
+ );
19654
20709
  if (!res.ok || !res.data) {
19655
20710
  fail(
19656
20711
  this,
@@ -19955,6 +21010,7 @@ function optionSchema(option) {
19955
21010
  description: option.description,
19956
21011
  required: option.required,
19957
21012
  optional: option.optional,
21013
+ mandatory: Boolean(option.mandatory),
19958
21014
  variadic: option.variadic,
19959
21015
  ...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
19960
21016
  };
@@ -19977,8 +21033,17 @@ function commandSchema(command) {
19977
21033
  };
19978
21034
  }
19979
21035
  function createSchemaCommand(program3) {
19980
- return new Command("schema").description("Print the current Apex CLI command and flag schema").option("--json", "Print JSON schema").action(function action78() {
19981
- const schema = commandSchema(program3);
21036
+ return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
21037
+ const schema = {
21038
+ schemaVersion: 2,
21039
+ contract: {
21040
+ optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
21041
+ mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
21042
+ requiredArguments: "An argument's required field means the positional is mandatory."
21043
+ },
21044
+ ...commandSchema(program3),
21045
+ files: experiment_filesystem_schema_default
21046
+ };
19982
21047
  output(this, schema, () => {
19983
21048
  process.stdout.write(`${JSON.stringify(schema, null, 2)}
19984
21049
  `);
@@ -20618,7 +21683,15 @@ var skillsCommand = new Command("skills").description("Print or install the Apex
20618
21683
  });
20619
21684
  });
20620
21685
 
21686
+ // ../../ops/org-split/move-plan.mjs
21687
+ var uuid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
21688
+ function validateMoveInput(shopIds, toOrgId) {
21689
+ if (typeof toOrgId !== "string" || !uuid.test(toOrgId) || !Array.isArray(shopIds) || !shopIds.length || shopIds.some((id) => typeof id !== "string" || !uuid.test(id))) throw new Error("Choose valid store and destination UUIDs");
21690
+ return { shopIds: [...new Set(shopIds.map((id) => id.toLowerCase()))].sort(), toOrgId: toOrgId.toLowerCase() };
21691
+ }
21692
+
20621
21693
  // src/commands/admin.ts
21694
+ var uuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
20622
21695
  function switchValue(value) {
20623
21696
  if (value === "on") return true;
20624
21697
  if (value === "off") return false;
@@ -20745,8 +21818,7 @@ var create6 = new Command("create").description("Create an internal, demo or dog
20745
21818
  fail(this, "--name must be 1 to 60 characters", 3);
20746
21819
  if (opts.reason.trim().length < 8 || opts.reason.trim().length > 500)
20747
21820
  fail(this, "--reason must be 8 to 500 characters", 3);
20748
- const 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;
20749
- if (opts.claimShop && !uuid.test(opts.claimShop)) fail(this, "--claim-shop must be a UUID", 3);
21821
+ if (opts.claimShop && !uuid2.test(opts.claimShop)) fail(this, "--claim-shop must be a UUID", 3);
20750
21822
  if (opts.claimShop && (opts.domain || opts.platform))
20751
21823
  fail(this, "Use --claim-shop or --domain with --platform", 3);
20752
21824
  if (!!opts.domain !== !!opts.platform)
@@ -20767,7 +21839,7 @@ var create6 = new Command("create").description("Create an internal, demo or dog
20767
21839
  fail(this, "--owner requires a store for the invitation", 3);
20768
21840
  if (opts.plan !== "self_managed" && (!opts.consultant || !opts.consultantTitle?.trim()))
20769
21841
  fail(this, "Managed plans require --consultant and --consultant-title", 3);
20770
- if (opts.consultant && !uuid.test(opts.consultant))
21842
+ if (opts.consultant && !uuid2.test(opts.consultant))
20771
21843
  fail(this, "--consultant must be a platform access UUID", 3);
20772
21844
  if (!!opts.consultant !== !!opts.consultantTitle?.trim())
20773
21845
  fail(this, "Provide --consultant and --consultant-title together", 3);
@@ -20831,14 +21903,169 @@ var create6 = new Command("create").description("Create an internal, demo or dog
20831
21903
  );
20832
21904
  });
20833
21905
  });
20834
- var customers = new Command("customers").description("Create engineering accounts; customer accounts use Admin, Links").addCommand(create6);
21906
+ var setCategory = new Command("set-category").description("Change an existing account's category with an audit reason").argument("<org-id>", "Organization UUID").requiredOption("--category <category>", "internal, demo, dogfood or customer").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm category change").option("--json", "Output JSON").action(async function(orgId, opts) {
21907
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
21908
+ if (!["internal", "demo", "dogfood", "customer"].includes(opts.category))
21909
+ fail(this, "--category must be internal, demo, dogfood or customer", 3);
21910
+ const reason2 = opts.reason.trim();
21911
+ if (reason2.length < 8 || reason2.length > 500)
21912
+ fail(this, "--reason must be 8 to 500 characters", 3);
21913
+ if (!opts.confirm) {
21914
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
21915
+ confirmationRequired(
21916
+ this,
21917
+ "--confirm is required to change an account category",
21918
+ `apex admin customers set-category ${orgId} --category ${quote(opts.category)} --reason ${quote(reason2)} --confirm`
21919
+ );
21920
+ }
21921
+ const body5 = {
21922
+ category: opts.category,
21923
+ reason: reason2
21924
+ };
21925
+ const res = await setAdminCustomerCategory(requireUserCredentials(), orgId, body5);
21926
+ if (!res.ok || !res.data)
21927
+ fail(
21928
+ this,
21929
+ res.error ?? "Failed to change account category",
21930
+ classifyStatus(res.status),
21931
+ res.errorDetails
21932
+ );
21933
+ output(this, res.data, () => {
21934
+ const result = res.data.data;
21935
+ printTable(["RESULT"], [[`Customer ${result.customerId} moved to ${result.category}`]]);
21936
+ });
21937
+ });
21938
+ var setManagedBy = new Command("set-managed-by").description("Assign or clear a managing agency with an audit reason").argument("<org-id>", "Organization UUID").option("--by <org-id>", "Managing agency UUID").option("--clear", "Clear the managing agency").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm manager change").option("--json", "Output JSON").action(async function(orgId, opts) {
21939
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
21940
+ if (!!opts.by === !!opts.clear) fail(this, "Use exactly one of --by or --clear", 3);
21941
+ if (opts.by && !uuid2.test(opts.by)) fail(this, "--by must be a UUID", 3);
21942
+ const reason2 = opts.reason.trim();
21943
+ if (reason2.length < 8 || reason2.length > 500)
21944
+ fail(this, "--reason must be 8 to 500 characters", 3);
21945
+ if (!opts.confirm) {
21946
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
21947
+ confirmationRequired(
21948
+ this,
21949
+ "--confirm is required to change a managing agency",
21950
+ `apex admin customers set-managed-by ${orgId} ${opts.by ? `--by ${opts.by}` : "--clear"} --reason ${quote(reason2)} --confirm`
21951
+ );
21952
+ }
21953
+ const res = await setAdminCustomerManagedBy(requireUserCredentials(), orgId, {
21954
+ managed_by_org_id: opts.by ?? null,
21955
+ reason: reason2
21956
+ });
21957
+ if (!res.ok || !res.data)
21958
+ fail(
21959
+ this,
21960
+ res.error ?? "Failed to change managing agency",
21961
+ classifyStatus(res.status),
21962
+ res.errorDetails
21963
+ );
21964
+ output(
21965
+ this,
21966
+ res.data,
21967
+ () => printTable(
21968
+ ["CUSTOMER", "MANAGING AGENCY"],
21969
+ [[res.data.data.customerId, res.data.data.managed_by_org_id ?? "None"]]
21970
+ )
21971
+ );
21972
+ });
21973
+ var setWorkspaceType = new Command("set-workspace-type").description("Change a workspace type with an audit reason").argument("<org-id>", "Organization UUID").requiredOption("--type <company|agency>", "Workspace type").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm workspace type change").option("--json", "Output JSON").action(async function(orgId, opts) {
21974
+ if (!uuid2.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
21975
+ if (opts.type !== "company" && opts.type !== "agency")
21976
+ fail(this, "--type must be company or agency", 3);
21977
+ const reason2 = opts.reason.trim();
21978
+ if (reason2.length < 8 || reason2.length > 500)
21979
+ fail(this, "--reason must be 8 to 500 characters", 3);
21980
+ if (!opts.confirm) {
21981
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
21982
+ confirmationRequired(
21983
+ this,
21984
+ "--confirm is required to change a workspace type",
21985
+ `apex admin customers set-workspace-type ${orgId} --type ${opts.type} --reason ${quote(reason2)} --confirm`
21986
+ );
21987
+ }
21988
+ const res = await setAdminCustomerWorkspaceType(requireUserCredentials(), orgId, {
21989
+ workspace_type: opts.type,
21990
+ reason: reason2
21991
+ });
21992
+ if (!res.ok || !res.data)
21993
+ fail(
21994
+ this,
21995
+ res.error ?? "Failed to change workspace type",
21996
+ classifyStatus(res.status),
21997
+ res.errorDetails
21998
+ );
21999
+ output(
22000
+ this,
22001
+ res.data,
22002
+ () => printTable(
22003
+ ["CUSTOMER", "WORKSPACE TYPE"],
22004
+ [[res.data.data.customerId, res.data.data.workspace_type]]
22005
+ )
22006
+ );
22007
+ });
22008
+ function storeMoveRequest(opts) {
22009
+ const { shopIds, toOrgId } = validateMoveInput(opts.shop, opts.to);
22010
+ const reason2 = opts.reason.trim();
22011
+ if (reason2.length < 8 || reason2.length > 500)
22012
+ throw new Error("--reason must be 8 to 500 characters");
22013
+ return {
22014
+ toOrgId,
22015
+ body: {
22016
+ shop_ids: shopIds,
22017
+ reason: reason2,
22018
+ keep_grants: !!opts.keepGrants,
22019
+ dry_run: !opts.confirm
22020
+ }
22021
+ };
22022
+ }
22023
+ var moveStores = new Command("move-stores").description("Preview or confirm an audited set of store moves").requiredOption("--to <org-id>", "Destination organization UUID").requiredOption(
22024
+ "--shop <shop-id>",
22025
+ "Store UUID, repeat for each store",
22026
+ (value, previous) => [...previous, value],
22027
+ []
22028
+ ).requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--keep-grants", "Preserve explicit store roles and overrides").option("--confirm", "Perform the move; otherwise return a read-only plan").option("--json", "Output JSON").action(async function(opts) {
22029
+ let request2;
22030
+ try {
22031
+ request2 = storeMoveRequest(opts);
22032
+ } catch (error) {
22033
+ fail(this, error instanceof Error ? error.message : "Invalid move request", 3);
22034
+ }
22035
+ const res = await moveAdminCustomerStores(
22036
+ requireUserCredentials(),
22037
+ request2.toOrgId,
22038
+ request2.body
22039
+ );
22040
+ if (!res.ok || !res.data)
22041
+ fail(
22042
+ this,
22043
+ res.error ?? "Failed to move stores",
22044
+ classifyStatus(res.status),
22045
+ res.errorDetails
22046
+ );
22047
+ output(this, res.data, () => {
22048
+ printTable(
22049
+ ["FIELD", "VALUE"],
22050
+ Object.entries(res.data.data).map(([key, value]) => [key, JSON.stringify(value)])
22051
+ );
22052
+ });
22053
+ });
22054
+ var customers = new Command("customers").description("Create engineering accounts and manage existing account categories").addCommand(create6).addCommand(setCategory).addCommand(setManagedBy).addCommand(setWorkspaceType).addCommand(moveStores);
20835
22055
  var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops).addCommand(customers);
20836
22056
 
20837
22057
  // src/commands/membership.ts
20838
22058
  init_source();
20839
22059
  async function run2(c, p, label) {
20840
22060
  const r = await p;
20841
- if (!r.ok) fail(c, r.error ?? `Failed to ${label}`, classifyStatus(r.status), r.errorDetails);
22061
+ if (!r.ok)
22062
+ fail(
22063
+ c,
22064
+ r.error ?? `Failed to ${label}`,
22065
+ classifyStatus(r.status),
22066
+ r.errorDetails,
22067
+ apiFailureOf(r)
22068
+ );
20842
22069
  output(c, r.data ?? {}, () => process.stdout.write(`${JSON.stringify(r.data, null, 2)}
20843
22070
  `));
20844
22071
  }
@@ -20874,7 +22101,27 @@ orgCommand.command("update").option("--name <name>").option("--slug <slug>").opt
20874
22101
  );
20875
22102
  });
20876
22103
  orgCommand.command("members").action(async function() {
20877
- await run2(this, listOrgMembers(requireUserCredentials()), "list members");
22104
+ const r = await listOrgMembers(requireUserCredentials());
22105
+ if (!r.ok)
22106
+ fail(
22107
+ this,
22108
+ r.error ?? "Failed to list members",
22109
+ classifyStatus(r.status),
22110
+ r.errorDetails,
22111
+ apiFailureOf(r)
22112
+ );
22113
+ const rows = r.data?.data ?? [];
22114
+ output(this, r.data ?? {}, () => {
22115
+ printTable(
22116
+ ["ID", "MEMBER", "ROLE", "SUSPENDED"],
22117
+ rows.map((row) => [
22118
+ String(row.id),
22119
+ String(row.email ?? row.name ?? "-"),
22120
+ String(row.role),
22121
+ String(row.suspended_at ?? "-")
22122
+ ])
22123
+ );
22124
+ });
20878
22125
  });
20879
22126
  orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias for --org-role").option("--org-role <role>", "Organization role", "member").addOption(
20880
22127
  new Option(
@@ -20911,6 +22158,26 @@ orgCommand.command("set-role").argument("<memberId>").argument("<role>").option(
20911
22158
  "set role"
20912
22159
  );
20913
22160
  });
22161
+ orgCommand.command("suspend-member").argument("<memberId>").option("--reason <text>").option("-f, --force").action(async function(id, o) {
22162
+ if (!await confirm(
22163
+ this,
22164
+ o.force,
22165
+ `Suspend organization member ${id}`,
22166
+ `apex org suspend-member ${id} --force`
22167
+ ))
22168
+ return;
22169
+ await run2(this, suspendOrgMember(requireUserCredentials(), id, o.reason), "suspend member");
22170
+ });
22171
+ orgCommand.command("reinstate-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
22172
+ if (!await confirm(
22173
+ this,
22174
+ o.force,
22175
+ `Reinstate organization member ${id}`,
22176
+ `apex org reinstate-member ${id} --force`
22177
+ ))
22178
+ return;
22179
+ await run2(this, reinstateOrgMember(requireUserCredentials(), id), "reinstate member");
22180
+ });
20914
22181
  orgCommand.command("remove-member").argument("<memberId>").option("-f, --force").action(async function(id, o) {
20915
22182
  if (!await confirm(
20916
22183
  this,
@@ -21568,7 +22835,7 @@ var import_node_fs29 = __toESM(require("node:fs"));
21568
22835
  var import_node_path26 = __toESM(require("node:path"));
21569
22836
 
21570
22837
  // src/commands/landing-pages.ts
21571
- var import_node_crypto7 = __toESM(require("node:crypto"));
22838
+ var import_node_crypto8 = __toESM(require("node:crypto"));
21572
22839
  var import_node_fs28 = __toESM(require("node:fs"));
21573
22840
  var import_node_path25 = __toESM(require("node:path"));
21574
22841
  function apiFailure(command, response, fallback2) {
@@ -21582,7 +22849,7 @@ function apiFailure(command, response, fallback2) {
21582
22849
  }
21583
22850
  }
21584
22851
  function sha256(body5) {
21585
- return import_node_crypto7.default.createHash("sha256").update(body5).digest("hex");
22852
+ return import_node_crypto8.default.createHash("sha256").update(body5).digest("hex");
21586
22853
  }
21587
22854
  function markerPath(folder) {
21588
22855
  return import_node_path25.default.join(folder, LANDING_PAGE_CHECKOUT_MARKER);
@@ -21910,7 +23177,10 @@ async function pullCheckout(command, creds, id, folder, force = false) {
21910
23177
  notes: listing.data.notes ?? []
21911
23178
  };
21912
23179
  }
21913
- var create9 = new Command("create").description("Create a landing page to build outside Apex").requiredOption("--name <name>", "Page name").option("--slug <slug>", "URL slug (default: from the name)").option("--idea <key>", "Idea key or id the page is built for").option("--out <folder>", "Also pull the new page into this folder").action(async function(opts) {
23180
+ var create9 = new Command("create").description("Create a landing page to build outside Apex").requiredOption("--name <name>", "Page name").option(
23181
+ "--slug <slug>",
23182
+ "URL slug (default: from the name; -2, -3 added only if the shop already uses it)"
23183
+ ).option("--idea <key>", "Idea key or id the page is built for").option("--out <folder>", "Also pull the new page into this folder").action(async function(opts) {
21914
23184
  const creds = requireCredentials();
21915
23185
  const created = await landingPages(creds).create({
21916
23186
  name: opts.name,
@@ -22405,6 +23675,35 @@ program2.hook("preAction", (_thisCommand, actionCommand) => {
22405
23675
  process.on("beforeExit", () => {
22406
23676
  flushFallbackEnvelope();
22407
23677
  });
22408
- maybeCheckForUpdate().then(() => program2.parseAsync(process.argv)).then(async () => {
23678
+ function isExplicitJsonVersionRequest(args) {
23679
+ let hasJson = false;
23680
+ let hasVersion = false;
23681
+ for (let index = 0; index < args.length; index++) {
23682
+ const arg = args[index];
23683
+ if (arg === "--") break;
23684
+ if (arg === "--json") {
23685
+ hasJson = true;
23686
+ } else if (arg === "--version" || arg === "-V") {
23687
+ hasVersion = true;
23688
+ } else if (arg === "--org" || arg === "--shop") {
23689
+ const value = args[++index];
23690
+ if (!value || value.startsWith("-")) return false;
23691
+ } else if (arg.startsWith("--org=") || arg.startsWith("--shop=")) {
23692
+ const value = arg.slice(arg.indexOf("=") + 1);
23693
+ if (!value || value.startsWith("-")) return false;
23694
+ } else {
23695
+ return false;
23696
+ }
23697
+ }
23698
+ return hasJson && hasVersion;
23699
+ }
23700
+ var explicitJsonVersionRequest = isExplicitJsonVersionRequest(process.argv.slice(2));
23701
+ maybeCheckForUpdate().then(() => {
23702
+ if (explicitJsonVersionRequest) {
23703
+ writeEnvelope({ name: "@drip-apex/apex-cli", version: CLI_VERSION }, "version");
23704
+ return;
23705
+ }
23706
+ return program2.parseAsync(process.argv);
23707
+ }).then(async () => {
22409
23708
  await finishInvocation({ exitCode: process.exitCode ?? 0 });
22410
23709
  }).catch((err) => handleCliError(err, program2));