@drip-apex/apex-cli 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/cli.js +255 -90
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -184,6 +184,7 @@ including every subcommand, argument, option, and alias.
184
184
  - `apex keys` — manage shop API keys.
185
185
  - `apex org` — inspect and manage the organization.
186
186
  - `apex team` — manage shop-scoped members and invitations.
187
+ - `apex notifications` — choose Ready to launch email recipients and toggle ready messages on an existing Slack destination.
187
188
 
188
189
  ### Experiments and delivery
189
190
 
@@ -742,3 +743,23 @@ Unavailable values print `–` (empty in CSV); `*` marks a denominator below 50.
742
743
  `apex tracking manifest [manifest]` includes the shop's form definitions in a schemaVersion 1 manifest.
743
744
 
744
745
  Flag goals: `apex flags create --key banner --name "Banner" --goal <uuid> --goal <uuid> --primary-goal <uuid>`. The same options work on `flags update <id>`; `--goal` replaces the attached set. `flags get <id>` prints goals and marks the primary goal. JSON input can clear the set with `{"goal_ids":[]}`.
746
+
747
+ ### Ready to launch notifications
748
+
749
+ Use a user API key with `team:read` / `team:write` and the shop permission `members:manage`.
750
+ Every command accepts `--shop <id>` and `--json`; otherwise the selected shop is used.
751
+
752
+ ```sh
753
+ apex notifications ready get --shop <id> --json
754
+ apex notifications ready email set --recipients a@example.com,b@example.com --shop <id>
755
+ apex notifications ready email add --recipients c@example.com --shop <id>
756
+ apex notifications ready email remove --recipients a@example.com --shop <id>
757
+ apex notifications ready email off --shop <id>
758
+ apex notifications ready slack on --shop <id>
759
+ apex notifications ready slack off --shop <id>
760
+ ```
761
+
762
+ Recipients must already be current shop members. Email `off` clears the recipient list.
763
+ Slack `off` removes only `experiment.ready_to_launch` from the existing destination's events.
764
+ Connect or resume Slack in Settings > Integrations; selecting the ready event preserves its pause state.
765
+ There is no master switch. If settings change concurrently, retry the command to read the new version.
package/dist/cli.js CHANGED
@@ -3074,14 +3074,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
3074
3074
  */
3075
3075
  helpInformation(contextOptions) {
3076
3076
  const helper = this.createHelp();
3077
- const context = this._getOutputContext(contextOptions);
3077
+ const context2 = this._getOutputContext(contextOptions);
3078
3078
  helper.prepareContext({
3079
- error: context.error,
3080
- helpWidth: context.helpWidth,
3081
- outputHasColors: context.hasColors
3079
+ error: context2.error,
3080
+ helpWidth: context2.helpWidth,
3081
+ outputHasColors: context2.hasColors
3082
3082
  });
3083
3083
  const text2 = helper.formatHelp(this, helper);
3084
- if (context.hasColors) return text2;
3084
+ if (context2.hasColors) return text2;
3085
3085
  return this._outputConfiguration.stripColor(text2);
3086
3086
  }
3087
3087
  /**
@@ -3243,15 +3243,15 @@ Expecting one of '${allowedValues.join("', '")}'`);
3243
3243
  Expecting one of '${allowedValues.join("', '")}'`);
3244
3244
  }
3245
3245
  const helpEvent = `${position}Help`;
3246
- this.on(helpEvent, (context) => {
3246
+ this.on(helpEvent, (context2) => {
3247
3247
  let helpStr;
3248
3248
  if (typeof text2 === "function") {
3249
- helpStr = text2({ error: context.error, command: context.command });
3249
+ helpStr = text2({ error: context2.error, command: context2.command });
3250
3250
  } else {
3251
3251
  helpStr = text2;
3252
3252
  }
3253
3253
  if (helpStr) {
3254
- context.write(`${helpStr}
3254
+ context2.write(`${helpStr}
3255
3255
  `);
3256
3256
  }
3257
3257
  });
@@ -7210,7 +7210,7 @@ function readStoredCredentials(file = import_node_path.default.join(import_node_
7210
7210
  }
7211
7211
  }
7212
7212
  function resolveCredentials(env2 = process.env, stored = readStoredCredentials()) {
7213
- const context = {
7213
+ const context2 = {
7214
7214
  orgId: env2.DRIP_ORG || stored?.context?.orgId,
7215
7215
  shopId: env2.DRIP_SHOP || stored?.context?.shopId
7216
7216
  };
@@ -7223,7 +7223,7 @@ function resolveCredentials(env2 = process.env, stored = readStoredCredentials()
7223
7223
  apiKey: environmentApiKey,
7224
7224
  baseUrl: environmentBaseUrl ?? "https://app.drip-apex.com",
7225
7225
  credentialKind: user ? "user" : "shop",
7226
- resolvedContext: context
7226
+ resolvedContext: context2
7227
7227
  };
7228
7228
  }
7229
7229
  if (stored?.userApiKey) return {
@@ -7231,9 +7231,9 @@ function resolveCredentials(env2 = process.env, stored = readStoredCredentials()
7231
7231
  apiKey: stored.userApiKey,
7232
7232
  baseUrl: stored.userBaseUrl ?? stored.baseUrl,
7233
7233
  credentialKind: "user",
7234
- resolvedContext: context
7234
+ resolvedContext: context2
7235
7235
  };
7236
- if (stored?.apiKey) return { ...stored, credentialKind: "shop", resolvedContext: context };
7236
+ if (stored?.apiKey) return { ...stored, credentialKind: "shop", resolvedContext: context2 };
7237
7237
  return null;
7238
7238
  }
7239
7239
 
@@ -7423,7 +7423,7 @@ async function checkForUpdate(current) {
7423
7423
  // package.json
7424
7424
  var package_default = {
7425
7425
  name: "@drip-apex/apex-cli",
7426
- version: "0.20.0",
7426
+ version: "0.21.0",
7427
7427
  license: "MIT",
7428
7428
  description: "Apex by DRIP CLI: build, preview and run A/B tests on your online shop from the terminal or an AI coding agent (Claude Code, Codex, Cursor).",
7429
7429
  keywords: [
@@ -7546,8 +7546,8 @@ var envelopeContext = {
7546
7546
  shop_id: null,
7547
7547
  credential: initialEnvironmentApiKey?.startsWith("apx_uk_") ? "user" : "shop"
7548
7548
  };
7549
- function setEnvelopeContext(context) {
7550
- envelopeContext = { ...envelopeContext, ...context };
7549
+ function setEnvelopeContext(context2) {
7550
+ envelopeContext = { ...envelopeContext, ...context2 };
7551
7551
  }
7552
7552
  var jsonMode = false;
7553
7553
  var commandName = "apex";
@@ -7955,14 +7955,14 @@ function argvOption(name) {
7955
7955
  return void 0;
7956
7956
  }
7957
7957
  var didWarnStoredContext = false;
7958
- function warnStoredContextFallback(creds, flags, context) {
7958
+ function warnStoredContextFallback(creds, flags, context2) {
7959
7959
  if (didWarnStoredContext) return;
7960
7960
  const storedOrg = !flags.orgId && !process.env.DRIP_ORG && Boolean(creds?.context?.orgId);
7961
7961
  const storedShop = !flags.shopId && !process.env.DRIP_SHOP && Boolean(creds?.context?.shopId);
7962
7962
  if (!storedOrg && !storedShop) return;
7963
7963
  const selections = [
7964
- storedOrg ? `org ${context.orgId}` : null,
7965
- storedShop ? `shop ${context.shopId}` : null
7964
+ storedOrg ? `org ${context2.orgId}` : null,
7965
+ storedShop ? `shop ${context2.shopId}` : null
7966
7966
  ].filter(Boolean).join(" / ");
7967
7967
  console.error(`Using ${selections} from 'apex use' context (pass --org/--shop to override).`);
7968
7968
  didWarnStoredContext = true;
@@ -7996,21 +7996,21 @@ function resolveContext(creds = getCredentials(), flags = { orgId: argvOption("o
7996
7996
  shopId: flags.shopId || process.env.DRIP_SHOP || void 0
7997
7997
  };
7998
7998
  }
7999
- const context = {
7999
+ const context2 = {
8000
8000
  orgId: flags.orgId || process.env.DRIP_ORG || creds?.context?.orgId,
8001
8001
  shopId: flags.shopId || process.env.DRIP_SHOP || creds?.context?.shopId
8002
8002
  };
8003
- warnStoredContextFallback(creds, flags, context);
8004
- return context;
8003
+ warnStoredContextFallback(creds, flags, context2);
8004
+ return context2;
8005
8005
  }
8006
8006
  function configureInvocationContext() {
8007
8007
  const creds = getCredentials();
8008
8008
  const credential = defaultCredentialKind(creds);
8009
- const context = resolveContext(creds, void 0, credential);
8009
+ const context2 = resolveContext(creds, void 0, credential);
8010
8010
  setEnvelopeContext({
8011
8011
  credential,
8012
- org_id: context.orgId ?? null,
8013
- shop_id: context.shopId ?? null
8012
+ org_id: context2.orgId ?? null,
8013
+ shop_id: context2.shopId ?? null
8014
8014
  });
8015
8015
  }
8016
8016
  var DRIP_DIR = import_node_path3.default.join(import_node_os4.default.homedir(), ".drip");
@@ -8059,15 +8059,15 @@ function requireCredentials(fallbackShopId) {
8059
8059
  EXIT_CODES.auth
8060
8060
  );
8061
8061
  }
8062
- const context = resolveContext(creds, void 0, defaultCredentialKind(creds));
8063
- if (!context.shopId) context.shopId = fallbackShopId?.trim() || void 0;
8062
+ const context2 = resolveContext(creds, void 0, defaultCredentialKind(creds));
8063
+ if (!context2.shopId) context2.shopId = fallbackShopId?.trim() || void 0;
8064
8064
  if (defaultCredentialKind(creds) === "user") {
8065
8065
  setEnvelopeContext({
8066
8066
  credential: "user",
8067
- org_id: context.orgId ?? null,
8068
- shop_id: context.shopId ?? null
8067
+ org_id: context2.orgId ?? null,
8068
+ shop_id: context2.shopId ?? null
8069
8069
  });
8070
- if (!context.shopId && !getEnvironmentApiKey()) {
8070
+ if (!context2.shopId && !getEnvironmentApiKey()) {
8071
8071
  throw new CliError(
8072
8072
  'Shop context is required. Run "apex use", or pass --shop <id>.',
8073
8073
  EXIT_CODES.validation,
@@ -8083,22 +8083,22 @@ function requireCredentials(fallbackShopId) {
8083
8083
  userApiKey: environmentApiKey,
8084
8084
  userBaseUrl: creds.baseUrl,
8085
8085
  credentialKind: "user",
8086
- resolvedContext: context
8086
+ resolvedContext: context2
8087
8087
  }
8088
8088
  ) : {
8089
8089
  ...creds,
8090
8090
  apiKey: creds.userApiKey,
8091
8091
  baseUrl: creds.userBaseUrl ?? creds.baseUrl,
8092
8092
  credentialKind: "user",
8093
- resolvedContext: context
8093
+ resolvedContext: context2
8094
8094
  };
8095
8095
  }
8096
8096
  setEnvelopeContext({
8097
8097
  credential: "shop",
8098
- org_id: context.orgId ?? null,
8099
- shop_id: context.shopId ?? null
8098
+ org_id: context2.orgId ?? null,
8099
+ shop_id: context2.shopId ?? null
8100
8100
  });
8101
- return { ...creds, credentialKind: "shop", resolvedContext: context };
8101
+ return { ...creds, credentialKind: "shop", resolvedContext: context2 };
8102
8102
  }
8103
8103
  function resolveCheckShop(explicitShopId, fallbackShopId) {
8104
8104
  const explicit = explicitShopId?.trim() || null;
@@ -8157,11 +8157,11 @@ function requireUserCredentials() {
8157
8157
  );
8158
8158
  }
8159
8159
  const resolvedCredentials = creds;
8160
- const context = resolveContext(creds, void 0, "user");
8160
+ const context2 = resolveContext(creds, void 0, "user");
8161
8161
  setEnvelopeContext({
8162
8162
  credential: "user",
8163
- org_id: context.orgId ?? null,
8164
- shop_id: context.shopId ?? null
8163
+ org_id: context2.orgId ?? null,
8164
+ shop_id: context2.shopId ?? null
8165
8165
  });
8166
8166
  return environmentUserKey ? (
8167
8167
  // The user-plane request path reads userApiKey/userBaseUrl, so the
@@ -8172,14 +8172,14 @@ function requireUserCredentials() {
8172
8172
  userApiKey: environmentUserKey,
8173
8173
  userBaseUrl: resolvedCredentials.baseUrl,
8174
8174
  credentialKind: "user",
8175
- resolvedContext: context
8175
+ resolvedContext: context2
8176
8176
  }
8177
8177
  ) : {
8178
8178
  ...resolvedCredentials,
8179
8179
  apiKey: resolvedCredentials.userApiKey,
8180
8180
  baseUrl: resolvedCredentials.userBaseUrl ?? resolvedCredentials.baseUrl,
8181
8181
  credentialKind: "user",
8182
- resolvedContext: context
8182
+ resolvedContext: context2
8183
8183
  };
8184
8184
  }
8185
8185
  var CONFIG_NAMES = [".drip.config.ts", ".drip.config.js", "drip.config.ts", "drip.config.js"];
@@ -8257,7 +8257,7 @@ function detectSessionId(env2) {
8257
8257
  const hostId = env2.CLAUDE_CODE_SESSION_ID || env2.CODEX_THREAD_ID;
8258
8258
  return hostId ? `h_${(0, import_node_crypto2.createHash)("sha256").update(hostId).digest("hex").slice(0, 32)}` : void 0;
8259
8259
  }
8260
- function beginInvocation(command, context = {}) {
8260
+ function beginInvocation(command, context2 = {}) {
8261
8261
  resolvedPath = telemetryCommandPath(command);
8262
8262
  helpPath = void 0;
8263
8263
  attempted = void 0;
@@ -8280,8 +8280,8 @@ function beginInvocation(command, context = {}) {
8280
8280
  json_mode: shouldUseJson(command),
8281
8281
  tty: process.stdout.isTTY === true,
8282
8282
  credential_kind: process.env.APEX_API_KEY || process.env.DRIP_API_KEY ? "env" : defaultCredentialKind(getCredentials()),
8283
- org_id: context.orgId,
8284
- shop_id: context.shopId
8283
+ org_id: context2.orgId,
8284
+ shop_id: context2.shopId
8285
8285
  };
8286
8286
  }
8287
8287
  var staticRouteSegments = /* @__PURE__ */ new Set([
@@ -9555,9 +9555,9 @@ function listOrgMembers(creds) {
9555
9555
  "/api/v1/org/members"
9556
9556
  );
9557
9557
  }
9558
- function inviteOrg(creds, email, org_role, default_shop_role) {
9558
+ function inviteOrg(creds, email2, org_role, default_shop_role) {
9559
9559
  return request(userCreds(creds), "POST", "/api/v1/org/invitations", {
9560
- email,
9560
+ email: email2,
9561
9561
  org_role,
9562
9562
  default_shop_role
9563
9563
  });
@@ -9589,9 +9589,9 @@ function listTeamMembers(creds, shop) {
9589
9589
  `/api/v1/team/members?shop_id=${encodeURIComponent(shop)}`
9590
9590
  );
9591
9591
  }
9592
- function inviteTeam(creds, email, shop_id, role, permission_overrides) {
9592
+ function inviteTeam(creds, email2, shop_id, role, permission_overrides) {
9593
9593
  return request(userCreds(creds), "POST", "/api/v1/team/invitations", {
9594
- email,
9594
+ email: email2,
9595
9595
  shop_id,
9596
9596
  role,
9597
9597
  ...permission_overrides ? { permission_overrides } : {}
@@ -9882,6 +9882,21 @@ function putCommerceDefinition(creds, experiment2, definition2, expectedRevision
9882
9882
  expectedRevision
9883
9883
  });
9884
9884
  }
9885
+ function notificationCredentials(creds, shopId) {
9886
+ return { ...creds, resolvedContext: { ...creds.resolvedContext, shopId } };
9887
+ }
9888
+ function getReadyEmail(creds, shopId) {
9889
+ return userRequest(notificationCredentials(creds, shopId), "GET", `/api/v1/notifications/ready/email?shop_id=${encodeURIComponent(shopId)}`);
9890
+ }
9891
+ function setReadyEmail(creds, shopId, body6) {
9892
+ return userRequest(notificationCredentials(creds, shopId), "PUT", `/api/v1/notifications/ready/email?shop_id=${encodeURIComponent(shopId)}`, body6);
9893
+ }
9894
+ function getReadySlack(creds, shopId) {
9895
+ return userRequest(notificationCredentials(creds, shopId), "GET", `/api/v1/notifications/ready/slack?shop_id=${encodeURIComponent(shopId)}`);
9896
+ }
9897
+ function setReadySlack(creds, shopId, body6) {
9898
+ return userRequest(notificationCredentials(creds, shopId), "PUT", `/api/v1/notifications/ready/slack?shop_id=${encodeURIComponent(shopId)}`, body6);
9899
+ }
9885
9900
 
9886
9901
  // src/context.ts
9887
9902
  var import_node_readline = __toESM(require("node:readline"));
@@ -10322,21 +10337,21 @@ var whoamiCommand = new Command("whoami").description("Show your identity, crede
10322
10337
  return;
10323
10338
  }
10324
10339
  if (defaultCredentialKind(stored) === "shop") {
10325
- const context = resolveContext(stored, void 0, "shop");
10340
+ const context2 = resolveContext(stored, void 0, "shop");
10326
10341
  setEnvelopeContext({
10327
10342
  credential: "shop",
10328
- org_id: context.orgId ?? null,
10329
- shop_id: context.shopId ?? null
10343
+ org_id: context2.orgId ?? null,
10344
+ shop_id: context2.shopId ?? null
10330
10345
  });
10331
10346
  const data2 = {
10332
10347
  logged_in: true,
10333
10348
  credential: { kind: "shop" },
10334
- context: { org_id: context.orgId ?? null, shop_id: context.shopId ?? null }
10349
+ context: { org_id: context2.orgId ?? null, shop_id: context2.shopId ?? null }
10335
10350
  };
10336
10351
  output(this, data2, () => {
10337
10352
  console.log(
10338
10353
  `Credential: shop key (pre-bound server-side)
10339
- Declared shop: ${context.shopId ?? "none \u2014 set DRIP_SHOP to assert one"}
10354
+ Declared shop: ${context2.shopId ?? "none \u2014 set DRIP_SHOP to assert one"}
10340
10355
  For account-wide visibility run "apex login" and "apex whoami" without APEX_API_KEY (or DRIP_API_KEY).`
10341
10356
  );
10342
10357
  });
@@ -16181,8 +16196,8 @@ function printFilesystemPlan(plan2) {
16181
16196
  if ((plan2.blockingIssues?.length ?? 0) > 0) {
16182
16197
  console.log(source_default.red("\nBlocking issues"));
16183
16198
  (plan2.blockingIssues ?? []).forEach((issue) => {
16184
- const context = [issue.path, issue.variation ? `variation: ${issue.variation}` : null].filter(Boolean).join(", ");
16185
- console.log(` - [${issue.code}] ${issue.message}${context ? ` (${context})` : ""}`);
16199
+ const context2 = [issue.path, issue.variation ? `variation: ${issue.variation}` : null].filter(Boolean).join(", ");
16200
+ console.log(` - [${issue.code}] ${issue.message}${context2 ? ` (${context2})` : ""}`);
16186
16201
  });
16187
16202
  }
16188
16203
  if ((plan2.commerceLaunchPreview?.length ?? 0) > 0) {
@@ -17709,8 +17724,8 @@ var experimentStatus = new Command("status").description(
17709
17724
  "Versions are unavailable without a pulled folder. Pull the experiment to compare local files."
17710
17725
  );
17711
17726
  if (creds.credentialKind === "user") {
17712
- const context = await getAccountContext(creds);
17713
- const owner = context.data?.data?.orgs?.find(
17727
+ const context2 = await getAccountContext(creds);
17728
+ const owner = context2.data?.data?.orgs?.find(
17714
17729
  (item) => item.shops?.some((item2) => item2.id === shopId)
17715
17730
  );
17716
17731
  if (owner) {
@@ -19096,6 +19111,10 @@ var import_node_path10 = __toESM(require("node:path"));
19096
19111
  var import_node_url2 = require("node:url");
19097
19112
  function toApiBody(exp) {
19098
19113
  return {
19114
+ ...exp.commerce !== void 0 ? {
19115
+ expectedCommerceRevision: exp.commerce.revision,
19116
+ ...exp.commerce.definition !== void 0 ? { definition: exp.commerce.definition } : {}
19117
+ } : {},
19099
19118
  name: exp.name,
19100
19119
  description: exp.description ?? null,
19101
19120
  traffic_allocation: exp.trafficAllocation === void 0 ? 1 : exp.trafficAllocation,
@@ -19106,7 +19125,8 @@ function toApiBody(exp) {
19106
19125
  name: v.name ?? v.id ?? "Unnamed",
19107
19126
  weight: v.weight,
19108
19127
  is_control: v.isControl ?? v.is_control ?? false,
19109
- mutations: v.mutations ?? [],
19128
+ ...v.mutations !== void 0 ? { mutations: v.mutations } : {},
19129
+ ...(v.artifactHash ?? v.artifact_hash) !== void 0 ? { expectedArtifactHash: v.artifactHash ?? v.artifact_hash } : {},
19110
19130
  custom_css: v.css ?? v.custom_css ?? null,
19111
19131
  custom_js: v.js ?? v.custom_js ?? null,
19112
19132
  reset_js: v.resetJs ?? v.reset_js ?? null,
@@ -19121,7 +19141,7 @@ async function rewriteExperimentAssets(exp, rewrite, baseDir) {
19121
19141
  variations: await Promise.all(
19122
19142
  exp.variations.map(async (variation) => ({
19123
19143
  ...variation,
19124
- mutations: await rewrite.rewriteUnknown(variation.mutations ?? [], baseDir),
19144
+ ...variation.mutations !== void 0 ? { mutations: await rewrite.rewriteUnknown(variation.mutations, baseDir) } : {},
19125
19145
  css: variation.css === void 0 ? void 0 : await rewrite.rewrite(variation.css, baseDir),
19126
19146
  custom_css: variation.custom_css === void 0 ? void 0 : await rewrite.rewrite(variation.custom_css, baseDir),
19127
19147
  js: variation.js === void 0 ? void 0 : await rewrite.rewrite(variation.js, baseDir),
@@ -19310,7 +19330,9 @@ var pushCommand = new Command("push").description("Push experiment definitions f
19310
19330
  slug: exp.slug,
19311
19331
  status: "failed",
19312
19332
  error: res.error,
19313
- statusCode: res.status
19333
+ statusCode: res.status,
19334
+ code: typeof res.errorDetails?.code === "string" ? res.errorDetails.code : void 0,
19335
+ details: res.errorDetails
19314
19336
  });
19315
19337
  continue;
19316
19338
  }
@@ -19813,6 +19835,8 @@ function sessionFromWorkspace(input) {
19813
19835
  variationId: input.variationId,
19814
19836
  workspaceId: input.workspace.workspace.id,
19815
19837
  workspaceRevisionId: input.workspace.workspace.currentRevisionId,
19838
+ commerceRevision: input.workspace.commerceRevision,
19839
+ artifactHash: input.workspace.artifactHash,
19816
19840
  checkedOutAt: (/* @__PURE__ */ new Date()).toISOString(),
19817
19841
  files: Object.fromEntries(
19818
19842
  input.workspace.files.map((file) => [
@@ -19902,6 +19926,7 @@ async function syncChangedFiles(folder, session) {
19902
19926
  file.path,
19903
19927
  {
19904
19928
  content: file.content,
19929
+ expected_version: session.files[file.path]?.sha256,
19905
19930
  source: "operator",
19906
19931
  title: `CLI sync ${file.path}`
19907
19932
  }
@@ -19912,6 +19937,13 @@ async function syncChangedFiles(folder, session) {
19912
19937
  classifyStatus(res.status),
19913
19938
  res.errorDetails
19914
19939
  );
19940
+ const saved = res.data;
19941
+ if (saved?.revision?.id) session.workspaceRevisionId = saved.revision.id;
19942
+ session.files[file.path] = {
19943
+ sha256: saved?.file?.sha256 ?? file.sha256,
19944
+ sizeBytes: file.sizeBytes
19945
+ };
19946
+ writeSession(folder, session);
19915
19947
  }
19916
19948
  return { localFiles, changed };
19917
19949
  }
@@ -19920,12 +19952,15 @@ async function runPublish(folderArg, opts = {}) {
19920
19952
  const session = readSession(folder);
19921
19953
  const { localFiles, changed } = await syncChangedFiles(folder, session);
19922
19954
  const creds = requireCredentials();
19923
- const build = await buildOperatorWorkspace(
19924
- creds,
19925
- session.experimentId,
19926
- session.variationId,
19927
- opts.allowGoalWrites ? { allowGoalWrites: true } : void 0
19928
- );
19955
+ const build = await buildOperatorWorkspace(creds, session.experimentId, session.variationId, {
19956
+ ...opts.allowGoalWrites ? { allowGoalWrites: true } : {},
19957
+ expectedCommerceRevision: session.commerceRevision,
19958
+ expectedArtifactHash: session.artifactHash,
19959
+ expectedWorkspaceRevision: session.workspaceRevisionId ?? void 0,
19960
+ expectedFileVersions: Object.fromEntries(
19961
+ Object.entries(session.files).filter(([path22]) => path22 !== "brand.md").map(([path22, file]) => [path22, file.sha256])
19962
+ )
19963
+ });
19929
19964
  if (!build.ok || !build.data?.ok) {
19930
19965
  const issueDetails = formatWorkspaceBlockingIssues(build.data);
19931
19966
  throw new CliError(
@@ -21862,13 +21897,13 @@ async function identifyRuntimeSettingsShop(command, requested) {
21862
21897
  if (credentials.credentialKind === "user") {
21863
21898
  let shopId = requested;
21864
21899
  if (!uuidPattern.test(requested)) {
21865
- const context = await getAccountContext({
21900
+ const context2 = await getAccountContext({
21866
21901
  ...credentials,
21867
21902
  resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
21868
21903
  });
21869
- if (!context.ok || !context.data)
21870
- fail(command, context.error ?? "Shop discovery failed", classifyStatus(context.status));
21871
- const match2 = context.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
21904
+ if (!context2.ok || !context2.data)
21905
+ fail(command, context2.error ?? "Shop discovery failed", classifyStatus(context2.status));
21906
+ const match2 = context2.data.data.orgs.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requested.toLowerCase());
21872
21907
  if (!match2)
21873
21908
  fail(command, "--shop domain is not available to this user", EXIT_CODES.validation);
21874
21909
  shopId = match2.id;
@@ -22484,17 +22519,17 @@ async function identify(command, requestedShop) {
22484
22519
  ...credentials,
22485
22520
  resolvedContext: { ...credentials.resolvedContext, shopId: void 0 }
22486
22521
  };
22487
- const context = await getAccountContext(discoveryCredentials);
22488
- if (!context.ok) {
22522
+ const context2 = await getAccountContext(discoveryCredentials);
22523
+ if (!context2.ok) {
22489
22524
  fail(
22490
22525
  command,
22491
- `${context.status === 401 || context.status === 403 ? "TRIGGERS_AUTH" : "TRIGGERS_CONFIG_ABSENT"}: ${context.error ?? "account context unavailable"}`,
22492
- classifyStatus(context.status),
22493
- context.errorDetails,
22494
- apiFailureOf(context)
22526
+ `${context2.status === 401 || context2.status === 403 ? "TRIGGERS_AUTH" : "TRIGGERS_CONFIG_ABSENT"}: ${context2.error ?? "account context unavailable"}`,
22527
+ classifyStatus(context2.status),
22528
+ context2.errorDetails,
22529
+ apiFailureOf(context2)
22495
22530
  );
22496
22531
  }
22497
- const match2 = context.data?.data?.orgs?.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requestedShop.toLowerCase());
22532
+ const match2 = context2.data?.data?.orgs?.flatMap((org) => org.shops).find((shop) => shop.domain.toLowerCase() === requestedShop.toLowerCase());
22498
22533
  if (!match2) {
22499
22534
  fail(
22500
22535
  command,
@@ -22829,9 +22864,9 @@ async function findLandingDomain(command, host) {
22829
22864
  return row;
22830
22865
  }
22831
22866
  async function verifyOnce(command, id) {
22832
- const checked = await verifyLandingPageDomain(requireCredentials(), id);
22833
- if (!checked.ok || !checked.data) domainFail(command, checked, "Failed to verify the domain");
22834
- return checked.data.data;
22867
+ const checked2 = await verifyLandingPageDomain(requireCredentials(), id);
22868
+ if (!checked2.ok || !checked2.data) domainFail(command, checked2, "Failed to verify the domain");
22869
+ return checked2.data.data;
22835
22870
  }
22836
22871
  var claim = new Command("claim").description("Claim a client hostname for landing pages (CNAME to the Apex edge)").argument("<host>", "hostname the client controls, for example offers.example.com").option("--wait", "check DNS and the certificate every 10 seconds for up to two minutes").action(async function action56(host, opts) {
22837
22872
  const result = await createLandingPageDomain(requireCredentials(), {
@@ -29089,13 +29124,28 @@ var settingsGet = new Command("settings").description("Show platform operator se
29089
29124
  res.data,
29090
29125
  () => printTable(
29091
29126
  ["SETTING", "VALUE"],
29092
- Object.entries(res.data.data).map(([k, v]) => [k, v ? "on" : "off"])
29127
+ Object.entries(res.data.data).map(([k, v]) => [
29128
+ k,
29129
+ typeof v === "boolean" ? v ? "on" : "off" : String(v)
29130
+ ])
29093
29131
  )
29094
29132
  );
29095
29133
  });
29096
- var set5 = new Command("set").description("Set platform kill switches").option("--guardian <on|off>").option("--srm-check <on|off>").option("--confirm", "Confirm immediate production impact").action(async function(opts) {
29097
- if (!opts.guardian && !opts.srmCheck) fail(this, "Provide --guardian or --srm-check", 3);
29098
- let body6 = {};
29134
+ var set5 = new Command("set").description("Set platform kill switches").option("--guardian <on|off>").option("--srm-check <on|off>").option("--undo-completion-window-days <days>", "Integer 0..365; 0 disables undo").option("--expected-version <version>", "Policy version returned by admin settings").option("--reason <text>", "Audit reason for a policy change").option("--confirm", "Confirm immediate production impact").action(async function(opts) {
29135
+ if (!opts.guardian && !opts.srmCheck && opts.undoCompletionWindowDays === void 0)
29136
+ fail(this, "Provide --guardian, --srm-check or --undo-completion-window-days", 3);
29137
+ const body6 = {};
29138
+ if (opts.undoCompletionWindowDays !== void 0) {
29139
+ const days = Number(opts.undoCompletionWindowDays);
29140
+ if (!/^(0|[1-9]\d*)$/.test(opts.undoCompletionWindowDays) || days > 365)
29141
+ fail(this, "--undo-completion-window-days must be an integer from 0 to 365", 3);
29142
+ if (!opts.expectedVersion || !opts.reason?.trim() || opts.reason.trim().length > 500)
29143
+ fail(this, "Policy changes require --expected-version and --reason (1..500 characters)", 3);
29144
+ body6.experiment_undo_completion_window_days = days;
29145
+ body6.expected_version = opts.expectedVersion;
29146
+ body6.reason = opts.reason.trim();
29147
+ } else if (opts.expectedVersion || opts.reason)
29148
+ fail(this, "--expected-version and --reason require --undo-completion-window-days", 3);
29099
29149
  try {
29100
29150
  if (opts.guardian) body6.guardian_enabled = switchValue(opts.guardian);
29101
29151
  if (opts.srmCheck) body6.srm_check_enabled = switchValue(opts.srmCheck);
@@ -29105,7 +29155,10 @@ var set5 = new Command("set").description("Set platform kill switches").option("
29105
29155
  if (!opts.confirm) {
29106
29156
  const switches = [
29107
29157
  opts.guardian ? `--guardian ${opts.guardian}` : "",
29108
- opts.srmCheck ? `--srm-check ${opts.srmCheck}` : ""
29158
+ opts.srmCheck ? `--srm-check ${opts.srmCheck}` : "",
29159
+ opts.undoCompletionWindowDays !== void 0 ? `--undo-completion-window-days ${opts.undoCompletionWindowDays}` : "",
29160
+ opts.expectedVersion ? `--expected-version '${opts.expectedVersion.replaceAll("'", `'"'"'`)}'` : "",
29161
+ opts.reason ? `--reason '${opts.reason.replaceAll("'", `'"'"'`)}'` : ""
29109
29162
  ].filter(Boolean).join(" ");
29110
29163
  confirmationRequired(
29111
29164
  this,
@@ -29619,11 +29672,11 @@ function adminClientLinkRequest(opts) {
29619
29672
  throw new Error("--consultant must be a UUID");
29620
29673
  const teammates = (opts.teammate ?? []).map((value2) => {
29621
29674
  const split = value2.lastIndexOf(":");
29622
- const email = value2.slice(0, split).trim().toLowerCase();
29675
+ const email2 = value2.slice(0, split).trim().toLowerCase();
29623
29676
  const role = value2.slice(split + 1);
29624
- if (split < 1 || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || !["editor", "client_viewer"].includes(role))
29677
+ if (split < 1 || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email2) || !["editor", "client_viewer"].includes(role))
29625
29678
  throw new Error("--teammate must be email:editor or email:client_viewer");
29626
- return { email, role };
29679
+ return { email: email2, role };
29627
29680
  });
29628
29681
  const discountMonths = opts.discountMonths === "forever" ? null : integerFlag("--discount-months", opts.discountMonths, 1);
29629
29682
  return {
@@ -29677,6 +29730,117 @@ var createClient = new Command("create").description("Create a personal managed-
29677
29730
  var clients = new Command("clients").description("Create managed-client links").addCommand(createClient);
29678
29731
  var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops).addCommand(clients).addCommand(customers);
29679
29732
 
29733
+ // src/commands/notifications.ts
29734
+ var notificationsCommand = new Command("notifications").description(
29735
+ "Manage notification settings"
29736
+ );
29737
+ var ready = notificationsCommand.command("ready").description("Ready to launch email recipients and Slack event toggle");
29738
+ function context(command, explicit) {
29739
+ const creds = requireUserCredentials();
29740
+ const shopId = explicit ?? creds.resolvedContext?.shopId;
29741
+ if (!shopId) fail(command, "Choose a shop with --shop <id> or apex use <org>/<shop>.", 3);
29742
+ return { creds, shopId };
29743
+ }
29744
+ function checked(command, response) {
29745
+ if (!response.ok)
29746
+ fail(
29747
+ command,
29748
+ response.status === 409 ? "Notification settings changed. Retry the command." : response.error ?? "Could not read or save notification settings.",
29749
+ classifyStatus(response.status),
29750
+ response.errorDetails,
29751
+ apiFailureOf(response)
29752
+ );
29753
+ if (!response.data) fail(command, "The API returned no notification settings.", 1);
29754
+ return response.data;
29755
+ }
29756
+ function print(command, data, lines) {
29757
+ output(command, data, () => {
29758
+ for (const line of lines) console.log(line);
29759
+ });
29760
+ }
29761
+ function emailLine(members, ids) {
29762
+ if (ids.length === 0) return "Email: off (no recipients)";
29763
+ const names = ids.map((id) => members.find((member2) => member2.user_id === id)?.email ?? id);
29764
+ return `Email: ${names.length} recipient${names.length === 1 ? "" : "s"}: ${names.join(", ")}`;
29765
+ }
29766
+ function slackLine(settings2) {
29767
+ const destination = settings2.destination;
29768
+ if (!destination) return "Slack: not connected";
29769
+ const paused = !destination.active || destination.disabled_at ? " (delivery paused)" : "";
29770
+ return `Slack: ${destination.ready_to_launch ? "on" : "off"}${paused}`;
29771
+ }
29772
+ ready.command("get").option("--shop <id>").action(async function(options) {
29773
+ const { creds, shopId } = context(this, options.shop);
29774
+ const email2 = checked(this, await getReadyEmail(creds, shopId)).data;
29775
+ assertResponseShop(email2.shop_id, shopId, creds.credentialKind);
29776
+ const slack2 = checked(this, await getReadySlack(creds, shopId)).data;
29777
+ assertResponseShop(slack2.shop_id, shopId, creds.credentialKind);
29778
+ print(this, { email: email2, slack: slack2 }, [
29779
+ emailLine(
29780
+ email2.members,
29781
+ email2.members.filter((member2) => member2.selected).map((member2) => member2.user_id)
29782
+ ),
29783
+ slackLine(slack2)
29784
+ ]);
29785
+ });
29786
+ var email = ready.command("email").description("Choose current shop members to receive ready emails; off clears recipients");
29787
+ for (const operation of ["set", "add", "remove", "off"]) {
29788
+ const command = email.command(operation).option("--shop <id>");
29789
+ if (operation !== "off")
29790
+ command.requiredOption("--recipients <emails>", "Comma-separated shop member emails");
29791
+ command.action(async function(options) {
29792
+ const { creds, shopId } = context(this, options.shop);
29793
+ const current = checked(this, await getReadyEmail(creds, shopId)).data;
29794
+ assertResponseShop(current.shop_id, shopId, creds.credentialKind);
29795
+ const ids = [];
29796
+ if (operation !== "off") {
29797
+ const emails = options.recipients.split(",").map((value2) => value2.trim().toLowerCase());
29798
+ if (emails.some((value2) => !value2))
29799
+ fail(this, "Provide a comma-separated list of member emails without empty entries.", 3);
29800
+ for (const address of new Set(emails)) {
29801
+ const matches = current.members.filter((member2) => member2.email?.toLowerCase() === address);
29802
+ if (matches.length !== 1)
29803
+ fail(this, `Unknown or ambiguous shop member email: ${address}`, 3);
29804
+ ids.push(matches[0].user_id);
29805
+ }
29806
+ }
29807
+ const selected2 = current.members.filter((member2) => member2.selected).map((member2) => member2.user_id);
29808
+ const recipient_user_ids = operation === "off" ? [] : operation === "set" ? ids : operation === "add" ? [.../* @__PURE__ */ new Set([...selected2, ...ids])] : selected2.filter((id) => !ids.includes(id));
29809
+ const saved = checked(
29810
+ this,
29811
+ await setReadyEmail(creds, shopId, { version: current.version, recipient_user_ids })
29812
+ );
29813
+ assertResponseShop(saved.data.shop_id, shopId, creds.credentialKind);
29814
+ print(this, saved, [emailLine(current.members, saved.data.recipient_user_ids)]);
29815
+ });
29816
+ }
29817
+ var slack = ready.command("slack").description(
29818
+ "Toggle the ready event on an existing Slack destination; other events and pause state stay as configured"
29819
+ );
29820
+ for (const enabled of [true, false]) {
29821
+ slack.command(enabled ? "on" : "off").option("--shop <id>").action(async function(options) {
29822
+ const { creds, shopId } = context(this, options.shop);
29823
+ const current = checked(this, await getReadySlack(creds, shopId)).data;
29824
+ assertResponseShop(current.shop_id, shopId, creds.credentialKind);
29825
+ if (!current.destination || !current.version)
29826
+ fail(this, "Connect a Slack destination in Settings > Integrations first.", 3);
29827
+ const result = checked(
29828
+ this,
29829
+ await setReadySlack(creds, shopId, {
29830
+ version: current.version,
29831
+ ready_to_launch: enabled
29832
+ })
29833
+ );
29834
+ assertResponseShop(result.data.shop_id, shopId, creds.credentialKind);
29835
+ if (enabled && result.data.destination && (!result.data.destination.active || result.data.destination.disabled_at)) {
29836
+ process.stderr.write(
29837
+ "Ready to launch is selected, but Slack delivery is paused. Resume the destination in Settings > Integrations.\n"
29838
+ );
29839
+ }
29840
+ print(this, result, [slackLine(result.data)]);
29841
+ });
29842
+ }
29843
+
29680
29844
  // src/commands/membership.ts
29681
29845
  init_source();
29682
29846
  async function run2(c, p, label) {
@@ -29751,7 +29915,7 @@ orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias
29751
29915
  "--default-shop-role <role>",
29752
29916
  "Default shop role for every brand in the workspace (workspace defaults are internal roles)"
29753
29917
  ).choices(["editor"]).default("editor")
29754
- ).action(async function(email, o) {
29918
+ ).action(async function(email2, o) {
29755
29919
  if (o.role !== void 0) {
29756
29920
  process.stderr.write(
29757
29921
  "Warning: --role is deprecated for apex org invite; use --org-role instead. --role will be removed in v0.7.0.\n"
@@ -29764,7 +29928,7 @@ orgCommand.command("invite").argument("<email>").option("--role <role>", "Alias
29764
29928
  }
29765
29929
  await run2(
29766
29930
  this,
29767
- inviteOrg(requireUserCredentials(), email, o.role ?? o.orgRole, o.defaultShopRole),
29931
+ inviteOrg(requireUserCredentials(), email2, o.role ?? o.orgRole, o.defaultShopRole),
29768
29932
  "invite member"
29769
29933
  );
29770
29934
  });
@@ -29836,13 +30000,13 @@ teamCommand.command("invite").argument("<email>").option("--shop <id>", teamShop
29836
30000
  ).option(
29837
30001
  "--design-approver",
29838
30002
  "Also let them sign off designs once they accept (a client_viewer with this is a Client approver). Needs store owner or admin rights."
29839
- ).action(async function(email, o) {
30003
+ ).action(async function(email2, o) {
29840
30004
  o.shop = teamShop(this, o.shop);
29841
30005
  await run2(
29842
30006
  this,
29843
30007
  inviteTeam(
29844
30008
  requireUserCredentials(),
29845
- email,
30009
+ email2,
29846
30010
  o.shop,
29847
30011
  o.role,
29848
30012
  o.designApprover ? { "approvals:design": true } : void 0
@@ -31532,6 +31696,7 @@ program2.addCommand(skillsCommand);
31532
31696
  program2.addCommand(adminCommand, { hidden: true });
31533
31697
  program2.addCommand(orgCommand);
31534
31698
  program2.addCommand(teamCommand);
31699
+ program2.addCommand(notificationsCommand);
31535
31700
  program2.addCommand(upgradeCommand);
31536
31701
  addJsonOption(program2);
31537
31702
  registerTelemetryProgram(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drip-apex/apex-cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "license": "MIT",
5
5
  "description": "Apex by DRIP CLI: build, preview and run A/B tests on your online shop from the terminal or an AI coding agent (Claude Code, Codex, Cursor).",
6
6
  "keywords": [