@drip-apex/apex-cli 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/dist/cli.js +484 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,6 +227,8 @@ command inventory:
|
|
|
227
227
|
|
|
228
228
|
| Command | Purpose |
|
|
229
229
|
| --- | --- |
|
|
230
|
+
| `apex admin customers set-category <org-id>` | Change an existing account to internal, demo, dogfood or customer with a platform user key. Requires an 8 to 500 character `--reason` and `--confirm`. |
|
|
231
|
+
| `apex admin customers create` | Create internal, demo or dogfood accounts with a platform user key. Requires `--name`, `--category`, an 8 to 500 character `--reason`, and `--confirm`. |
|
|
230
232
|
| `apex admin shops archive <shopId>` | Archive a shop and disable Apex delivery; use `--force` in non-interactive environments. |
|
|
231
233
|
| `apex admin shops restore <shopId>` | Restore an archived shop while leaving its runtime kill switch unchanged. |
|
|
232
234
|
|
|
@@ -240,18 +242,19 @@ apex landing-pages create --name "Fall sale" --out ./fall-sale # or: pull <id>
|
|
|
240
242
|
# edit ./fall-sale/index.html and its assets; context/ holds brand, catalog, offers and reviews
|
|
241
243
|
apex landing-pages plan ./fall-sale # what would change
|
|
242
244
|
apex landing-pages apply ./fall-sale --yes # new version + signed preview link (24 hours)
|
|
243
|
-
apex landing-pages deploy ./fall-sale # live on the
|
|
245
|
+
apex landing-pages deploy ./fall-sale # live on <shop>.apex-page.com, or the host in apex-landing-page.json
|
|
244
246
|
apex landing-pages rollback ./fall-sale --version 1
|
|
245
247
|
apex landing-pages experiment ./fall-sale --control 1 --variant 2 # draft split; start it in the dashboard
|
|
246
248
|
```
|
|
247
249
|
|
|
248
250
|
The folder holds the page files (index.html at the root, at most 300 files
|
|
249
|
-
and
|
|
251
|
+
and 100 MB; html, css, js, json, svg, images, fonts, txt, xml, webmanifest, mp4 and webm),
|
|
250
252
|
`apex-landing-page.json` and the `context/` folder (each pull replaces its JSON
|
|
251
253
|
files with the server's current set). Build with relative asset
|
|
252
254
|
URLs (Vite `base: "./"`). `apex-landing-page.json` sets the name, slug, an
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
optional `host`, a domain claimed with `apex domains claim <host>` (without
|
|
256
|
+
one, or `--host`, a deploy goes to the shop's own instant host
|
|
257
|
+
`<shop>.apex-page.com`), the `path` (default `/<slug>`), whether Apex injects the shop snippet
|
|
255
258
|
(`snippet`, on by default), the `shop_hosts` that receive the visitor's identity
|
|
256
259
|
on outbound links (default: the shop's install domains), the `goals`
|
|
257
260
|
(key, name, type click, pageview or custom) and an optional `experiment`
|
|
@@ -522,3 +525,31 @@ are sent. Login/logout, tests, the mock transport, and runs with no resolvable c
|
|
|
522
525
|
(`APEX_API_KEY` or `DRIP_API_KEY`) are recorded with credential kind `env` unless
|
|
523
526
|
`APEX_TELEMETRY=0`. Set `APEX_TELEMETRY=0` to disable the beacon. The best-effort request has an
|
|
524
527
|
800 ms timeout and never changes command output or exit status.
|
|
528
|
+
|
|
529
|
+
MP4 (`video/mp4`) and WebM (`video/webm`) videos are hosted directly by Apex.
|
|
530
|
+
Each version holds at most 300 files and 100 MB; each video is limited to 50 MB,
|
|
531
|
+
and other files to 3 MB. `apply` automatically obtains a 15-minute upload grant
|
|
532
|
+
for videos above 3 MB and sends resumable 8 MB parts straight to the Worker,
|
|
533
|
+
retrying failed parts with progress output. The Worker verifies the declared
|
|
534
|
+
size and SHA-256 before apply records the file. Pull and signed previews also
|
|
535
|
+
fetch large videos directly from the Worker. Video supports byte ranges,
|
|
536
|
+
including Safari's `bytes=0-1` probe; rollback preserves the older bytes.
|
|
537
|
+
|
|
538
|
+
Engineering account creation uses `apex login --user` credentials with `admin:write`
|
|
539
|
+
and a live platform role with onboarding write permission. Real customer accounts
|
|
540
|
+
are created in **Admin, Links**.
|
|
541
|
+
|
|
542
|
+
```sh
|
|
543
|
+
apex admin customers create --name "Demo workspace" --category demo --reason "Workshop demo account" --confirm --json
|
|
544
|
+
apex admin customers create --name "Dogfood store" --category dogfood --domain dogfood.example.com --platform woo --owner owner@example.com --reason "Engineering dogfood account" --confirm
|
|
545
|
+
apex admin customers set-category 22222222-2222-4222-8222-222222222222 --category internal --reason "Engineering dev store" --confirm --json
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`--type` defaults to `company` (`company|agency`); `--plan` defaults to
|
|
549
|
+
`self_managed` (`self_managed|apex_managed|drip_managed`). Managed plans require
|
|
550
|
+
`--consultant <platform_access UUID>` and `--consultant-title <text>`.
|
|
551
|
+
An optional first store uses `--domain <host> --platform <shopify|shopware|woo|other>`
|
|
552
|
+
or `--claim-shop <UUID>`, exclusively. `woo` maps to `woocommerce` and `other` to
|
|
553
|
+
`custom`. `--owner <email>` sends an owner invitation and requires a store;
|
|
554
|
+
accounts without a store have no invitations. Output includes the organization ID,
|
|
555
|
+
store ID (or null), invitation results and partial delivery status.
|
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.
|
|
7328
|
+
version: "0.15.2",
|
|
7329
7329
|
license: "MIT",
|
|
7330
7330
|
description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
|
|
7331
7331
|
private: false,
|
|
@@ -8236,6 +8236,7 @@ function landingPagesApi(request2) {
|
|
|
8236
8236
|
),
|
|
8237
8237
|
plan: (id, body5) => call("POST", pagePath(id, "filesystem/plan"), body5),
|
|
8238
8238
|
upload: (id, files) => call("POST", pagePath(id, "filesystem/upload"), { files }),
|
|
8239
|
+
uploadGrant: (id, file) => call("POST", pagePath(id, "filesystem/upload-grant"), file),
|
|
8239
8240
|
apply: (id, body5) => call("POST", pagePath(id, "filesystem/apply"), body5),
|
|
8240
8241
|
deploy: (id, body5) => call("POST", pagePath(id, "deploy"), body5),
|
|
8241
8242
|
versions: (id) => call("GET", pagePath(id, "versions")),
|
|
@@ -8261,6 +8262,53 @@ function landingPageBatches(items, size) {
|
|
|
8261
8262
|
if (current.length) batches.push(current);
|
|
8262
8263
|
return batches;
|
|
8263
8264
|
}
|
|
8265
|
+
async function uploadLandingVideo(grant, body5, options = {}) {
|
|
8266
|
+
if (!Number.isSafeInteger(grant.part_bytes) || grant.part_bytes !== 8 * 1024 * 1024) throw new Error("Invalid video part size");
|
|
8267
|
+
const fetcher = options.fetch ?? fetch;
|
|
8268
|
+
const send3 = async (action78, method = "POST", part, bytes) => {
|
|
8269
|
+
const url = new URL(grant.upload_url);
|
|
8270
|
+
url.searchParams.set("action", action78);
|
|
8271
|
+
if (part !== void 0) url.searchParams.set("part", String(part));
|
|
8272
|
+
let error;
|
|
8273
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
8274
|
+
try {
|
|
8275
|
+
const response = await fetcher(url, { method, ...bytes ? { body: bytes } : {}, signal: AbortSignal.timeout(6e4) });
|
|
8276
|
+
if (!response.ok) {
|
|
8277
|
+
const message = await response.text();
|
|
8278
|
+
if (response.status < 500 && response.status !== 429) throw new Error(`Video upload refused (${response.status}): ${message}`);
|
|
8279
|
+
error = new Error(`Video upload failed (${response.status}): ${message}`);
|
|
8280
|
+
} else return await response.json();
|
|
8281
|
+
} catch (caught) {
|
|
8282
|
+
if (caught instanceof Error && caught.message.startsWith("Video upload refused")) throw caught;
|
|
8283
|
+
error = caught;
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
throw error;
|
|
8287
|
+
};
|
|
8288
|
+
const started2 = await send3("start");
|
|
8289
|
+
if (!started2.complete) {
|
|
8290
|
+
for (let offset = 0, part = 1; offset < body5.byteLength; offset += grant.part_bytes, part++) {
|
|
8291
|
+
const bytes = body5.subarray(offset, Math.min(body5.byteLength, offset + grant.part_bytes));
|
|
8292
|
+
await send3("part", "PUT", part, bytes);
|
|
8293
|
+
options.progress?.(offset + bytes.byteLength, body5.byteLength);
|
|
8294
|
+
}
|
|
8295
|
+
const result = await send3("complete");
|
|
8296
|
+
if (!result.complete) throw new Error("Worker did not confirm the video upload");
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
function validateLandingPageLocalFiles(files) {
|
|
8300
|
+
const allowed2 = new Set("html css js mjs json svg png jpg jpeg webp avif gif ico woff woff2 txt xml webmanifest mp4 webm".split(" "));
|
|
8301
|
+
if (files.length > 300) throw new Error("A bundle is limited to 300 files");
|
|
8302
|
+
let total = 0;
|
|
8303
|
+
for (const file of files) {
|
|
8304
|
+
const ext = file.path.split(".").at(-1)?.toLowerCase() ?? "";
|
|
8305
|
+
if (!allowed2.has(ext)) throw new Error(`File type is not allowed: ${file.path}`);
|
|
8306
|
+
const cap = ext === "mp4" || ext === "webm" ? 50 * 1024 * 1024 : LANDING_PAGE_BATCH_BYTES;
|
|
8307
|
+
if (!Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > cap) throw new Error(`File exceeds ${cap} bytes: ${file.path}`);
|
|
8308
|
+
total += file.bytes;
|
|
8309
|
+
}
|
|
8310
|
+
if (total > 100 * 1024 * 1024) throw new Error("A bundle is limited to 100 MB");
|
|
8311
|
+
}
|
|
8264
8312
|
|
|
8265
8313
|
// src/api.ts
|
|
8266
8314
|
function simulateTriggers(creds, body5) {
|
|
@@ -8482,6 +8530,7 @@ function applyExperimentFilesystem(creds, id, body5) {
|
|
|
8482
8530
|
`/api/v1/experiments/${id}/filesystem/apply`,
|
|
8483
8531
|
{
|
|
8484
8532
|
files: body5.files,
|
|
8533
|
+
...body5.accept_qa_invalidation !== void 0 ? { accept_qa_invalidation: body5.accept_qa_invalidation } : {},
|
|
8485
8534
|
...body5.confirmTargetingReplace === true ? { confirmTargetingReplace: true } : {}
|
|
8486
8535
|
}
|
|
8487
8536
|
);
|
|
@@ -8698,7 +8747,7 @@ function testTargeting(creds, url, opts) {
|
|
|
8698
8747
|
if (opts?.status) params.set("status", opts.status);
|
|
8699
8748
|
return request(creds, "GET", `/api/v1/targeting/test?${params.toString()}`);
|
|
8700
8749
|
}
|
|
8701
|
-
function createSos(creds, body5) {
|
|
8750
|
+
function createSos(creds, body5, options = {}) {
|
|
8702
8751
|
const idempotency_key = body5.idempotency_key ?? (0, import_node_crypto2.createHash)("sha256").update(
|
|
8703
8752
|
[
|
|
8704
8753
|
body5.type,
|
|
@@ -8706,11 +8755,29 @@ function createSos(creds, body5) {
|
|
|
8706
8755
|
body5.description,
|
|
8707
8756
|
body5.sub_class ?? "",
|
|
8708
8757
|
body5.loom_url ?? "",
|
|
8758
|
+
...options.attachmentDigests ?? [],
|
|
8709
8759
|
String(Math.floor(Date.now() / 36e5))
|
|
8710
8760
|
].join("\0")
|
|
8711
8761
|
).digest("hex");
|
|
8712
8762
|
return request(creds, "POST", "/api/v1/sos", { ...body5, idempotency_key });
|
|
8713
8763
|
}
|
|
8764
|
+
function createSosUploadGrants(creds, files) {
|
|
8765
|
+
return request(
|
|
8766
|
+
creds,
|
|
8767
|
+
"POST",
|
|
8768
|
+
"/api/v1/sos/attachment-uploads",
|
|
8769
|
+
{ files }
|
|
8770
|
+
);
|
|
8771
|
+
}
|
|
8772
|
+
async function uploadSosAttachment(signedUrl, contentType, bytes) {
|
|
8773
|
+
const response = await fetch(signedUrl, {
|
|
8774
|
+
method: "PUT",
|
|
8775
|
+
headers: { "Content-Type": contentType },
|
|
8776
|
+
body: bytes,
|
|
8777
|
+
signal: AbortSignal.timeout(12e4)
|
|
8778
|
+
});
|
|
8779
|
+
if (!response.ok) throw new Error(`Attachment upload failed (${response.status})`);
|
|
8780
|
+
}
|
|
8714
8781
|
function listMySos(creds) {
|
|
8715
8782
|
return request(creds, "GET", "/api/v1/sos/mine");
|
|
8716
8783
|
}
|
|
@@ -8813,6 +8880,17 @@ function markInboxRead(creds, channel) {
|
|
|
8813
8880
|
function userCredentials(creds) {
|
|
8814
8881
|
return { ...creds, apiKey: creds.userApiKey ?? "", baseUrl: creds.userBaseUrl ?? creds.baseUrl };
|
|
8815
8882
|
}
|
|
8883
|
+
function createAdminCustomer(creds, body5) {
|
|
8884
|
+
return request(
|
|
8885
|
+
userCredentials(creds),
|
|
8886
|
+
"POST",
|
|
8887
|
+
"/api/v1/admin/customers",
|
|
8888
|
+
body5
|
|
8889
|
+
);
|
|
8890
|
+
}
|
|
8891
|
+
function setAdminCustomerCategory(creds, orgId, body5) {
|
|
8892
|
+
return request(userCredentials(creds), "PATCH", `/api/v1/admin/customers/${orgId}`, body5);
|
|
8893
|
+
}
|
|
8816
8894
|
function getAdminSettings(creds) {
|
|
8817
8895
|
return request(userCredentials(creds), "GET", "/api/v1/admin/settings");
|
|
8818
8896
|
}
|
|
@@ -10451,14 +10529,31 @@ function printFilesystemPlan(plan2) {
|
|
|
10451
10529
|
}
|
|
10452
10530
|
console.log();
|
|
10453
10531
|
console.log(`QA invalidated: ${plan2.invalidatesQa ? source_default.yellow("yes") : "no"}`);
|
|
10454
|
-
console.log(
|
|
10532
|
+
console.log(
|
|
10533
|
+
`QA approval: ${!plan2.qaApproval ? "unknown (older server)" : plan2.qaApproval.present ? `present${plan2.qaApproval.approvedAt ? ` (approved ${plan2.qaApproval.approvedAt})` : ""}` : "none"}`
|
|
10534
|
+
);
|
|
10535
|
+
console.log(`Acknowledgment: ${requiredQaAcknowledgment(plan2) ?? "none"}`);
|
|
10455
10536
|
console.log(`Precompile: ${plan2.precompileRequired ? "yes" : "no"}`);
|
|
10456
10537
|
}
|
|
10538
|
+
function requiredQaAcknowledgment(plan2) {
|
|
10539
|
+
return "requiredAcknowledgment" in plan2 ? plan2.requiredAcknowledgment ?? null : plan2.invalidatesQa ? "accept-qa-invalidation" : null;
|
|
10540
|
+
}
|
|
10457
10541
|
function blockingIssueSummary(issues) {
|
|
10458
10542
|
if (!issues?.length) return "Experiment filesystem plan has errors.";
|
|
10459
10543
|
const remaining = issues.length - 1;
|
|
10460
10544
|
return `Experiment filesystem plan has errors. ${issues[0].message}${remaining ? ` and ${remaining} more` : ""}`;
|
|
10461
10545
|
}
|
|
10546
|
+
function qaApprovalRefusal(approvedAt, folderArg) {
|
|
10547
|
+
return {
|
|
10548
|
+
message: `Applying these changes revokes the QA approval from ${approvedAt ?? "an unspecified time"}; pass --accept-qa-invalidation to confirm.`,
|
|
10549
|
+
details: {
|
|
10550
|
+
reason: "qa_approval_present",
|
|
10551
|
+
qa_approved_at: approvedAt,
|
|
10552
|
+
required_acknowledgment: "accept-qa-invalidation",
|
|
10553
|
+
retry: `apex experiments apply ${folderArg} --accept-qa-invalidation`
|
|
10554
|
+
}
|
|
10555
|
+
};
|
|
10556
|
+
}
|
|
10462
10557
|
async function getQaPreviewUrls(creds, experimentId) {
|
|
10463
10558
|
const fallbackDashboardUrl = buildDashboardUrl(
|
|
10464
10559
|
creds.baseUrl,
|
|
@@ -11834,7 +11929,7 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
11834
11929
|
folder,
|
|
11835
11930
|
experimentId: session.experimentId,
|
|
11836
11931
|
plan: res.data,
|
|
11837
|
-
requiredAcknowledgment: res.data
|
|
11932
|
+
requiredAcknowledgment: requiredQaAcknowledgment(res.data)
|
|
11838
11933
|
},
|
|
11839
11934
|
() => printFilesystemPlan(res.data)
|
|
11840
11935
|
);
|
|
@@ -11846,7 +11941,10 @@ var planFilesystem = new Command("plan").description("Plan experiment filesystem
|
|
|
11846
11941
|
var applyFilesystem = new Command("apply").option(
|
|
11847
11942
|
"--confirm-targeting-replace",
|
|
11848
11943
|
"Confirm that attaching a Page may replace URL targeting authored through the API"
|
|
11849
|
-
).option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option(
|
|
11944
|
+
).option("--sync", "Refresh local authored files with the stored server bytes").description("Apply experiment filesystem changes to Apex").argument("[folder]", "Experiment filesystem folder", ".").option(
|
|
11945
|
+
"--accept-qa-invalidation",
|
|
11946
|
+
"Confirm an apply that revokes an existing QA approval (required only when the experiment holds one)"
|
|
11947
|
+
).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) {
|
|
11850
11948
|
const creds = requireCredentials();
|
|
11851
11949
|
try {
|
|
11852
11950
|
const folder = folderPath(folderArg);
|
|
@@ -11893,7 +11991,7 @@ var applyFilesystem = new Command("apply").option(
|
|
|
11893
11991
|
why: "Retry with hosted asset upload."
|
|
11894
11992
|
}
|
|
11895
11993
|
] : [],
|
|
11896
|
-
requiredAcknowledgment: planRes.data
|
|
11994
|
+
requiredAcknowledgment: requiredQaAcknowledgment(planRes.data)
|
|
11897
11995
|
});
|
|
11898
11996
|
}
|
|
11899
11997
|
if (planRes.data.changes.length === 0) {
|
|
@@ -11910,7 +12008,7 @@ var applyFilesystem = new Command("apply").option(
|
|
|
11910
12008
|
synced: synced2,
|
|
11911
12009
|
dashboardUrl: dashboardUrl2,
|
|
11912
12010
|
plan: planRes.data,
|
|
11913
|
-
requiredAcknowledgment: planRes.data
|
|
12011
|
+
requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
|
|
11914
12012
|
message: "No changes."
|
|
11915
12013
|
},
|
|
11916
12014
|
() => {
|
|
@@ -11922,23 +12020,23 @@ var applyFilesystem = new Command("apply").option(
|
|
|
11922
12020
|
);
|
|
11923
12021
|
return;
|
|
11924
12022
|
}
|
|
11925
|
-
|
|
11926
|
-
|
|
12023
|
+
let acceptQaInvalidation = Boolean(opts.force || opts.acceptQaInvalidation);
|
|
12024
|
+
if (requiredQaAcknowledgment(planRes.data) && !acceptQaInvalidation) {
|
|
12025
|
+
const refusal = qaApprovalRefusal(planRes.data.qaApproval?.approvedAt ?? null, folderArg);
|
|
12026
|
+
const ok = await confirmApply(`${refusal.message} Continue?`);
|
|
11927
12027
|
if (ok === null) {
|
|
11928
|
-
confirmationRequired
|
|
11929
|
-
this,
|
|
11930
|
-
"Applying these changes invalidates QA approval and requires confirmation.",
|
|
11931
|
-
`apex experiments apply ${folderArg} --accept-qa-invalidation`
|
|
11932
|
-
);
|
|
12028
|
+
fail(this, refusal.message, EXIT_CODES.confirmationRequired, refusal.details);
|
|
11933
12029
|
}
|
|
11934
12030
|
if (!ok) {
|
|
11935
12031
|
output(this, { aborted: true }, () => console.log("Aborted."));
|
|
11936
12032
|
return;
|
|
11937
12033
|
}
|
|
12034
|
+
acceptQaInvalidation = true;
|
|
11938
12035
|
}
|
|
11939
12036
|
const spinner = process.stderr.isTTY ? await createSpinner("Applying experiment filesystem...") : null;
|
|
11940
12037
|
let res = await applyExperimentFilesystem(creds, session.experimentId, {
|
|
11941
12038
|
files,
|
|
12039
|
+
accept_qa_invalidation: acceptQaInvalidation,
|
|
11942
12040
|
...opts.confirmTargetingReplace ? { confirmTargetingReplace: true } : {}
|
|
11943
12041
|
});
|
|
11944
12042
|
if (!res.ok && res.status === 409 && res.errorDetails?.code === "confirm_targeting_replace_required" && !opts.confirmTargetingReplace) {
|
|
@@ -11959,9 +12057,19 @@ var applyFilesystem = new Command("apply").option(
|
|
|
11959
12057
|
}
|
|
11960
12058
|
res = await applyExperimentFilesystem(creds, session.experimentId, {
|
|
11961
12059
|
files,
|
|
12060
|
+
accept_qa_invalidation: acceptQaInvalidation,
|
|
11962
12061
|
confirmTargetingReplace: true
|
|
11963
12062
|
});
|
|
11964
12063
|
}
|
|
12064
|
+
if (!res.ok && res.status === 409 && res.errorDetails?.code === "qa_acknowledgment_required") {
|
|
12065
|
+
spinner?.stop();
|
|
12066
|
+
const approvedAt = res.errorDetails.qa_approved_at;
|
|
12067
|
+
const refusal = qaApprovalRefusal(
|
|
12068
|
+
typeof approvedAt === "string" ? approvedAt : null,
|
|
12069
|
+
folderArg
|
|
12070
|
+
);
|
|
12071
|
+
fail(this, refusal.message, EXIT_CODES.confirmationRequired, refusal.details);
|
|
12072
|
+
}
|
|
11965
12073
|
if (!res.ok || !res.data?.applied) {
|
|
11966
12074
|
spinner?.fail("Failed to apply experiment filesystem");
|
|
11967
12075
|
fail(this, res.error ?? "Apply failed", classifyStatus(res.status), {
|
|
@@ -12000,7 +12108,7 @@ var applyFilesystem = new Command("apply").option(
|
|
|
12000
12108
|
experimentId: session.experimentId,
|
|
12001
12109
|
applied: true,
|
|
12002
12110
|
plan: planRes.data,
|
|
12003
|
-
requiredAcknowledgment: planRes.data
|
|
12111
|
+
requiredAcknowledgment: requiredQaAcknowledgment(planRes.data),
|
|
12004
12112
|
result: res.data,
|
|
12005
12113
|
synced,
|
|
12006
12114
|
dashboardUrl,
|
|
@@ -13001,6 +13109,25 @@ var pushCommand = new Command("push").description("Push experiment definitions f
|
|
|
13001
13109
|
|
|
13002
13110
|
// src/commands/preview.ts
|
|
13003
13111
|
init_source();
|
|
13112
|
+
async function signedPreviewFromReadiness(creds, experimentId, variationId, targetUrl) {
|
|
13113
|
+
try {
|
|
13114
|
+
const client = createHttpClient(creds, {
|
|
13115
|
+
onNetworkError: (info) => {
|
|
13116
|
+
throw new Error(info.cause);
|
|
13117
|
+
}
|
|
13118
|
+
});
|
|
13119
|
+
const query = new URLSearchParams({ variation_id: variationId, target_url: targetUrl });
|
|
13120
|
+
const response = await client.request(
|
|
13121
|
+
"GET",
|
|
13122
|
+
`/api/v1/experiments/${encodeURIComponent(experimentId)}/readiness?${query}`,
|
|
13123
|
+
void 0,
|
|
13124
|
+
5e3
|
|
13125
|
+
);
|
|
13126
|
+
return response.ok ? response.data?.data : void 0;
|
|
13127
|
+
} catch {
|
|
13128
|
+
return void 0;
|
|
13129
|
+
}
|
|
13130
|
+
}
|
|
13004
13131
|
var previewCommand = new Command("preview").description(
|
|
13005
13132
|
"Open QA preview URL for an experiment variation; proxy previews return a QA run receipt"
|
|
13006
13133
|
).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(
|
|
@@ -13112,7 +13239,7 @@ var previewCommand = new Command("preview").description(
|
|
|
13112
13239
|
variation = exp.variations.find((v) => !v.is_control) ?? exp.variations[0];
|
|
13113
13240
|
}
|
|
13114
13241
|
spinner.stop();
|
|
13115
|
-
const storeUrl = opts?.storeUrl ?? exp.preview_target?.url ?? exp.store_url;
|
|
13242
|
+
const storeUrl = opts?.storeUrl ?? (!variation.is_control && variation.redirect_url?.trim() ? variation.redirect_url : null) ?? exp.preview_target?.url ?? exp.store_url;
|
|
13116
13243
|
if (!storeUrl) {
|
|
13117
13244
|
fail(
|
|
13118
13245
|
previewCommand,
|
|
@@ -13128,12 +13255,18 @@ var previewCommand = new Command("preview").description(
|
|
|
13128
13255
|
'Invalid store URL. Pass an absolute URL like "https://example.com/products/item".'
|
|
13129
13256
|
);
|
|
13130
13257
|
}
|
|
13258
|
+
const signed = await signedPreviewFromReadiness(creds, exp.id, variation.id, storeUrl);
|
|
13259
|
+
const selectedPreviewUrl = signed?.signed_preview_url ?? previewUrl.toString();
|
|
13260
|
+
const warning = signed?.signed_preview_url ? null : "Test images may not load on this unsigned link. Open Apex \u2192 QA for a signed link.";
|
|
13261
|
+
if (warning) console.error(warning);
|
|
13131
13262
|
output(
|
|
13132
13263
|
previewCommand,
|
|
13133
13264
|
{
|
|
13134
13265
|
experimentId: exp.id,
|
|
13135
13266
|
variationId: variation.id,
|
|
13136
|
-
previewUrl:
|
|
13267
|
+
previewUrl: selectedPreviewUrl,
|
|
13268
|
+
expiresAt: signed?.signed_preview_expires_at ?? null,
|
|
13269
|
+
...warning ? { warning } : {},
|
|
13137
13270
|
...!opts?.storeUrl && exp.preview_target ? { previewTarget: exp.preview_target } : {}
|
|
13138
13271
|
},
|
|
13139
13272
|
() => {
|
|
@@ -13149,14 +13282,16 @@ var previewCommand = new Command("preview").description(
|
|
|
13149
13282
|
}
|
|
13150
13283
|
}
|
|
13151
13284
|
console.log();
|
|
13152
|
-
console.log(source_default.cyan(
|
|
13285
|
+
console.log(source_default.cyan(selectedPreviewUrl));
|
|
13286
|
+
if (signed?.signed_preview_expires_at)
|
|
13287
|
+
console.log(`Valid until ${signed.signed_preview_expires_at}.`);
|
|
13153
13288
|
console.log();
|
|
13154
13289
|
}
|
|
13155
13290
|
);
|
|
13156
13291
|
if (opts?.open !== false && !shouldUseJson(previewCommand)) {
|
|
13157
13292
|
try {
|
|
13158
13293
|
const open3 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
13159
|
-
await open3(
|
|
13294
|
+
await open3(selectedPreviewUrl);
|
|
13160
13295
|
console.log(source_default.dim("Opened in browser."));
|
|
13161
13296
|
} catch {
|
|
13162
13297
|
console.log(source_default.dim("Copy the URL above to preview."));
|
|
@@ -14143,9 +14278,7 @@ function commonCookieDomain(hostnames) {
|
|
|
14143
14278
|
const suffix = suffixes[0];
|
|
14144
14279
|
return suffix && suffixes.every((candidate) => candidate === suffix) ? `.${suffix}` : null;
|
|
14145
14280
|
}
|
|
14146
|
-
|
|
14147
|
-
return import_node_path11.default.resolve(process.cwd(), ".apex", "tracking-manifest.json");
|
|
14148
|
-
}
|
|
14281
|
+
var DEFAULT_MANIFEST_PATH = ".apex/tracking-manifest.json";
|
|
14149
14282
|
function readManifest(filePath) {
|
|
14150
14283
|
if (!import_node_fs13.default.existsSync(filePath)) throw new Error(`Manifest not found: ${filePath}`);
|
|
14151
14284
|
const parsed = JSON.parse(import_node_fs13.default.readFileSync(filePath, "utf8"));
|
|
@@ -14221,7 +14354,7 @@ function buildManifest(urlArgs, opts) {
|
|
|
14221
14354
|
};
|
|
14222
14355
|
}
|
|
14223
14356
|
function manifestPath(input) {
|
|
14224
|
-
return import_node_path11.default.resolve(process.cwd(), input ??
|
|
14357
|
+
return import_node_path11.default.resolve(process.cwd(), input ?? DEFAULT_MANIFEST_PATH);
|
|
14225
14358
|
}
|
|
14226
14359
|
function printManifestPlan(manifest) {
|
|
14227
14360
|
console.log(source_default.bold("Apex tracking ingestion plan"));
|
|
@@ -14249,7 +14382,11 @@ function printManifestPlan(manifest) {
|
|
|
14249
14382
|
var trackingCommand = new Command("tracking").description(
|
|
14250
14383
|
"Create Apex-first tracking ingestion manifests and QA handoffs"
|
|
14251
14384
|
);
|
|
14252
|
-
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(
|
|
14385
|
+
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(
|
|
14386
|
+
"--out <path>",
|
|
14387
|
+
"Manifest output path, relative to the current directory",
|
|
14388
|
+
DEFAULT_MANIFEST_PATH
|
|
14389
|
+
).action(function(urls, opts) {
|
|
14253
14390
|
const manifest = buildManifest(urls, opts);
|
|
14254
14391
|
const outPath = manifestPath(opts.out);
|
|
14255
14392
|
writeManifest(outPath, manifest);
|
|
@@ -14265,12 +14402,20 @@ trackingCommand.command("inspect").description("Create a local tracking manifest
|
|
|
14265
14402
|
}
|
|
14266
14403
|
});
|
|
14267
14404
|
});
|
|
14268
|
-
trackingCommand.command("plan").description("Print the implementation plan from a tracking manifest").argument(
|
|
14405
|
+
trackingCommand.command("plan").description("Print the implementation plan from a tracking manifest").argument(
|
|
14406
|
+
"[manifest]",
|
|
14407
|
+
"Tracking manifest path, relative to the current directory",
|
|
14408
|
+
DEFAULT_MANIFEST_PATH
|
|
14409
|
+
).action(function(input) {
|
|
14269
14410
|
const filePath = manifestPath(input);
|
|
14270
14411
|
const manifest = readManifest(filePath);
|
|
14271
14412
|
output(this, { path: filePath, manifest }, () => printManifestPlan(manifest));
|
|
14272
14413
|
});
|
|
14273
|
-
trackingCommand.command("qa").description("Print the QA checklist for a tracking manifest").argument(
|
|
14414
|
+
trackingCommand.command("qa").description("Print the QA checklist for a tracking manifest").argument(
|
|
14415
|
+
"[manifest]",
|
|
14416
|
+
"Tracking manifest path, relative to the current directory",
|
|
14417
|
+
DEFAULT_MANIFEST_PATH
|
|
14418
|
+
).action(function(input) {
|
|
14274
14419
|
const filePath = manifestPath(input);
|
|
14275
14420
|
const manifest = readManifest(filePath);
|
|
14276
14421
|
output(this, { path: filePath, checks: manifest.qa.checks }, () => {
|
|
@@ -18484,16 +18629,21 @@ async function checkServedConfig(configUrl, workerBaseUrl, workerBaseSource) {
|
|
|
18484
18629
|
message: "Worker served a recognized config document for this shop."
|
|
18485
18630
|
};
|
|
18486
18631
|
}
|
|
18487
|
-
var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description(
|
|
18632
|
+
var installCommand = new Command("install").description("Check Apex storefront installation").command("check").description(
|
|
18633
|
+
"Verify snippet, served config, and recent tracking using --shop-id, the selected shop, or DRIP_SHOP_ID"
|
|
18634
|
+
).option("--url <url>", "Check one explicit bound storefront URL").option(
|
|
18635
|
+
"--shop-id <id>",
|
|
18636
|
+
"Apex shop ID to verify (falls back to --shop/apex use, then DRIP_SHOP_ID)"
|
|
18637
|
+
).option(
|
|
18488
18638
|
"--worker-url <url>",
|
|
18489
18639
|
"Apex Worker base URL that serves /config (defaults to the URL in the served snippet, then DRIP_WORKER_URL, then the production worker)"
|
|
18490
18640
|
).option("--environment <key>", "Environment key to request from the worker", "production").action(async function action65(opts) {
|
|
18491
18641
|
const creds = requireCredentials();
|
|
18492
|
-
const shopId = opts.shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
|
|
18642
|
+
const shopId = opts.shopId?.trim() || resolveContext(creds).shopId?.trim() || process.env.DRIP_SHOP_ID?.trim() || null;
|
|
18493
18643
|
if (!shopId)
|
|
18494
18644
|
fail(
|
|
18495
18645
|
this,
|
|
18496
|
-
"Shop ID is required: pass --shop-id <id> or set DRIP_SHOP_ID",
|
|
18646
|
+
"Shop ID is required: pass --shop-id <id>, select a shop with --shop <id> or apex use, or set DRIP_SHOP_ID",
|
|
18497
18647
|
EXIT_CODES.validation,
|
|
18498
18648
|
{ field: "shop-id" }
|
|
18499
18649
|
);
|
|
@@ -19533,6 +19683,7 @@ var test2 = new Command("test").description("Evaluate experiment URL targeting f
|
|
|
19533
19683
|
var targetingCommand = new Command("targeting").description("Test experiment targeting").addCommand(test2);
|
|
19534
19684
|
|
|
19535
19685
|
// src/commands/sos.ts
|
|
19686
|
+
var import_node_crypto7 = require("node:crypto");
|
|
19536
19687
|
init_source();
|
|
19537
19688
|
var import_node_fs24 = __toESM(require("node:fs"));
|
|
19538
19689
|
var import_node_path21 = __toESM(require("node:path"));
|
|
@@ -19574,6 +19725,11 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
19574
19725
|
"--type <type>",
|
|
19575
19726
|
"full_handover|design|development|qa|config|bug|help (internal types require a DRIP account)",
|
|
19576
19727
|
"qa"
|
|
19728
|
+
).option(
|
|
19729
|
+
"--attach <file>",
|
|
19730
|
+
"Attach an image or video (repeatable, maximum 5)",
|
|
19731
|
+
(file, files) => [...files, file],
|
|
19732
|
+
[]
|
|
19577
19733
|
).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) {
|
|
19578
19734
|
if (!SOS_TYPES.has(opts.type)) {
|
|
19579
19735
|
fail(
|
|
@@ -19585,16 +19741,76 @@ var create4 = new Command("create").description("Create an Apex SOS ticket").opt
|
|
|
19585
19741
|
if (opts.experiment && opts.backlogItem) {
|
|
19586
19742
|
fail(this, "Provide only one of --experiment or --backlog-item", 3);
|
|
19587
19743
|
}
|
|
19744
|
+
const mediaTypes = {
|
|
19745
|
+
".png": "image/png",
|
|
19746
|
+
".jpg": "image/jpeg",
|
|
19747
|
+
".jpeg": "image/jpeg",
|
|
19748
|
+
".webp": "image/webp",
|
|
19749
|
+
".gif": "image/gif",
|
|
19750
|
+
".mp4": "video/mp4",
|
|
19751
|
+
".webm": "video/webm",
|
|
19752
|
+
".mov": "video/quicktime"
|
|
19753
|
+
};
|
|
19754
|
+
if (opts.attach.length > 5) fail(this, "Attach no more than 5 files", 3);
|
|
19755
|
+
const files = opts.attach.map((file) => {
|
|
19756
|
+
const filename = import_node_path21.default.basename(file);
|
|
19757
|
+
const content_type = mediaTypes[import_node_path21.default.extname(file).toLowerCase()];
|
|
19758
|
+
if (!content_type) fail(this, "Unsupported attachment type", 3);
|
|
19759
|
+
let size;
|
|
19760
|
+
try {
|
|
19761
|
+
size = import_node_fs24.default.statSync(file).size;
|
|
19762
|
+
} catch {
|
|
19763
|
+
fail(this, `Attachment not found: ${file}`, 3);
|
|
19764
|
+
}
|
|
19765
|
+
const cap = (content_type.startsWith("video/") ? 50 : 10) * 1024 * 1024;
|
|
19766
|
+
if (size <= 0 || size > cap) fail(this, "Attachment exceeds its size limit or is empty", 3);
|
|
19767
|
+
let bytes;
|
|
19768
|
+
try {
|
|
19769
|
+
bytes = import_node_fs24.default.readFileSync(file);
|
|
19770
|
+
} catch {
|
|
19771
|
+
fail(this, `Attachment not found: ${file}`, 3);
|
|
19772
|
+
}
|
|
19773
|
+
return { filename, content_type, size, bytes };
|
|
19774
|
+
});
|
|
19588
19775
|
const creds = requireCredentials();
|
|
19776
|
+
const attachmentPaths = [];
|
|
19777
|
+
const attachmentDigests = [];
|
|
19778
|
+
if (files.length) {
|
|
19779
|
+
const grants = await createSosUploadGrants(
|
|
19780
|
+
creds,
|
|
19781
|
+
files.map(({ filename, content_type, size }) => ({ filename, content_type, size }))
|
|
19782
|
+
);
|
|
19783
|
+
if (!grants.ok || !grants.data)
|
|
19784
|
+
fail(
|
|
19785
|
+
this,
|
|
19786
|
+
grants.error ?? "Failed to get upload grants",
|
|
19787
|
+
classifyStatus(grants.status),
|
|
19788
|
+
grants.errorDetails
|
|
19789
|
+
);
|
|
19790
|
+
if (grants.data.data.uploads.length !== files.length)
|
|
19791
|
+
fail(this, "Invalid upload grant response", 1);
|
|
19792
|
+
for (const [index, file] of files.entries()) {
|
|
19793
|
+
const grant = grants.data.data.uploads[index];
|
|
19794
|
+
const bytes = file.bytes;
|
|
19795
|
+
await uploadSosAttachment(grant.signed_url, file.content_type, bytes);
|
|
19796
|
+
attachmentPaths.push(grant.path);
|
|
19797
|
+
attachmentDigests.push((0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex"));
|
|
19798
|
+
}
|
|
19799
|
+
}
|
|
19589
19800
|
const description = readText(opts.description) ?? descriptionArg ?? `Apex CLI ${opts.type} SOS request${opts.loom ? ` with Loom evidence: ${opts.loom}` : "."}`;
|
|
19590
|
-
const res = await createSos(
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19801
|
+
const res = await createSos(
|
|
19802
|
+
creds,
|
|
19803
|
+
{
|
|
19804
|
+
...attachmentPaths.length ? { attachment_paths: attachmentPaths } : {},
|
|
19805
|
+
type: opts.type,
|
|
19806
|
+
description,
|
|
19807
|
+
experiment_id: opts.experiment ?? null,
|
|
19808
|
+
backlog_item_id: opts.backlogItem ?? null,
|
|
19809
|
+
sub_class: opts.subClass ?? null,
|
|
19810
|
+
loom_url: opts.loom ?? null
|
|
19811
|
+
},
|
|
19812
|
+
{ attachmentDigests }
|
|
19813
|
+
);
|
|
19598
19814
|
if (!res.ok || !res.data) {
|
|
19599
19815
|
fail(
|
|
19600
19816
|
this,
|
|
@@ -19899,6 +20115,7 @@ function optionSchema(option) {
|
|
|
19899
20115
|
description: option.description,
|
|
19900
20116
|
required: option.required,
|
|
19901
20117
|
optional: option.optional,
|
|
20118
|
+
mandatory: Boolean(option.mandatory),
|
|
19902
20119
|
variadic: option.variadic,
|
|
19903
20120
|
...option.defaultValue === void 0 ? {} : { defaultValue: option.defaultValue }
|
|
19904
20121
|
};
|
|
@@ -19921,8 +20138,16 @@ function commandSchema(command) {
|
|
|
19921
20138
|
};
|
|
19922
20139
|
}
|
|
19923
20140
|
function createSchemaCommand(program3) {
|
|
19924
|
-
return new Command("schema").description("Print the current Apex CLI command and
|
|
19925
|
-
const schema =
|
|
20141
|
+
return new Command("schema").description("Print the current Apex CLI command, flag, and mandatory-option schema").option("--json", "Print JSON schema").action(function action78() {
|
|
20142
|
+
const schema = {
|
|
20143
|
+
schemaVersion: 2,
|
|
20144
|
+
contract: {
|
|
20145
|
+
optionValueArity: "Options' required and optional fields describe whether the option's value is required or optional when the flag is given.",
|
|
20146
|
+
mandatoryOptions: "An option's mandatory field is true only when the flag itself must be passed.",
|
|
20147
|
+
requiredArguments: "An argument's required field means the positional is mandatory."
|
|
20148
|
+
},
|
|
20149
|
+
...commandSchema(program3)
|
|
20150
|
+
};
|
|
19926
20151
|
output(this, schema, () => {
|
|
19927
20152
|
process.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
19928
20153
|
`);
|
|
@@ -20563,6 +20788,7 @@ var skillsCommand = new Command("skills").description("Print or install the Apex
|
|
|
20563
20788
|
});
|
|
20564
20789
|
|
|
20565
20790
|
// src/commands/admin.ts
|
|
20791
|
+
var 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;
|
|
20566
20792
|
function switchValue(value) {
|
|
20567
20793
|
if (value === "on") return true;
|
|
20568
20794
|
if (value === "off") return false;
|
|
@@ -20677,7 +20903,137 @@ var restore2 = new Command("restore").description("Restore an archived shop").ar
|
|
|
20677
20903
|
);
|
|
20678
20904
|
});
|
|
20679
20905
|
var shops = new Command("shops").description("Archive and restore shops on the platform admin plane").addCommand(archive2).addCommand(restore2);
|
|
20680
|
-
var
|
|
20906
|
+
var create6 = new Command("create").description("Create an internal, demo or dogfood account").requiredOption("--name <text>").requiredOption("--category <category>", "internal, demo or dogfood").option("--type <type>", "company or agency", "company").option("--plan <plan>", "self_managed, apex_managed or drip_managed", "self_managed").option("--domain <host>").option("--platform <platform>", "shopify, shopware, woo or other").option("--claim-shop <uuid>").option("--owner <email>").option("--consultant <uuid>", "Platform access ID").option("--consultant-title <text>").requiredOption("--reason <text>", "Audit reason, 8 to 500 characters").option("--confirm", "Confirm account creation").option("--json", "Output JSON").action(async function(opts) {
|
|
20907
|
+
if (opts.category === "customer") fail(this, "Create customer accounts in Admin, Links", 3);
|
|
20908
|
+
if (!["internal", "demo", "dogfood"].includes(opts.category))
|
|
20909
|
+
fail(this, "--category must be internal, demo or dogfood", 3);
|
|
20910
|
+
if (!["company", "agency"].includes(opts.type))
|
|
20911
|
+
fail(this, "--type must be company or agency", 3);
|
|
20912
|
+
if (!["self_managed", "apex_managed", "drip_managed"].includes(opts.plan))
|
|
20913
|
+
fail(this, "Invalid --plan", 3);
|
|
20914
|
+
if (!opts.name.trim() || opts.name.trim().length > 60)
|
|
20915
|
+
fail(this, "--name must be 1 to 60 characters", 3);
|
|
20916
|
+
if (opts.reason.trim().length < 8 || opts.reason.trim().length > 500)
|
|
20917
|
+
fail(this, "--reason must be 8 to 500 characters", 3);
|
|
20918
|
+
if (opts.claimShop && !uuid.test(opts.claimShop)) fail(this, "--claim-shop must be a UUID", 3);
|
|
20919
|
+
if (opts.claimShop && (opts.domain || opts.platform))
|
|
20920
|
+
fail(this, "Use --claim-shop or --domain with --platform", 3);
|
|
20921
|
+
if (!!opts.domain !== !!opts.platform)
|
|
20922
|
+
fail(this, "--domain and --platform must be provided together", 3);
|
|
20923
|
+
if (opts.domain && !/^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(opts.domain.trim()))
|
|
20924
|
+
fail(this, "--domain must be a hostname without a protocol or path", 3);
|
|
20925
|
+
const platforms = {
|
|
20926
|
+
shopify: "shopify",
|
|
20927
|
+
shopware: "shopware",
|
|
20928
|
+
woo: "woocommerce",
|
|
20929
|
+
other: "custom"
|
|
20930
|
+
};
|
|
20931
|
+
if (opts.platform && !Object.hasOwn(platforms, opts.platform))
|
|
20932
|
+
fail(this, "--platform must be shopify, shopware, woo or other", 3);
|
|
20933
|
+
if (opts.owner && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(opts.owner.trim()))
|
|
20934
|
+
fail(this, "--owner must be an email address", 3);
|
|
20935
|
+
if (opts.owner && !opts.domain && !opts.claimShop)
|
|
20936
|
+
fail(this, "--owner requires a store for the invitation", 3);
|
|
20937
|
+
if (opts.plan !== "self_managed" && (!opts.consultant || !opts.consultantTitle?.trim()))
|
|
20938
|
+
fail(this, "Managed plans require --consultant and --consultant-title", 3);
|
|
20939
|
+
if (opts.consultant && !uuid.test(opts.consultant))
|
|
20940
|
+
fail(this, "--consultant must be a platform access UUID", 3);
|
|
20941
|
+
if (!!opts.consultant !== !!opts.consultantTitle?.trim())
|
|
20942
|
+
fail(this, "Provide --consultant and --consultant-title together", 3);
|
|
20943
|
+
if (opts.consultantTitle && opts.consultantTitle.trim().length > 240)
|
|
20944
|
+
fail(this, "--consultant-title must be at most 240 characters", 3);
|
|
20945
|
+
if (!opts.confirm) {
|
|
20946
|
+
const flags = [
|
|
20947
|
+
["name", opts.name],
|
|
20948
|
+
["category", opts.category],
|
|
20949
|
+
["type", opts.type],
|
|
20950
|
+
["plan", opts.plan],
|
|
20951
|
+
["reason", opts.reason],
|
|
20952
|
+
["domain", opts.domain],
|
|
20953
|
+
["platform", opts.platform],
|
|
20954
|
+
["claim-shop", opts.claimShop],
|
|
20955
|
+
["owner", opts.owner],
|
|
20956
|
+
["consultant", opts.consultant],
|
|
20957
|
+
["consultant-title", opts.consultantTitle]
|
|
20958
|
+
];
|
|
20959
|
+
const retry = flags.filter((entry) => entry[1] !== void 0).map(([flag, value]) => `--${flag} '${value.replaceAll("'", "'\\''")}'`).join(" ");
|
|
20960
|
+
confirmationRequired(
|
|
20961
|
+
this,
|
|
20962
|
+
"--confirm is required to create an account",
|
|
20963
|
+
`apex admin customers create ${retry} --confirm`
|
|
20964
|
+
);
|
|
20965
|
+
}
|
|
20966
|
+
const body5 = {
|
|
20967
|
+
name: opts.name.trim(),
|
|
20968
|
+
category: opts.category,
|
|
20969
|
+
type: opts.type,
|
|
20970
|
+
plan: opts.plan,
|
|
20971
|
+
reason: opts.reason.trim(),
|
|
20972
|
+
teammates: [],
|
|
20973
|
+
consultant: opts.consultant ? { platformAccessId: opts.consultant, title: opts.consultantTitle.trim() } : null,
|
|
20974
|
+
...opts.claimShop ? { store: { claimShopId: opts.claimShop } } : opts.domain ? {
|
|
20975
|
+
store: {
|
|
20976
|
+
domain: opts.domain.trim().toLowerCase(),
|
|
20977
|
+
platform: platforms[opts.platform]
|
|
20978
|
+
}
|
|
20979
|
+
} : {},
|
|
20980
|
+
...opts.owner ? { ownerEmail: opts.owner.trim() } : {}
|
|
20981
|
+
};
|
|
20982
|
+
const res = await createAdminCustomer(requireUserCredentials(), body5);
|
|
20983
|
+
if (!res.ok || !res.data)
|
|
20984
|
+
fail(
|
|
20985
|
+
this,
|
|
20986
|
+
res.error ?? "Failed to create account",
|
|
20987
|
+
classifyStatus(res.status),
|
|
20988
|
+
res.errorDetails
|
|
20989
|
+
);
|
|
20990
|
+
output(this, res.data, () => {
|
|
20991
|
+
const result = res.data.data;
|
|
20992
|
+
printTable(
|
|
20993
|
+
["ORGANIZATION", "STORE", "INVITATIONS"],
|
|
20994
|
+
[[result.orgId, result.shopId ?? "-", String(result.invitations.length)]]
|
|
20995
|
+
);
|
|
20996
|
+
if (result.invitations.length)
|
|
20997
|
+
printTable(
|
|
20998
|
+
["EMAIL", "ROLE", "STATUS", "NOTE"],
|
|
20999
|
+
result.invitations.map((i) => [i.email, i.role, i.status, i.note])
|
|
21000
|
+
);
|
|
21001
|
+
});
|
|
21002
|
+
});
|
|
21003
|
+
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) {
|
|
21004
|
+
if (!uuid.test(orgId)) fail(this, "<org-id> must be a UUID", 3);
|
|
21005
|
+
if (!["internal", "demo", "dogfood", "customer"].includes(opts.category))
|
|
21006
|
+
fail(this, "--category must be internal, demo, dogfood or customer", 3);
|
|
21007
|
+
const reason2 = opts.reason.trim();
|
|
21008
|
+
if (reason2.length < 8 || reason2.length > 500)
|
|
21009
|
+
fail(this, "--reason must be 8 to 500 characters", 3);
|
|
21010
|
+
if (!opts.confirm) {
|
|
21011
|
+
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
21012
|
+
confirmationRequired(
|
|
21013
|
+
this,
|
|
21014
|
+
"--confirm is required to change an account category",
|
|
21015
|
+
`apex admin customers set-category ${orgId} --category ${quote(opts.category)} --reason ${quote(reason2)} --confirm`
|
|
21016
|
+
);
|
|
21017
|
+
}
|
|
21018
|
+
const body5 = {
|
|
21019
|
+
category: opts.category,
|
|
21020
|
+
reason: reason2
|
|
21021
|
+
};
|
|
21022
|
+
const res = await setAdminCustomerCategory(requireUserCredentials(), orgId, body5);
|
|
21023
|
+
if (!res.ok || !res.data)
|
|
21024
|
+
fail(
|
|
21025
|
+
this,
|
|
21026
|
+
res.error ?? "Failed to change account category",
|
|
21027
|
+
classifyStatus(res.status),
|
|
21028
|
+
res.errorDetails
|
|
21029
|
+
);
|
|
21030
|
+
output(this, res.data, () => {
|
|
21031
|
+
const result = res.data.data;
|
|
21032
|
+
printTable(["RESULT"], [[`Customer ${result.customerId} moved to ${result.category}`]]);
|
|
21033
|
+
});
|
|
21034
|
+
});
|
|
21035
|
+
var customers = new Command("customers").description("Create engineering accounts and manage existing account categories").addCommand(create6).addCommand(setCategory);
|
|
21036
|
+
var adminCommand = new Command("admin").description("Operate allowlisted platform controls").addCommand(settingsGet).addCommand(quarantine).addCommand(shops).addCommand(customers);
|
|
20681
21037
|
|
|
20682
21038
|
// src/commands/membership.ts
|
|
20683
21039
|
init_source();
|
|
@@ -20873,7 +21229,7 @@ var list13 = new Command("list").action(async function() {
|
|
|
20873
21229
|
)
|
|
20874
21230
|
);
|
|
20875
21231
|
});
|
|
20876
|
-
var
|
|
21232
|
+
var create7 = new Command("create").requiredOption("--name <name>").requiredOption("--type <type>").option("--permissions <csv>").action(async function(opts) {
|
|
20877
21233
|
if (!["live_secret", "test_secret", "publishable"].includes(opts.type))
|
|
20878
21234
|
throw new CliError(
|
|
20879
21235
|
"--type must be live_secret, test_secret, or publishable",
|
|
@@ -20913,7 +21269,7 @@ var revoke = new Command("revoke").argument("<keyId>").action(async function(key
|
|
|
20913
21269
|
);
|
|
20914
21270
|
output(this, res.data, () => console.log(`Revoked ${keyId}`));
|
|
20915
21271
|
});
|
|
20916
|
-
var keysCommand = new Command("keys").description("Manage shop API keys in the current context").addCommand(list13).addCommand(
|
|
21272
|
+
var keysCommand = new Command("keys").description("Manage shop API keys in the current context").addCommand(list13).addCommand(create7).addCommand(revoke);
|
|
20917
21273
|
|
|
20918
21274
|
// src/commands/prospect.ts
|
|
20919
21275
|
init_source();
|
|
@@ -21172,7 +21528,7 @@ var list14 = new Command("list").alias("ls").description(
|
|
|
21172
21528
|
const data = await loadWebhookIndex(this);
|
|
21173
21529
|
output(this, data, () => printWebhooks(data.data));
|
|
21174
21530
|
});
|
|
21175
|
-
var
|
|
21531
|
+
var create8 = new Command("create").description(`Create a webhook. Prints the signing secret once. Signature docs: ${SIGNATURE_DOC}`).requiredOption("--url <url>", "HTTPS callback URL").option(
|
|
21176
21532
|
"--event <event>",
|
|
21177
21533
|
"Webhook event to subscribe to; repeat for multiple events",
|
|
21178
21534
|
(value, previous = []) => [...previous, value],
|
|
@@ -21280,7 +21636,7 @@ var test3 = new Command("test").description("Send a signed test event to a webho
|
|
|
21280
21636
|
)
|
|
21281
21637
|
);
|
|
21282
21638
|
});
|
|
21283
|
-
var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(
|
|
21639
|
+
var webhooksCommand = new Command("webhooks").description("Manage shop webhooks").addCommand(list14).addCommand(create8).addCommand(update5).addCommand(remove3).addCommand(deliveries).addCommand(test3);
|
|
21284
21640
|
|
|
21285
21641
|
// src/commands/simulate.ts
|
|
21286
21642
|
init_source();
|
|
@@ -21413,7 +21769,7 @@ var import_node_fs29 = __toESM(require("node:fs"));
|
|
|
21413
21769
|
var import_node_path26 = __toESM(require("node:path"));
|
|
21414
21770
|
|
|
21415
21771
|
// src/commands/landing-pages.ts
|
|
21416
|
-
var
|
|
21772
|
+
var import_node_crypto8 = __toESM(require("node:crypto"));
|
|
21417
21773
|
var import_node_fs28 = __toESM(require("node:fs"));
|
|
21418
21774
|
var import_node_path25 = __toESM(require("node:path"));
|
|
21419
21775
|
function apiFailure(command, response, fallback2) {
|
|
@@ -21427,7 +21783,7 @@ function apiFailure(command, response, fallback2) {
|
|
|
21427
21783
|
}
|
|
21428
21784
|
}
|
|
21429
21785
|
function sha256(body5) {
|
|
21430
|
-
return
|
|
21786
|
+
return import_node_crypto8.default.createHash("sha256").update(body5).digest("hex");
|
|
21431
21787
|
}
|
|
21432
21788
|
function markerPath(folder) {
|
|
21433
21789
|
return import_node_path25.default.join(folder, LANDING_PAGE_CHECKOUT_MARKER);
|
|
@@ -21502,12 +21858,31 @@ function readCheckoutFiles(folder) {
|
|
|
21502
21858
|
if (!isLandingPageCheckoutPath(relative)) continue;
|
|
21503
21859
|
if (entry.isDirectory()) visit2(absolute);
|
|
21504
21860
|
else if (entry.isFile()) {
|
|
21861
|
+
try {
|
|
21862
|
+
validateLandingPageLocalFiles([
|
|
21863
|
+
...files,
|
|
21864
|
+
{ path: relative, bytes: import_node_fs28.default.statSync(absolute).size, sha256: "" }
|
|
21865
|
+
]);
|
|
21866
|
+
} catch (error) {
|
|
21867
|
+
throw new CliError(
|
|
21868
|
+
error instanceof Error ? error.message : "Invalid bundle",
|
|
21869
|
+
EXIT_CODES.validation
|
|
21870
|
+
);
|
|
21871
|
+
}
|
|
21505
21872
|
const body5 = import_node_fs28.default.readFileSync(absolute);
|
|
21506
21873
|
files.push({ path: relative, sha256: sha256(body5), bytes: body5.byteLength, body: body5 });
|
|
21507
21874
|
}
|
|
21508
21875
|
}
|
|
21509
21876
|
};
|
|
21510
21877
|
visit2(folder);
|
|
21878
|
+
try {
|
|
21879
|
+
validateLandingPageLocalFiles(files);
|
|
21880
|
+
} catch (error) {
|
|
21881
|
+
throw new CliError(
|
|
21882
|
+
error instanceof Error ? error.message : "Invalid bundle",
|
|
21883
|
+
EXIT_CODES.validation
|
|
21884
|
+
);
|
|
21885
|
+
}
|
|
21511
21886
|
return files;
|
|
21512
21887
|
}
|
|
21513
21888
|
function readManifest2(command, folder) {
|
|
@@ -21564,7 +21939,7 @@ function printPlan(plan2) {
|
|
|
21564
21939
|
);
|
|
21565
21940
|
}
|
|
21566
21941
|
console.log(
|
|
21567
|
-
`Deploy target: ${plan2.deploy_target ?? "none
|
|
21942
|
+
`Deploy target: ${plan2.deploy_target ?? "none: set host in apex-landing-page.json to a domain this shop holds"}`
|
|
21568
21943
|
);
|
|
21569
21944
|
}
|
|
21570
21945
|
async function confirm2(message) {
|
|
@@ -21653,7 +22028,10 @@ async function pullCheckout(command, creds, id, folder, force = false) {
|
|
|
21653
22028
|
const target = import_node_path25.default.resolve(folder, file.path);
|
|
21654
22029
|
if (!target.startsWith(folder + import_node_path25.default.sep))
|
|
21655
22030
|
fail(command, `Refusing to write outside the checkout: ${file.path}`);
|
|
21656
|
-
const
|
|
22031
|
+
const download = file.download_url ? await fetch(file.download_url, { signal: AbortSignal.timeout(6e4) }) : null;
|
|
22032
|
+
if (download && !download.ok)
|
|
22033
|
+
fail(command, `Failed to download ${file.path}: ${download.status}`);
|
|
22034
|
+
const body5 = download ? Buffer.from(await download.arrayBuffer()) : file.encoding === "base64" ? Buffer.from(file.content ?? "", "base64") : Buffer.from(file.content ?? "", "utf8");
|
|
21657
22035
|
if (sha256(body5) !== file.sha256)
|
|
21658
22036
|
fail(command, `The server returned ${file.path} incomplete. Pull again.`);
|
|
21659
22037
|
downloaded.push({ path: file.path, target, body: body5, sha256: file.sha256 });
|
|
@@ -21733,7 +22111,10 @@ async function pullCheckout(command, creds, id, folder, force = false) {
|
|
|
21733
22111
|
notes: listing.data.notes ?? []
|
|
21734
22112
|
};
|
|
21735
22113
|
}
|
|
21736
|
-
var
|
|
22114
|
+
var create9 = new Command("create").description("Create a landing page to build outside Apex").requiredOption("--name <name>", "Page name").option(
|
|
22115
|
+
"--slug <slug>",
|
|
22116
|
+
"URL slug (default: from the name; -2, -3 added only if the shop already uses it)"
|
|
22117
|
+
).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) {
|
|
21737
22118
|
const creds = requireCredentials();
|
|
21738
22119
|
const created = await landingPages(creds).create({
|
|
21739
22120
|
name: opts.name,
|
|
@@ -21811,7 +22192,24 @@ var apply = new Command("apply").description("Apply a checkout as a new version"
|
|
|
21811
22192
|
}
|
|
21812
22193
|
const required = new Set(planned.data.files.upload_required);
|
|
21813
22194
|
const toUpload = files.filter((file) => required.has(file.path));
|
|
21814
|
-
for (const
|
|
22195
|
+
for (const file of toUpload.filter((file2) => file2.bytes > LANDING_PAGE_BATCH_BYTES)) {
|
|
22196
|
+
const granted = await api.uploadGrant(marker.landingPageId, {
|
|
22197
|
+
path: file.path,
|
|
22198
|
+
bytes: file.bytes,
|
|
22199
|
+
sha256: file.sha256,
|
|
22200
|
+
base_version_id: planned.data.base_version_id
|
|
22201
|
+
});
|
|
22202
|
+
apiFailure(this, granted, "Failed to grant video upload");
|
|
22203
|
+
await uploadLandingVideo(granted.data, file.body, {
|
|
22204
|
+
progress: (uploaded, total) => {
|
|
22205
|
+
console.error(`Uploading ${file.path}: ${uploaded}/${total} bytes`);
|
|
22206
|
+
}
|
|
22207
|
+
});
|
|
22208
|
+
}
|
|
22209
|
+
for (const batch of landingPageBatches(
|
|
22210
|
+
toUpload.filter((file) => file.bytes <= LANDING_PAGE_BATCH_BYTES),
|
|
22211
|
+
(file) => file.bytes
|
|
22212
|
+
)) {
|
|
21815
22213
|
const uploaded = await api.upload(
|
|
21816
22214
|
marker.landingPageId,
|
|
21817
22215
|
batch.map((file) => ({
|
|
@@ -21860,7 +22258,7 @@ var apply = new Command("apply").description("Apply a checkout as a new version"
|
|
|
21860
22258
|
console.log(`Version ${applied.data.version_number}`);
|
|
21861
22259
|
if (applied.data.preview_url) console.log(`Preview: ${applied.data.preview_url}`);
|
|
21862
22260
|
console.log(
|
|
21863
|
-
`Deploy target: ${applied.data.deploy_target ?? "none
|
|
22261
|
+
`Deploy target: ${applied.data.deploy_target ?? "none: set host in apex-landing-page.json to a domain this shop holds"}`
|
|
21864
22262
|
);
|
|
21865
22263
|
});
|
|
21866
22264
|
});
|
|
@@ -21888,9 +22286,11 @@ async function versionIdFor(command, creds, id, raw) {
|
|
|
21888
22286
|
return match2.id;
|
|
21889
22287
|
}
|
|
21890
22288
|
var SPLIT_KEEPS_SERVING_HELP = "\nWhile an experiment splits this page, visitors keep getting its two arms and its goals stay as they are (new manifest goals are added). The deployed version serves when the experiment ends; its goal changes apply at the next deploy after that.\n";
|
|
21891
|
-
var deploy = new Command("deploy").description(
|
|
22289
|
+
var deploy = new Command("deploy").description(
|
|
22290
|
+
"Deploy a version to the shop's instant host (<shop>.apex-page.com) or a domain this shop holds"
|
|
22291
|
+
).argument("[folder-or-id]", "Checkout folder or landing page id", ".").option(
|
|
21892
22292
|
"--host <host>",
|
|
21893
|
-
"Domain claimed with apex domains claim (default: the manifest host
|
|
22293
|
+
"Domain claimed with apex domains claim (default: the manifest host, else the shop's apex-page.com subdomain)"
|
|
21894
22294
|
).option("--path <path>", "Path on the host").option("--version <number>", "Version number (default: current)").action(async function(target, opts) {
|
|
21895
22295
|
const creds = requireCredentials();
|
|
21896
22296
|
const id = resolvePageId(target);
|
|
@@ -21974,7 +22374,7 @@ var open2 = new Command("open").description("Print the page's dashboard URL").ar
|
|
|
21974
22374
|
output(this, { url }, () => console.log(url));
|
|
21975
22375
|
});
|
|
21976
22376
|
var landingPageWorkspaceCommands = [
|
|
21977
|
-
|
|
22377
|
+
create9,
|
|
21978
22378
|
pull,
|
|
21979
22379
|
plan,
|
|
21980
22380
|
apply,
|
|
@@ -22209,6 +22609,35 @@ program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
22209
22609
|
process.on("beforeExit", () => {
|
|
22210
22610
|
flushFallbackEnvelope();
|
|
22211
22611
|
});
|
|
22212
|
-
|
|
22612
|
+
function isExplicitJsonVersionRequest(args) {
|
|
22613
|
+
let hasJson = false;
|
|
22614
|
+
let hasVersion = false;
|
|
22615
|
+
for (let index = 0; index < args.length; index++) {
|
|
22616
|
+
const arg = args[index];
|
|
22617
|
+
if (arg === "--") break;
|
|
22618
|
+
if (arg === "--json") {
|
|
22619
|
+
hasJson = true;
|
|
22620
|
+
} else if (arg === "--version" || arg === "-V") {
|
|
22621
|
+
hasVersion = true;
|
|
22622
|
+
} else if (arg === "--org" || arg === "--shop") {
|
|
22623
|
+
const value = args[++index];
|
|
22624
|
+
if (!value || value.startsWith("-")) return false;
|
|
22625
|
+
} else if (arg.startsWith("--org=") || arg.startsWith("--shop=")) {
|
|
22626
|
+
const value = arg.slice(arg.indexOf("=") + 1);
|
|
22627
|
+
if (!value || value.startsWith("-")) return false;
|
|
22628
|
+
} else {
|
|
22629
|
+
return false;
|
|
22630
|
+
}
|
|
22631
|
+
}
|
|
22632
|
+
return hasJson && hasVersion;
|
|
22633
|
+
}
|
|
22634
|
+
var explicitJsonVersionRequest = isExplicitJsonVersionRequest(process.argv.slice(2));
|
|
22635
|
+
maybeCheckForUpdate().then(() => {
|
|
22636
|
+
if (explicitJsonVersionRequest) {
|
|
22637
|
+
writeEnvelope({ name: "@drip-apex/apex-cli", version: CLI_VERSION }, "version");
|
|
22638
|
+
return;
|
|
22639
|
+
}
|
|
22640
|
+
return program2.parseAsync(process.argv);
|
|
22641
|
+
}).then(async () => {
|
|
22213
22642
|
await finishInvocation({ exitCode: process.exitCode ?? 0 });
|
|
22214
22643
|
}).catch((err) => handleCliError(err, program2));
|